From b17e47bce05163e4303e3ed50ddff365f3642f04 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Tue, 21 Apr 2020 11:49:26 -0700 Subject: [PATCH 01/15] add extension --- lib/brex_elixirpb.pb.ex | 20 ++++++++++++++++++++ src/brex_elixirpb.proto | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 lib/brex_elixirpb.pb.ex create mode 100644 src/brex_elixirpb.proto diff --git a/lib/brex_elixirpb.pb.ex b/lib/brex_elixirpb.pb.ex new file mode 100644 index 00000000..7124410e --- /dev/null +++ b/lib/brex_elixirpb.pb.ex @@ -0,0 +1,20 @@ +defmodule Brex.Elixirpb.FieldOptions do + @moduledoc false + use Protobuf, syntax: :proto2 + + @type t :: %__MODULE__{ + extype: String.t() + } + defstruct [:extype] + + field :extype, 1, optional: true, type: :string +end + +defmodule Brex.Elixirpb.PbExtension do + @moduledoc false + use Protobuf, syntax: :proto2 + + extend Google.Protobuf.FieldOptions, :field, 65007, + optional: true, + type: Brex.Elixirpb.FieldOptions +end diff --git a/src/brex_elixirpb.proto b/src/brex_elixirpb.proto new file mode 100644 index 00000000..a6138245 --- /dev/null +++ b/src/brex_elixirpb.proto @@ -0,0 +1,23 @@ +syntax = "proto2"; + +package brex.elixirpb; +import "google/protobuf/descriptor.proto"; + +// Sample Field Option Extension +// Defines an extension to specify the elixir type generated for the given field. + +// For example: +// google.protobuf.StringValue my_string = 1 [(brex.elixirpb.field).extype="String.t"]; + +// To compile +//protoc --plugin=./protoc-gen-elixir --proto_path=lib --proto_path=src --elixir_out=lib src/brex_elixirpb.proto + +message FieldOptions { + // Specifies an elixir type to generate for this field. This will override usual type. + optional string extype = 1; +} + +extend google.protobuf.FieldOptions { + // Note: number to change + optional FieldOptions field = 65007; +} \ No newline at end of file From 132cc9d8c5851ed0b69b906049ed5874ff5a204c Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Tue, 21 Apr 2020 11:49:35 -0700 Subject: [PATCH 02/15] add wrappers and timestamps --- lib/google/timestamp.pb.ex | 13 +++++ lib/google/wrappers.pb.ex | 107 +++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 lib/google/timestamp.pb.ex create mode 100644 lib/google/wrappers.pb.ex diff --git a/lib/google/timestamp.pb.ex b/lib/google/timestamp.pb.ex new file mode 100644 index 00000000..e4bfa313 --- /dev/null +++ b/lib/google/timestamp.pb.ex @@ -0,0 +1,13 @@ +defmodule Google.Protobuf.Timestamp do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + seconds: integer, + nanos: integer + } + defstruct [:seconds, :nanos] + + field :seconds, 1, type: :int64 + field :nanos, 2, type: :int32 +end diff --git a/lib/google/wrappers.pb.ex b/lib/google/wrappers.pb.ex new file mode 100644 index 00000000..a6ee1606 --- /dev/null +++ b/lib/google/wrappers.pb.ex @@ -0,0 +1,107 @@ +defmodule Google.Protobuf.DoubleValue do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: float + } + defstruct [:value] + + field :value, 1, type: :double +end + +defmodule Google.Protobuf.FloatValue do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: float + } + defstruct [:value] + + field :value, 1, type: :float +end + +defmodule Google.Protobuf.Int64Value do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: integer + } + defstruct [:value] + + field :value, 1, type: :int64 +end + +defmodule Google.Protobuf.UInt64Value do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: non_neg_integer + } + defstruct [:value] + + field :value, 1, type: :uint64 +end + +defmodule Google.Protobuf.Int32Value do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: integer + } + defstruct [:value] + + field :value, 1, type: :int32 +end + +defmodule Google.Protobuf.UInt32Value do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: non_neg_integer + } + defstruct [:value] + + field :value, 1, type: :uint32 +end + +defmodule Google.Protobuf.BoolValue do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: boolean + } + defstruct [:value] + + field :value, 1, type: :bool +end + +defmodule Google.Protobuf.StringValue do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: String.t() + } + defstruct [:value] + + field :value, 1, type: :string +end + +defmodule Google.Protobuf.BytesValue do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + value: String.t() + } + defstruct [:value] + + field :value, 1, type: :bytes +end From 8f60306caab3cc5d93ad7a7e53027cf1f7928c20 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Tue, 21 Apr 2020 13:29:18 -0700 Subject: [PATCH 03/15] field options are present in generated code --- lib/protobuf/protoc/generator/message.ex | 12 +++++ lib/protobuf/protoc/generator/util.ex | 1 + .../protoc/generator/message_test.exs | 49 +++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/lib/protobuf/protoc/generator/message.ex b/lib/protobuf/protoc/generator/message.ex index 33d631a5..16901cf2 100644 --- a/lib/protobuf/protoc/generator/message.ex +++ b/lib/protobuf/protoc/generator/message.ex @@ -288,8 +288,20 @@ defmodule Protobuf.Protoc.Generator.Message do end defp merge_field_options(opts, f) do + custom_options = + f.options + |> Google.Protobuf.FieldOptions.get_extension(Brex.Elixirpb.PbExtension, :field) + |> case do + nil -> nil + elixir_field_options -> + elixir_field_options + |> Map.from_struct() + |> Enum.into([]) + end + opts |> Map.put(:packed, f.options.packed) |> Map.put(:deprecated, f.options.deprecated) + |> Map.put(:options, custom_options) end end diff --git a/lib/protobuf/protoc/generator/util.ex b/lib/protobuf/protoc/generator/util.ex index a9ac87a5..84ddb41e 100644 --- a/lib/protobuf/protoc/generator/util.ex +++ b/lib/protobuf/protoc/generator/util.ex @@ -49,5 +49,6 @@ defmodule Protobuf.Protoc.Generator.Util do end def print(v) when is_atom(v), do: inspect(v) + def print(v) when is_list(v), do: inspect(v) def print(v), do: v end diff --git a/test/protobuf/protoc/generator/message_test.exs b/test/protobuf/protoc/generator/message_test.exs index 605b1780..dcb2e9e8 100644 --- a/test/protobuf/protoc/generator/message_test.exs +++ b/test/protobuf/protoc/generator/message_test.exs @@ -173,6 +173,55 @@ defmodule Protobuf.Protoc.Generator.MessageTest do assert msg =~ "field :a, 1, optional: true, type: :int32, deprecated: true\n" end + test "generate/2 supports custom field options" do + ctx = %Context{ + dep_type_mapping: %{ + ".brex.elixirpb.FieldOptions" => %{type_name: "Brex.Elixirpb.FieldOptions"}, + ".google.protobuf.StringValue" => %{type_name: "Google.Protobuf.StringValue"} + }, + package: "" + } + + field_opts = Google.Protobuf.FieldOptions.new() + custom_opts = Brex.Elixirpb.FieldOptions.new(extype: "String.t") + + opts = + Google.Protobuf.FieldOptions.put_extension( + field_opts, + Brex.Elixirpb.PbExtension, + :field, + custom_opts + ) + + desc = + Google.Protobuf.DescriptorProto.new( + name: "Foo", + field: [ + Google.Protobuf.FieldDescriptorProto.new( + name: "a", + number: 1, + type: :TYPE_MESSAGE, + type_name: ".google.protobuf.StringValue", + label: :LABEL_OPTIONAL, + options: opts + ), + Google.Protobuf.FieldDescriptorProto.new( + name: "b", + number: 1, + type: :TYPE_MESSAGE, + type_name: ".google.protobuf.StringValue", + label: :LABEL_OPTIONAL + ) + ] + ) + + {[], [msg]} = Generator.generate(ctx, desc) + + assert msg =~ + "field :a, 1, optional: true, type: Google.Protobuf.StringValue, options: [extype: \"String.t\"]\n" + assert msg =~ "field :b, 1, optional: true, type: Google.Protobuf.StringValue\n\nend" + end + test "generete/2 supports message type field" do ctx = %Context{ package: "", From e5a83783ab89f73e0333224bc8f6f0298421a541 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Tue, 21 Apr 2020 13:56:30 -0700 Subject: [PATCH 04/15] field option integration test --- test/protobuf/protoc/integration_test.exs | 4 ++++ test/protobuf/protoc/proto/extension.proto | 7 +++++++ test/protobuf/protoc/proto_gen/extension.pb.ex | 14 ++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/test/protobuf/protoc/integration_test.exs b/test/protobuf/protoc/integration_test.exs index f4f70498..f0243a7d 100644 --- a/test/protobuf/protoc/integration_test.exs +++ b/test/protobuf/protoc/integration_test.exs @@ -54,5 +54,9 @@ defmodule Protobuf.Protoc.IntegrationTest do test "extensions" do assert "hello" == Protobuf.Protoc.ExtTest.Foo.new(a: "hello").a + dual = Protobuf.Protoc.ExtTest.Dual.new(a: Google.Protobuf.StringValue.new(value: "s1"), b: Google.Protobuf.StringValue.new(value: "s2")) + + assert dual.a.value == "s1" + assert dual.b.value == "s2" end end diff --git a/test/protobuf/protoc/proto/extension.proto b/test/protobuf/protoc/proto/extension.proto index 6d3be1f4..0c16512e 100644 --- a/test/protobuf/protoc/proto/extension.proto +++ b/test/protobuf/protoc/proto/extension.proto @@ -4,9 +4,16 @@ package ext; // -I src is needed, see Makefile import "elixirpb.proto"; +import "brex_elixirpb.proto"; +import "google/protobuf/wrappers.proto"; option (elixirpb.file).module_prefix = "Protobuf.Protoc.ExtTest"; message Foo { optional string a = 1; } + +message Dual { + optional google.protobuf.StringValue a = 1 [(brex.elixirpb.field).extype="String.t"]; + optional google.protobuf.StringValue b = 2; +} \ No newline at end of file diff --git a/test/protobuf/protoc/proto_gen/extension.pb.ex b/test/protobuf/protoc/proto_gen/extension.pb.ex index 8fbddfe9..ef5f2c78 100644 --- a/test/protobuf/protoc/proto_gen/extension.pb.ex +++ b/test/protobuf/protoc/proto_gen/extension.pb.ex @@ -9,3 +9,17 @@ defmodule Protobuf.Protoc.ExtTest.Foo do field :a, 1, optional: true, type: :string end + +defmodule Protobuf.Protoc.ExtTest.Dual do + @moduledoc false + use Protobuf, syntax: :proto2 + + @type t :: %__MODULE__{ + a: Google.Protobuf.StringValue.t() | nil, + b: Google.Protobuf.StringValue.t() | nil + } + defstruct [:a, :b] + + field :a, 1, optional: true, type: Google.Protobuf.StringValue, options: [extype: "String.t"] + field :b, 2, optional: true, type: Google.Protobuf.StringValue +end From 2573420c1b46581f683c17ac433867418096830a Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Tue, 21 Apr 2020 14:23:06 -0700 Subject: [PATCH 05/15] field options show up in message props --- lib/protobuf/extension/props.ex | 2 +- lib/protobuf/field_props.ex | 6 ++++-- lib/protobuf/message_props.ex | 2 +- test/protobuf/dsl_test.exs | 13 +++++++++++++ test/protobuf/protoc/integration_test.exs | 2 ++ test/support/test_msg.ex | 14 ++++++++++++++ 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/protobuf/extension/props.ex b/lib/protobuf/extension/props.ex index efef629b..60186943 100644 --- a/lib/protobuf/extension/props.ex +++ b/lib/protobuf/extension/props.ex @@ -5,7 +5,7 @@ defmodule Protobuf.Extension.Props do @moduledoc false @type t :: %__MODULE__{ extendee: module, - field_props: FieldProps.T + field_props: FieldProps.t } defstruct extendee: nil, field_props: nil diff --git a/lib/protobuf/field_props.ex b/lib/protobuf/field_props.ex index 693bed75..c9e414ba 100644 --- a/lib/protobuf/field_props.ex +++ b/lib/protobuf/field_props.ex @@ -17,7 +17,8 @@ defmodule Protobuf.FieldProps do packed?: boolean, map?: boolean, deprecated?: boolean, - encoded_fnum: iodata + encoded_fnum: iodata, + options: Keyword.t() | nil } defstruct fnum: nil, name: nil, @@ -34,5 +35,6 @@ defmodule Protobuf.FieldProps do packed?: nil, map?: false, deprecated?: false, - encoded_fnum: nil + encoded_fnum: nil, + options: nil end diff --git a/lib/protobuf/message_props.ex b/lib/protobuf/message_props.ex index 5afbbe9d..d8b001b1 100644 --- a/lib/protobuf/message_props.ex +++ b/lib/protobuf/message_props.ex @@ -6,7 +6,7 @@ defmodule Protobuf.MessageProps do @type t :: %__MODULE__{ ordered_tags: [integer], tags_map: %{integer => integer}, - field_props: %{integer => FieldProps.T}, + field_props: %{integer => FieldProps.t}, field_tags: %{atom => integer}, repeated_fields: [atom], embedded_fields: [atom], diff --git a/test/protobuf/dsl_test.exs b/test/protobuf/dsl_test.exs index 12b7e254..b4d0598e 100644 --- a/test/protobuf/dsl_test.exs +++ b/test/protobuf/dsl_test.exs @@ -3,6 +3,7 @@ defmodule Protobuf.DSLTest do alias Protobuf.FieldProps alias TestMsg.{Foo, Foo2} + alias TestMsg.Ext.DualUseCase defmodule DefaultSyntax do use Protobuf @@ -139,6 +140,18 @@ defmodule Protobuf.DSLTest do assert %FieldProps{fnum: 17, name: "p", deprecated?: true} = field_props[17] end + test "field options is nil by default" do + msg_props = DualUseCase.__message_props__() + field_props = msg_props.field_props + assert %FieldProps{options: nil} = field_props[2] + end + + test "field options can by keyword list" do + msg_props = DualUseCase.__message_props__() + field_props = msg_props.field_props + assert %FieldProps{options: [extype: "String.t"]} = field_props[1] + end + test "supports enum" do msg_props = TestMsg.EnumFoo.__message_props__() assert msg_props.enum? == true diff --git a/test/protobuf/protoc/integration_test.exs b/test/protobuf/protoc/integration_test.exs index f0243a7d..81423459 100644 --- a/test/protobuf/protoc/integration_test.exs +++ b/test/protobuf/protoc/integration_test.exs @@ -58,5 +58,7 @@ defmodule Protobuf.Protoc.IntegrationTest do assert dual.a.value == "s1" assert dual.b.value == "s2" + + assert %{options: [extype: "String.t"]} = Protobuf.Protoc.ExtTest.Dual.__message_props__().field_props[1] end end diff --git a/test/support/test_msg.ex b/test/support/test_msg.ex index 9d60154d..7fdf0a34 100644 --- a/test/support/test_msg.ex +++ b/test/support/test_msg.ex @@ -212,4 +212,18 @@ defmodule TestMsg do extend Ext.Foo2, :bar, 1047, optional: true, type: :string extend Ext.Foo1, :"Parent.foo", 1048, optional: true, type: Ext.EnumFoo, enum: true end + + defmodule Ext.DualUseCase do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + a: String.t() | nil, + b: Google.Protobuf.StringValue.t() | nil + } + defstruct [:a, :b] + + field :a, 1, optional: true, type: Google.Protobuf.StringValue, options: [extype: "String.t"] + field :b, 2, optional: true, type: Google.Protobuf.StringValue + end end From 6be3df9dbae6ad724712c4d256b63f903cb428b9 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Tue, 21 Apr 2020 16:49:37 -0700 Subject: [PATCH 06/15] custom_field_options? as cli on off switch --- Makefile | 1 + lib/protobuf/protoc/cli.ex | 5 ++ lib/protobuf/protoc/context.ex | 6 +- lib/protobuf/protoc/generator/message.ex | 25 +++++++-- test/protobuf/protoc/cli_test.exs | 6 ++ .../protoc/generator/message_test.exs | 56 ++++++++++++++++++- .../protobuf/protoc/proto_gen/extension.pb.ex | 4 +- 7 files changed, 93 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 6fbacd16..deb0b548 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ gen-google-protos: protoc-gen-elixir gen-protos: protoc-gen-elixir protoc -I src -I test/protobuf/protoc/proto --elixir_out=test/protobuf/protoc/proto_gen --plugin=./protoc-gen-elixir test/protobuf/protoc/proto/*.proto + protoc -I src -I test/protobuf/protoc/proto --elixir_out=custom_field_options?=true:test/protobuf/protoc/proto_gen --plugin=./protoc-gen-elixir test/protobuf/protoc/proto/extension.proto protoc -I src --elixir_out=lib --plugin=./protoc-gen-elixir elixirpb.proto .PHONY: clean gen_google_proto gen_test_protos diff --git a/lib/protobuf/protoc/cli.ex b/lib/protobuf/protoc/cli.ex index 58a15047..1f014264 100644 --- a/lib/protobuf/protoc/cli.ex +++ b/lib/protobuf/protoc/cli.ex @@ -66,6 +66,11 @@ defmodule Protobuf.Protoc.CLI do parse_params(ctx, t) end + def parse_params(ctx, ["custom_field_options?=true" | t]) do + ctx = %{ctx | custom_field_options?: true} + parse_params(ctx, t) + end + def parse_params(ctx, _), do: ctx @doc false diff --git a/lib/protobuf/protoc/context.ex b/lib/protobuf/protoc/context.ex index 9c7dacbf..b88a3c1e 100644 --- a/lib/protobuf/protoc/context.ex +++ b/lib/protobuf/protoc/context.ex @@ -29,7 +29,11 @@ defmodule Protobuf.Protoc.Context do gen_descriptors?: false, # Elixirpb.FileOptions - custom_file_options: %{} + custom_file_options: %{}, + + # Brex.Elixirpb.FieldOptions + # Note: Right now just true or false, could have more complex values later + custom_field_options?: false def cal_file_options(ctx, nil) do %{ctx | custom_file_options: %{}, module_prefix: ctx.package || ""} diff --git a/lib/protobuf/protoc/generator/message.ex b/lib/protobuf/protoc/generator/message.ex index 16901cf2..da0aa75c 100644 --- a/lib/protobuf/protoc/generator/message.ex +++ b/lib/protobuf/protoc/generator/message.ex @@ -76,15 +76,22 @@ defmodule Protobuf.Protoc.Generator.Message do inspect(exts) end - def msg_opts_str(%{syntax: syntax}, opts) do + def msg_opts_str(%{syntax: syntax, custom_field_options?: custom_field_options}, opts) do msg_options = opts opts = %{ syntax: syntax, map: msg_options && msg_options.map_entry, - deprecated: msg_options && msg_options.deprecated + deprecated: msg_options && msg_options.deprecated, } + opts = + if custom_field_options do + Map.put(opts, :custom_field_options?, true) + else + opts + end + str = Util.options_to_str(opts) if String.length(str) > 0, do: ", " <> str, else: "" end @@ -177,7 +184,7 @@ defmodule Protobuf.Protoc.Generator.Message do end def get_field(ctx, f, nested_maps, oneofs) do - opts = field_options(f) + opts = field_options(ctx, f) map = nested_maps[f.type_name] opts = if map, do: Map.put(opts, :map, true), else: opts @@ -238,9 +245,9 @@ defmodule Protobuf.Protoc.Generator.Message do end) end - defp field_options(f) do + defp field_options(ctx, f) do opts = %{enum: f.type == :TYPE_ENUM, default: default_value(f.type, f.default_value)} - if f.options, do: merge_field_options(opts, f), else: opts + if f.options, do: merge_field_options(ctx, opts, f), else: opts end defp label_name(:LABEL_OPTIONAL), do: "optional" @@ -287,7 +294,7 @@ defmodule Protobuf.Protoc.Generator.Message do end end - defp merge_field_options(opts, f) do + defp merge_field_options(%{custom_field_options?: true}, opts, f) do custom_options = f.options |> Google.Protobuf.FieldOptions.get_extension(Brex.Elixirpb.PbExtension, :field) @@ -304,4 +311,10 @@ defmodule Protobuf.Protoc.Generator.Message do |> Map.put(:deprecated, f.options.deprecated) |> Map.put(:options, custom_options) end + + defp merge_field_options(_ctx, opts, f) do + opts + |> Map.put(:packed, f.options.packed) + |> Map.put(:deprecated, f.options.deprecated) + end end diff --git a/test/protobuf/protoc/cli_test.exs b/test/protobuf/protoc/cli_test.exs index 0a627a40..290a3c00 100644 --- a/test/protobuf/protoc/cli_test.exs +++ b/test/protobuf/protoc/cli_test.exs @@ -14,6 +14,12 @@ defmodule Protobuf.Protoc.CLITest do assert ctx == %Context{plugins: ["grpc"], gen_descriptors?: true} end + test "parse_params/2 parse custom_field_options" do + ctx = %Context{} + ctx = parse_params(ctx, "plugins=grpc,custom_field_options?=true") + assert ctx == %Context{plugins: ["grpc"], custom_field_options?: true} + end + test "find_types/2 returns multiple files" do ctx = %Context{} descs = [FileDescriptorProto.new(name: "file1"), FileDescriptorProto.new(name: "file2")] diff --git a/test/protobuf/protoc/generator/message_test.exs b/test/protobuf/protoc/generator/message_test.exs index dcb2e9e8..6697e31d 100644 --- a/test/protobuf/protoc/generator/message_test.exs +++ b/test/protobuf/protoc/generator/message_test.exs @@ -173,13 +173,66 @@ defmodule Protobuf.Protoc.Generator.MessageTest do assert msg =~ "field :a, 1, optional: true, type: :int32, deprecated: true\n" end + test "generate/2 output unchanged if custom_field_options? is false" do + ctx = %Context{ + dep_type_mapping: %{ + ".brex.elixirpb.FieldOptions" => %{type_name: "Brex.Elixirpb.FieldOptions"}, + ".google.protobuf.StringValue" => %{type_name: "Google.Protobuf.StringValue"} + }, + package: "", + } + + refute ctx.custom_field_options? + + field_opts = Google.Protobuf.FieldOptions.new() + custom_opts = Brex.Elixirpb.FieldOptions.new(extype: "String.t") + + opts = + Google.Protobuf.FieldOptions.put_extension( + field_opts, + Brex.Elixirpb.PbExtension, + :field, + custom_opts + ) + + desc = + Google.Protobuf.DescriptorProto.new( + name: "Foo", + field: [ + Google.Protobuf.FieldDescriptorProto.new( + name: "a", + number: 1, + type: :TYPE_MESSAGE, + type_name: ".google.protobuf.StringValue", + label: :LABEL_OPTIONAL, + options: opts + ), + Google.Protobuf.FieldDescriptorProto.new( + name: "b", + number: 1, + type: :TYPE_MESSAGE, + type_name: ".google.protobuf.StringValue", + label: :LABEL_OPTIONAL + ) + ] + ) + + {[], [msg]} = Generator.generate(ctx, desc) + + assert msg =~ "use Protobuf\n\n" + assert msg =~ + "field :a, 1, optional: true, type: Google.Protobuf.StringValue\n" + assert msg =~ "field :b, 1, optional: true, type: Google.Protobuf.StringValue\n\nend" + end + test "generate/2 supports custom field options" do ctx = %Context{ dep_type_mapping: %{ ".brex.elixirpb.FieldOptions" => %{type_name: "Brex.Elixirpb.FieldOptions"}, ".google.protobuf.StringValue" => %{type_name: "Google.Protobuf.StringValue"} }, - package: "" + package: "", + custom_field_options?: true } field_opts = Google.Protobuf.FieldOptions.new() @@ -217,6 +270,7 @@ defmodule Protobuf.Protoc.Generator.MessageTest do {[], [msg]} = Generator.generate(ctx, desc) + assert msg =~ "use Protobuf, custom_field_options?: true" assert msg =~ "field :a, 1, optional: true, type: Google.Protobuf.StringValue, options: [extype: \"String.t\"]\n" assert msg =~ "field :b, 1, optional: true, type: Google.Protobuf.StringValue\n\nend" diff --git a/test/protobuf/protoc/proto_gen/extension.pb.ex b/test/protobuf/protoc/proto_gen/extension.pb.ex index ef5f2c78..82b45085 100644 --- a/test/protobuf/protoc/proto_gen/extension.pb.ex +++ b/test/protobuf/protoc/proto_gen/extension.pb.ex @@ -1,6 +1,6 @@ defmodule Protobuf.Protoc.ExtTest.Foo do @moduledoc false - use Protobuf, syntax: :proto2 + use Protobuf, custom_field_options?: true, syntax: :proto2 @type t :: %__MODULE__{ a: String.t() @@ -12,7 +12,7 @@ end defmodule Protobuf.Protoc.ExtTest.Dual do @moduledoc false - use Protobuf, syntax: :proto2 + use Protobuf, custom_field_options?: true, syntax: :proto2 @type t :: %__MODULE__{ a: Google.Protobuf.StringValue.t() | nil, From 278e55cc4627ccff8ffdc50d7feb668fb7468122 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Tue, 21 Apr 2020 18:05:12 -0700 Subject: [PATCH 07/15] support custom type spec for fields with custom options --- lib/protobuf/field_options_processor.ex | 25 +++++++++++ lib/protobuf/protoc/generator/message.ex | 20 +++++---- .../protobuf/field_options_processor_test.exs | 41 +++++++++++++++++++ .../protoc/generator/message_test.exs | 7 +++- .../protobuf/protoc/proto_gen/extension.pb.ex | 2 +- 5 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 lib/protobuf/field_options_processor.ex create mode 100644 test/protobuf/field_options_processor_test.exs diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex new file mode 100644 index 00000000..13c62b3e --- /dev/null +++ b/lib/protobuf/field_options_processor.ex @@ -0,0 +1,25 @@ +defmodule Protobuf.FieldOptionsProcessor do + @moduledoc """ + Defines hooks to process custom field options. + """ + + @type options :: Keyword.t(String.t) + + @callback type_to_spec(type_enum :: atom, type :: String.t(), repeated :: boolean, options) :: String.t() + + def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t()" = extype]), do: extype + def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t" = extype]), do: extype + def validate_options_str!(_, type, options) do + raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" + end + + def type_to_spec(type_enum, type, repeated, options) do + extype = validate_options_str!(type_enum, type, options) + type_str = extype <> " | nil" + if repeated do + "[#{type_str}]" + else + type_str + end + end +end diff --git a/lib/protobuf/protoc/generator/message.ex b/lib/protobuf/protoc/generator/message.ex index da0aa75c..b05f1d25 100644 --- a/lib/protobuf/protoc/generator/message.ex +++ b/lib/protobuf/protoc/generator/message.ex @@ -30,7 +30,7 @@ defmodule Protobuf.Protoc.Generator.Message do name: Util.mod_name(ctx, new_ns), options: msg_opts_str(ctx, desc.options), structs: structs_str(desc, extensions), - typespec: typespec_str(fields, desc.oneof_decl, extensions), + typespec: typespec_str(ctx, fields, desc.oneof_decl, extensions), fields: fields, oneofs: oneofs_str(desc.oneof_decl), desc: generate_desc, @@ -109,10 +109,10 @@ defmodule Protobuf.Protoc.Generator.Message do Enum.map_join(struct.oneof_decl ++ fields, ", ", fn f -> ":#{f.name}" end) end - def typespec_str([], [], []), do: " @type t :: %__MODULE__{}\n" - def typespec_str([], [], [_ | _]), do: " @type t :: %__MODULE__{__pb_extensions__: map}\n" + def typespec_str(_ctx, [], [], []), do: " @type t :: %__MODULE__{}\n" + def typespec_str(_ctx, [], [], [_ | _]), do: " @type t :: %__MODULE__{__pb_extensions__: map}\n" - def typespec_str(fields, oneofs, extensions) do + def typespec_str(ctx, fields, oneofs, extensions) do longest_field = fields |> Enum.max_by(&String.length(&1[:name])) longest_width = String.length(longest_field[:name]) fields = Enum.filter(fields, fn f -> !f[:oneof] end) @@ -125,7 +125,7 @@ defmodule Protobuf.Protoc.Generator.Message do types = types ++ Enum.map(fields, fn f -> - {fmt_type_name(f[:name], longest_width), fmt_type(f)} + {fmt_type_name(f[:name], longest_width), fmt_type(ctx, f)} end) types = @@ -153,17 +153,21 @@ defmodule Protobuf.Protoc.Generator.Message do String.pad_trailing("#{name}:", len + 1) end - defp fmt_type(%{opts: %{map: true}, map: {{k_type, k_name}, {v_type, v_name}}}) do + defp fmt_type(%{custom_field_options?: true}, %{label: label, type_enum: type_enum, type: type, opts: %{options: options}}) when not is_nil(options) do + repeated = if label == "repeated", do: true, else: false + "#{Protobuf.FieldOptionsProcessor.type_to_spec(type_enum, type, repeated, options)}" + end + defp fmt_type(_ctx, %{opts: %{map: true}, map: {{k_type, k_name}, {v_type, v_name}}}) do k_type = type_to_spec(k_type, k_name) v_type = type_to_spec(v_type, v_name) "%{#{k_type} => #{v_type}}" end - defp fmt_type(%{label: "repeated", type_enum: type_enum, type: type}) do + defp fmt_type(_ctx, %{label: "repeated", type_enum: type_enum, type: type}) do "[#{type_to_spec(type_enum, type, true)}]" end - defp fmt_type(%{type_enum: type_enum, type: type}) do + defp fmt_type(_ctx, %{type_enum: type_enum, type: type}) do "#{type_to_spec(type_enum, type)}" end diff --git a/test/protobuf/field_options_processor_test.exs b/test/protobuf/field_options_processor_test.exs new file mode 100644 index 00000000..6861a15f --- /dev/null +++ b/test/protobuf/field_options_processor_test.exs @@ -0,0 +1,41 @@ +defmodule Protobuf.FieldOptionsProcessorTest do + use ExUnit.Case, async: true + + alias Protobuf.FieldOptionsProcessor + + test "type_to_spec String.t and StringValue" do + extype = "String.t" + assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", false, [extype: extype]) == + extype <> " | nil" + end + + test "type_to_spec String.t() and StringValue" do + extype = "String.t()" + assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", false, [extype: extype]) == + extype <> " | nil" + end + + test "type_to_spec repeated" do + extype = "String.t()" + assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", true, [extype: extype]) == + "[String.t() | nil]" + end + + test "type_to_spec invalid extype" do + extype = "integer" + assert_raise RuntimeError, "The custom field option is invalid. " <> + "Options: [extype: \"integer\"] incompatible with type: Google.Protobuf.StringValue", + fn -> + FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", false, [extype: "integer"]) + end + end + + test "type_to_spec invalid type" do + extype = "String.t()" + assert_raise RuntimeError, "The custom field option is invalid. " <> + "Options: [extype: \"String.t()\"] incompatible with type: Google.Protobuf.UnrealValue", + fn -> + FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.UnrealValue", false, [extype: extype]) + end + end +end diff --git a/test/protobuf/protoc/generator/message_test.exs b/test/protobuf/protoc/generator/message_test.exs index 6697e31d..5cdd3b48 100644 --- a/test/protobuf/protoc/generator/message_test.exs +++ b/test/protobuf/protoc/generator/message_test.exs @@ -220,6 +220,8 @@ defmodule Protobuf.Protoc.Generator.MessageTest do {[], [msg]} = Generator.generate(ctx, desc) assert msg =~ "use Protobuf\n\n" + assert msg =~ "a: Google.Protobuf.StringValue.t | nil" + assert msg =~ "b: Google.Protobuf.StringValue.t | nil" assert msg =~ "field :a, 1, optional: true, type: Google.Protobuf.StringValue\n" assert msg =~ "field :b, 1, optional: true, type: Google.Protobuf.StringValue\n\nend" @@ -271,8 +273,9 @@ defmodule Protobuf.Protoc.Generator.MessageTest do {[], [msg]} = Generator.generate(ctx, desc) assert msg =~ "use Protobuf, custom_field_options?: true" - assert msg =~ - "field :a, 1, optional: true, type: Google.Protobuf.StringValue, options: [extype: \"String.t\"]\n" + assert msg =~ "a: String.t | nil" + assert msg =~ "b: Google.Protobuf.StringValue.t | nil" + assert msg =~ "field :a, 1, optional: true, type: Google.Protobuf.StringValue, options: [extype: \"String.t\"]\n" assert msg =~ "field :b, 1, optional: true, type: Google.Protobuf.StringValue\n\nend" end diff --git a/test/protobuf/protoc/proto_gen/extension.pb.ex b/test/protobuf/protoc/proto_gen/extension.pb.ex index 82b45085..dd66190c 100644 --- a/test/protobuf/protoc/proto_gen/extension.pb.ex +++ b/test/protobuf/protoc/proto_gen/extension.pb.ex @@ -15,7 +15,7 @@ defmodule Protobuf.Protoc.ExtTest.Dual do use Protobuf, custom_field_options?: true, syntax: :proto2 @type t :: %__MODULE__{ - a: Google.Protobuf.StringValue.t() | nil, + a: String.t() | nil, b: Google.Protobuf.StringValue.t() | nil } defstruct [:a, :b] From 08deef5f107a2c0daca181fcf3c85b000341efd7 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Tue, 21 Apr 2020 21:08:38 -0700 Subject: [PATCH 08/15] add type defaults & new function for tagged fields --- lib/protobuf/builder.ex | 16 +++++++-------- lib/protobuf/field_options_processor.ex | 20 +++++++++++++++++++ test/protobuf/builder_test.exs | 20 +++++++++++++++++++ .../protobuf/field_options_processor_test.exs | 2 +- test/protobuf/protoc/integration_test.exs | 4 ++-- 5 files changed, 51 insertions(+), 11 deletions(-) diff --git a/lib/protobuf/builder.ex b/lib/protobuf/builder.ex index e13124a5..3dc02de6 100644 --- a/lib/protobuf/builder.ex +++ b/lib/protobuf/builder.ex @@ -13,6 +13,9 @@ defmodule Protobuf.Builder do new_maybe_strict(mod, attrs, _strict? = true) end + def field_default(_, %{options: options} = props) when not is_nil(options) do + Protobuf.FieldOptionsProcessor.type_default(props.type, options) + end def field_default(_, %{default: default}) when not is_nil(default), do: default def field_default(_, %{repeated?: true}), do: [] def field_default(_, %{map?: true}), do: %{} @@ -69,14 +72,11 @@ defmodule Protobuf.Builder do f_props = props.field_props[props.field_tags[k]] v = - if f_props.embedded? do - if f_props.repeated? do - Enum.map(v, fn i -> f_props.type.new(i) end) - else - f_props.type.new(v) - end - else - v + cond do + not is_nil(f_props.options) -> Protobuf.FieldOptionsProcessor.new(f_props.type, v, f_props.options) + f_props.embedded? and f_props.repeated? -> Enum.map(v, fn i -> f_props.type.new(i) end) + f_props.embedded? -> f_props.type.new(v) + true -> v end Map.put(acc, k, v) diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex index 13c62b3e..2a7c3b54 100644 --- a/lib/protobuf/field_options_processor.ex +++ b/lib/protobuf/field_options_processor.ex @@ -6,6 +6,8 @@ defmodule Protobuf.FieldOptionsProcessor do @type options :: Keyword.t(String.t) @callback type_to_spec(type_enum :: atom, type :: String.t(), repeated :: boolean, options) :: String.t() + @callback type_default(type :: atom, options) :: any + @callback new(type :: atom, value :: any, options) :: any # TODO what type? def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t()" = extype]), do: extype def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t" = extype]), do: extype @@ -13,6 +15,13 @@ defmodule Protobuf.FieldOptionsProcessor do raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" end + def validate_options!(Google.Protobuf.StringValue, [extype: "String.t()" = extype]), do: extype + def validate_options!(Google.Protobuf.StringValue, [extype: "String.t" = extype]), do: extype + def validate_options!(type, options) do + raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" + end + + def type_to_spec(type_enum, type, repeated, options) do extype = validate_options_str!(type_enum, type, options) type_str = extype <> " | nil" @@ -22,4 +31,15 @@ defmodule Protobuf.FieldOptionsProcessor do type_str end end + + def type_default(type, options) do + validate_options!(type, options) + nil + end + + # Note: Could do type check here if we wanted to. + def new(type, value, options) do + validate_options!(type, options) + value + end end diff --git a/test/protobuf/builder_test.exs b/test/protobuf/builder_test.exs index 0eee47cc..1030a266 100644 --- a/test/protobuf/builder_test.exs +++ b/test/protobuf/builder_test.exs @@ -2,6 +2,7 @@ defmodule Protobuf.BuilderTest do use ExUnit.Case, async: true alias TestMsg.{Foo, Foo2, Link} + alias TestMsg.Ext.DualUseCase test "new/2 uses default values for proto3" do assert Foo.new().a == 0 @@ -67,4 +68,23 @@ defmodule Protobuf.BuilderTest do Foo.encode(foo) end end + + test "new/2 correct defaults for custom_field_options" do + assert %DualUseCase{a: nil, b: nil} == DualUseCase.new() + end + + test "new/2 build for custom_field_options" do + assert %DualUseCase{a: "s1", b: nil} == DualUseCase.new(a: "s1") + end + + test "new/2 build for custom_field_options, bad value" do + assert_raise Protocol.UndefinedError, + fn -> DualUseCase.new(a: "s1", b: "s2") end + end + + test "new/2 build for custom_field_options, doesn't type check value" do + # Should be just string + assert %DualUseCase{a: %Google.Protobuf.StringValue{value: "s1"}} = + DualUseCase.new!(a: %Google.Protobuf.StringValue{value: "s1"}) + end end diff --git a/test/protobuf/field_options_processor_test.exs b/test/protobuf/field_options_processor_test.exs index 6861a15f..6454c626 100644 --- a/test/protobuf/field_options_processor_test.exs +++ b/test/protobuf/field_options_processor_test.exs @@ -26,7 +26,7 @@ defmodule Protobuf.FieldOptionsProcessorTest do assert_raise RuntimeError, "The custom field option is invalid. " <> "Options: [extype: \"integer\"] incompatible with type: Google.Protobuf.StringValue", fn -> - FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", false, [extype: "integer"]) + FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", false, [extype: extype]) end end diff --git a/test/protobuf/protoc/integration_test.exs b/test/protobuf/protoc/integration_test.exs index 81423459..f44df8ad 100644 --- a/test/protobuf/protoc/integration_test.exs +++ b/test/protobuf/protoc/integration_test.exs @@ -54,9 +54,9 @@ defmodule Protobuf.Protoc.IntegrationTest do test "extensions" do assert "hello" == Protobuf.Protoc.ExtTest.Foo.new(a: "hello").a - dual = Protobuf.Protoc.ExtTest.Dual.new(a: Google.Protobuf.StringValue.new(value: "s1"), b: Google.Protobuf.StringValue.new(value: "s2")) + dual = Protobuf.Protoc.ExtTest.Dual.new(a: "s1", b: Google.Protobuf.StringValue.new(value: "s2")) - assert dual.a.value == "s1" + assert dual.a == "s1" assert dual.b.value == "s2" assert %{options: [extype: "String.t"]} = Protobuf.Protoc.ExtTest.Dual.__message_props__().field_props[1] From 8bf18f59b76a9e255033f394fcf23dfe59dc45ee Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Wed, 22 Apr 2020 09:02:35 -0700 Subject: [PATCH 09/15] add encoding --- lib/protobuf/encoder.ex | 21 +++++++++++++++++++++ lib/protobuf/field_options_processor.ex | 22 ++++++++++++++++++---- test/protobuf/encoder_test.exs | 5 +++++ test/protobuf/encoder_validation_test.exs | 20 ++++++++++++++++++++ test/support/test_msg.ex | 14 ++++++++++++++ 5 files changed, 78 insertions(+), 4 deletions(-) diff --git a/lib/protobuf/encoder.ex b/lib/protobuf/encoder.ex index dc48df72..beb8c4bf 100644 --- a/lib/protobuf/encoder.ex +++ b/lib/protobuf/encoder.ex @@ -98,6 +98,22 @@ defmodule Protobuf.Encoder do def skip_field?(_, _, _), do: false @spec encode_field(atom, any, FieldProps.t()) :: iodata + defp encode_field( + :custom, + val, + %{encoded_fnum: fnum, repeated?: is_repeated, map?: is_map, type: type, options: options} = + prop + ) do + repeated = is_repeated || is_map + + repeated_or_not(val, repeated, fn v -> + v = if is_map, do: struct(prop.type, %{key: elem(v, 0), value: elem(v, 1)}), else: v + encoded = Protobuf.FieldOptionsProcessor.encode_type(type, v, options) + byte_size = byte_size(encoded) + [fnum, encode_varint(byte_size), encoded] + end) + end + defp encode_field(:normal, val, %{encoded_fnum: fnum, type: type, repeated?: is_repeated}) do repeated_or_not(val, is_repeated, fn v -> [fnum, encode_type(type, v)] @@ -126,6 +142,11 @@ defmodule Protobuf.Encoder do [fnum, encode_varint(byte_size), encoded] end + @spec class_field(map) :: atom + defp class_field(%{options: options}) when not is_nil(options) do + :custom + end + @spec class_field(map) :: atom defp class_field(%{wire_type: wire_delimited(), embedded?: true}) do :embedded diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex index 2a7c3b54..557829d5 100644 --- a/lib/protobuf/field_options_processor.ex +++ b/lib/protobuf/field_options_processor.ex @@ -4,10 +4,12 @@ defmodule Protobuf.FieldOptionsProcessor do """ @type options :: Keyword.t(String.t) + @type type :: atom @callback type_to_spec(type_enum :: atom, type :: String.t(), repeated :: boolean, options) :: String.t() - @callback type_default(type :: atom, options) :: any - @callback new(type :: atom, value :: any, options) :: any # TODO what type? + @callback type_default(type, options) :: any + @callback new(type, value :: any, options) :: struct | any # TODO what type? + @callback encode_type(type, v :: any, options) :: binary def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t()" = extype]), do: extype def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t" = extype]), do: extype @@ -15,8 +17,8 @@ defmodule Protobuf.FieldOptionsProcessor do raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" end - def validate_options!(Google.Protobuf.StringValue, [extype: "String.t()" = extype]), do: extype - def validate_options!(Google.Protobuf.StringValue, [extype: "String.t" = extype]), do: extype + def validate_options!(Google.Protobuf.StringValue, [extype: "String.t()"]), do: :string + def validate_options!(Google.Protobuf.StringValue, [extype: "String.t"]), do: :string def validate_options!(type, options) do raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" end @@ -42,4 +44,16 @@ defmodule Protobuf.FieldOptionsProcessor do validate_options!(type, options) value end + + def encode_type(type, v, options) do + extype = validate_options!(type, options) + encoded = do_encode_type(type, v, extype) + IO.iodata_to_binary(encoded) + end + + defp do_encode_type(type, v, extype) do + fnum = type.__message_props__.field_props[1].encoded_fnum + encoded = Protobuf.Encoder.encode_type(extype, v) + [[fnum, encoded]] + end end diff --git a/test/protobuf/encoder_test.exs b/test/protobuf/encoder_test.exs index c7f988a7..1ef99b64 100644 --- a/test/protobuf/encoder_test.exs +++ b/test/protobuf/encoder_test.exs @@ -181,4 +181,9 @@ defmodule Protobuf.EncoderTest do msg = TestMsg.Bar2.new(a: 0, b: 1) assert Encoder.encode(msg) == <<8, 0, 16, 1>> end + + test "encoding with custom field options" do + msg = TestMsg.Ext.DualUseCase.new(a: "s1", b: Google.Protobuf.StringValue.new(value: "s2")) + assert Encoder.encode(msg) == <<10, 4, 10, 2, 115, 49, 18, 4, 10, 2, 115, 50>> + end end diff --git a/test/protobuf/encoder_validation_test.exs b/test/protobuf/encoder_validation_test.exs index bdc124c2..d17f6934 100644 --- a/test/protobuf/encoder_validation_test.exs +++ b/test/protobuf/encoder_validation_test.exs @@ -148,4 +148,24 @@ defmodule Protobuf.EncoderTest.Validation do assert Protobuf.Encoder.encode(msg) == Protobuf.Encoder.encode(msg1) end + + test "field with custom options is valid" do + msg = TestMsg.Ext.DualUseCase.new(a: "s1", b: Google.Protobuf.StringValue.new(value: "s2")) + msg1 = TestMsg.Ext.DualNonUse.new(a: Google.Protobuf.StringValue.new(value: "s1"), b: Google.Protobuf.StringValue.new(value: "s2")) + + assert Protobuf.Encoder.encode(msg) == Protobuf.Encoder.encode(msg1) + end + + test "field with custom options is valid, empty structs" do + msg = TestMsg.Ext.DualUseCase.new() + msg1 = TestMsg.Ext.DualNonUse.new() + + assert Protobuf.Encoder.encode(msg) == Protobuf.Encoder.encode(msg1) + end + + test "field with custom options, bad values" do + msg = TestMsg.Ext.DualUseCase.new(a: Google.Protobuf.StringValue.new(value: "s1"), b: Google.Protobuf.StringValue.new(value: "s2")) + + assert_raise Protobuf.EncodeError, fn -> Protobuf.Encoder.encode(msg) end + end end diff --git a/test/support/test_msg.ex b/test/support/test_msg.ex index 7fdf0a34..858abb12 100644 --- a/test/support/test_msg.ex +++ b/test/support/test_msg.ex @@ -226,4 +226,18 @@ defmodule TestMsg do field :a, 1, optional: true, type: Google.Protobuf.StringValue, options: [extype: "String.t"] field :b, 2, optional: true, type: Google.Protobuf.StringValue end + + defmodule Ext.DualNonUse do + @moduledoc false + use Protobuf, syntax: :proto3 + + @type t :: %__MODULE__{ + a: String.t() | nil, + b: Google.Protobuf.StringValue.t() | nil + } + defstruct [:a, :b] + + field :a, 1, optional: true, type: Google.Protobuf.StringValue + field :b, 2, optional: true, type: Google.Protobuf.StringValue + end end From 9c2c199488918bc38e6f9d4cda0284b44f8bcf78 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Wed, 22 Apr 2020 09:38:13 -0700 Subject: [PATCH 10/15] support decoding --- lib/protobuf/decoder.ex | 35 +++++++++++++++-------- lib/protobuf/field_options_processor.ex | 32 +++++++++++++++++++-- test/protobuf/decoder_test.exs | 23 +++++++++++++++ test/protobuf/encoder_test.exs | 5 ++++ test/protobuf/protoc/integration_test.exs | 4 +++ 5 files changed, 85 insertions(+), 14 deletions(-) diff --git a/lib/protobuf/decoder.ex b/lib/protobuf/decoder.ex index 8cd2b9d4..9a24f752 100644 --- a/lib/protobuf/decoder.ex +++ b/lib/protobuf/decoder.ex @@ -142,14 +142,15 @@ defmodule Protobuf.Decoder do type: type, oneof: oneof, name_atom: name_atom, - embedded?: embedded + embedded?: embedded, + options: options } = prop } -> key = if oneof, do: oneof_field(prop, msg_props), else: name_atom struct = - if embedded do - embedded_msg = decode(val, type) + if not is_nil(options) do + embedded_msg = Protobuf.FieldOptionsProcessor.decode_type(val, type, options) val = if is_map, do: %{embedded_msg.key => embedded_msg.value}, else: embedded_msg val = if oneof, do: {name_atom, val}, else: val @@ -157,17 +158,27 @@ defmodule Protobuf.Decoder do Map.put(struct, key, val) else - val = decode_type_m(type, key, val) - val = if oneof, do: {name_atom, val}, else: val + if embedded do + embedded_msg = decode(val, type) + val = if is_map, do: %{embedded_msg.key => embedded_msg.value}, else: embedded_msg + val = if oneof, do: {name_atom, val}, else: val - val = - if is_repeated do - merge_simple_repeated_value(struct, key, val) - else - val - end + val = merge_embedded_value(struct, key, val, is_repeated) - Map.put(struct, key, val) + Map.put(struct, key, val) + else + val = decode_type_m(type, key, val) + val = if oneof, do: {name_atom, val}, else: val + + val = + if is_repeated do + merge_simple_repeated_value(struct, key, val) + else + val + end + + Map.put(struct, key, val) + end end build_struct(rest, msg_props, struct) diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex index 557829d5..22db2323 100644 --- a/lib/protobuf/field_options_processor.ex +++ b/lib/protobuf/field_options_processor.ex @@ -3,13 +3,27 @@ defmodule Protobuf.FieldOptionsProcessor do Defines hooks to process custom field options. """ + @typedoc """ + Keyword list of field options. Right now only [extype: mytype]. + """ @type options :: Keyword.t(String.t) + + @typedoc """ + The existing type of the field. Often the module name of the struct. + """ @type type :: atom + @typedoc """ + A value with type extype. + """ + @type value :: struct | any + + @callback type_to_spec(type_enum :: atom, type :: String.t(), repeated :: boolean, options) :: String.t() @callback type_default(type, options) :: any - @callback new(type, value :: any, options) :: struct | any # TODO what type? - @callback encode_type(type, v :: any, options) :: binary + @callback new(type, value, options) :: value + @callback encode_type(type, value, options) :: binary + @callback decode_type(val :: binary, type, options) :: value def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t()" = extype]), do: extype def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t" = extype]), do: extype @@ -56,4 +70,18 @@ defmodule Protobuf.FieldOptionsProcessor do encoded = Protobuf.Encoder.encode_type(extype, v) [[fnum, encoded]] end + + def decode_type(val, type, options) do + extype = validate_options!(type, options) + do_decode_type(type, val, extype) + end + + defp do_decode_type(_type, val, extype) do + require Logger + require Protobuf.Decoder + import Protobuf.Decoder, only: [decode_zigzag: 1] + + [_tag, _wire, val | _rest] = Protobuf.Decoder.decode_raw(val) + Protobuf.Decoder.decode_type_m(extype, :value, val) + end end diff --git a/test/protobuf/decoder_test.exs b/test/protobuf/decoder_test.exs index 19662da1..b0c44efa 100644 --- a/test/protobuf/decoder_test.exs +++ b/test/protobuf/decoder_test.exs @@ -140,4 +140,27 @@ defmodule Protobuf.DecoderTest do assert Decoder.decode(<<18, 0, 24, 0>>, TestMsg.Oneof) == TestMsg.Oneof.new(first: {:b, ""}, second: {:c, 0}) end + + test "decode with and without custom field options" do + bin = <<10, 4, 10, 2, 115, 49, 18, 4, 10, 2, 115, 50>> + + assert Decoder.decode(bin, TestMsg.Ext.DualUseCase) == + TestMsg.Ext.DualUseCase.new(a: "s1", b: Google.Protobuf.StringValue.new(value: "s2")) + + assert Decoder.decode(bin, TestMsg.Ext.DualNonUse) == + TestMsg.Ext.DualNonUse.new( + a: Google.Protobuf.StringValue.new(value: "s1"), + b: Google.Protobuf.StringValue.new(value: "s2") + ) + end + + test "decode with and without custom field options, empty" do + bin = <<18, 4, 10, 2, 115, 50>> + + assert Decoder.decode(bin, TestMsg.Ext.DualUseCase) == + TestMsg.Ext.DualUseCase.new(a: nil, b: Google.Protobuf.StringValue.new(value: "s2")) + + assert Decoder.decode(bin, TestMsg.Ext.DualNonUse) == + TestMsg.Ext.DualNonUse.new(a: nil, b: Google.Protobuf.StringValue.new(value: "s2")) + end end diff --git a/test/protobuf/encoder_test.exs b/test/protobuf/encoder_test.exs index 1ef99b64..bd86d7c3 100644 --- a/test/protobuf/encoder_test.exs +++ b/test/protobuf/encoder_test.exs @@ -186,4 +186,9 @@ defmodule Protobuf.EncoderTest do msg = TestMsg.Ext.DualUseCase.new(a: "s1", b: Google.Protobuf.StringValue.new(value: "s2")) assert Encoder.encode(msg) == <<10, 4, 10, 2, 115, 49, 18, 4, 10, 2, 115, 50>> end + + test "encoding with custom field options, empty" do + msg = TestMsg.Ext.DualUseCase.new(b: Google.Protobuf.StringValue.new(value: "s2")) + assert Encoder.encode(msg) == <<18, 4, 10, 2, 115, 50>> + end end diff --git a/test/protobuf/protoc/integration_test.exs b/test/protobuf/protoc/integration_test.exs index f44df8ad..077e7ff3 100644 --- a/test/protobuf/protoc/integration_test.exs +++ b/test/protobuf/protoc/integration_test.exs @@ -60,5 +60,9 @@ defmodule Protobuf.Protoc.IntegrationTest do assert dual.b.value == "s2" assert %{options: [extype: "String.t"]} = Protobuf.Protoc.ExtTest.Dual.__message_props__().field_props[1] + + output = Protobuf.Protoc.ExtTest.Dual.encode(dual) + + assert Protobuf.Protoc.ExtTest.Dual.decode(output) == dual end end From 078680b3c4e6ef04df0c38f593a08d472bced252 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Wed, 22 Apr 2020 10:53:12 -0700 Subject: [PATCH 11/15] wrappers added --- lib/protobuf/extype/wrappers.ex | 72 ++++++++++++++++ lib/protobuf/field_options_processor.ex | 86 ++++++++++++------- test/protobuf/builder_test.exs | 13 ++- test/protobuf/encoder_validation_test.exs | 3 +- .../protobuf/field_options_processor_test.exs | 8 +- 5 files changed, 140 insertions(+), 42 deletions(-) create mode 100644 lib/protobuf/extype/wrappers.ex diff --git a/lib/protobuf/extype/wrappers.ex b/lib/protobuf/extype/wrappers.ex new file mode 100644 index 00000000..4568c674 --- /dev/null +++ b/lib/protobuf/extype/wrappers.ex @@ -0,0 +1,72 @@ +defmodule Protobuf.Extype.Wrappers do + @moduledoc """ + Implement value unwrapping for Google Wrappers. + """ + + require Protobuf.Decoder + require Logger + import Protobuf.Decoder, only: [decode_zigzag: 1] + + def validate_extype!(Google.Protobuf.DoubleValue, "float"), do: :double + def validate_extype!(Google.Protobuf.FloatValue, "float"), do: :float + def validate_extype!(Google.Protobuf.Int64Value, "integer"), do: :int64 + def validate_extype!(Google.Protobuf.UInt64Value, "non_neg_integer"), do: :uint64 + def validate_extype!(Google.Protobuf.Int32Value, "integer"), do: :int32 + def validate_extype!(Google.Protobuf.UInt32Value, "non_neg_integer"), do: :uint32 + def validate_extype!(Google.Protobuf.BoolValue, "boolean"), do: :bool + def validate_extype!(Google.Protobuf.StringValue, "String.t"), do: :string + def validate_extype!(Google.Protobuf.StringValue, "String.t()"), do: :string + def validate_extype!(Google.Protobuf.BytesValue, "String.t"), do: :bytes + def validate_extype!(Google.Protobuf.BytesValue, "String.t()"), do: :bytes + def validate_extype!(type, extype) do + raise "Invalid extype pairing, #{extype} not compatible with #{type}" + end + + def validate_extype_string!("Google.Protobuf.DoubleValue", "float"), do: "float" + def validate_extype_string!("Google.Protobuf.FloatValue", "float"), do: "float" + def validate_extype_string!("Google.Protobuf.Int64Value", "integer"), do: "integer" + def validate_extype_string!("Google.Protobuf.UInt64Value", "non_neg_integer"), do: "non_neg_integer" + def validate_extype_string!("Google.Protobuf.Int32Value", "integer"), do: "integer" + def validate_extype_string!("Google.Protobuf.UInt32Value", "non_neg_integer"), do: "non_neg_integer" + def validate_extype_string!("Google.Protobuf.BoolValue", "boolean"), do: "boolean" + def validate_extype_string!("Google.Protobuf.StringValue", "String.t"), do: "String.t" + def validate_extype_string!("Google.Protobuf.StringValue", "String.t()"), do: "String.t()" + def validate_extype_string!("Google.Protobuf.BytesValue", "String.t"), do: "String.t" + def validate_extype_string!("Google.Protobuf.BytesValue", "String.t()"), do: "String.t()" + def validate_extype_string!(type, extype) do + raise "Invalid extype pairing, #{extype} not compatible with #{type}" + end + + def do_type_default(type, extype) do + validate_extype!(type, extype) + nil + end + + def do_type_to_spec(type, extype) do + string_type = validate_extype_string!(type, extype) + string_type <> " | nil" + end + + def do_new(type, value, extype) do + validate_extype!(type, extype) + # No type check, just shape check. + if is_map(value) or is_list(value) do + raise "When extype option is present, new expects unwrapped value, not struct." + else + value + end + end + + def do_encode_type(type, v, extype) do + atom_type = validate_extype!(type, extype) + fnum = type.__message_props__.field_props[1].encoded_fnum + encoded = Protobuf.Encoder.encode_type(atom_type, v) + IO.iodata_to_binary([[fnum, encoded]]) + end + + def do_decode_type(type, val, extype) do + atom_type = validate_extype!(type, extype) + [_tag, _wire, val | _rest] = Protobuf.Decoder.decode_raw(val) + Protobuf.Decoder.decode_type_m(atom_type, :value, val) + end +end diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex index 22db2323..f2138263 100644 --- a/lib/protobuf/field_options_processor.ex +++ b/lib/protobuf/field_options_processor.ex @@ -17,30 +17,67 @@ defmodule Protobuf.FieldOptionsProcessor do A value with type extype. """ @type value :: struct | any - - @callback type_to_spec(type_enum :: atom, type :: String.t(), repeated :: boolean, options) :: String.t() @callback type_default(type, options) :: any @callback new(type, value, options) :: value @callback encode_type(type, value, options) :: binary @callback decode_type(val :: binary, type, options) :: value - def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t()" = extype]), do: extype - def validate_options_str!(:TYPE_MESSAGE, "Google.Protobuf.StringValue", [extype: "String.t" = extype]), do: extype + @wrappers [ + Google.Protobuf.DoubleValue, + Google.Protobuf.FloatValue, + Google.Protobuf.Int64Value, + Google.Protobuf.UInt64Value, + Google.Protobuf.Int32Value, + Google.Protobuf.UInt32Value, + Google.Protobuf.BoolValue, + Google.Protobuf.StringValue, + Google.Protobuf.BytesValue + ] + + @wrappers_str [ + "Google.Protobuf.DoubleValue", + "Google.Protobuf.FloatValue", + "Google.Protobuf.Int64Value", + "Google.Protobuf.UInt64Value", + "Google.Protobuf.Int32Value", + "Google.Protobuf.UInt32Value", + "Google.Protobuf.BoolValue", + "Google.Protobuf.StringValue", + "Google.Protobuf.BytesValue" + ] + + def get_extype_mod(type) do + cond do + type in @wrappers -> Protobuf.Extype.Wrappers + true -> raise "Sorry #{type} does not support the field option extype" + end + end + + def get_extype_mod_string(:TYPE_MESSAGE, type) do + cond do + type in @wrappers_str -> Protobuf.Extype.Wrappers + true -> raise "Sorry #{type} does not support the field option extype" + end + end + + def validate_options_str!(type_enum, type, extype: extype) do + {get_extype_mod_string(type_enum, type), extype} + end def validate_options_str!(_, type, options) do raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" end - def validate_options!(Google.Protobuf.StringValue, [extype: "String.t()"]), do: :string - def validate_options!(Google.Protobuf.StringValue, [extype: "String.t"]), do: :string + def validate_options!(type, extype: extype), do: {get_extype_mod(type), extype} def validate_options!(type, options) do raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" end def type_to_spec(type_enum, type, repeated, options) do - extype = validate_options_str!(type_enum, type, options) - type_str = extype <> " | nil" + {module, option_value} = validate_options_str!(type_enum, type, options) + type_str = module.do_type_to_spec(type, option_value) + if repeated do "[#{type_str}]" else @@ -49,39 +86,22 @@ defmodule Protobuf.FieldOptionsProcessor do end def type_default(type, options) do - validate_options!(type, options) - nil + {module, option_value} = validate_options!(type, options) + module.do_type_default(type, option_value) end - # Note: Could do type check here if we wanted to. def new(type, value, options) do - validate_options!(type, options) - value + {module, option_value} = validate_options!(type, options) + module.do_new(type, value, option_value) end def encode_type(type, v, options) do - extype = validate_options!(type, options) - encoded = do_encode_type(type, v, extype) - IO.iodata_to_binary(encoded) - end - - defp do_encode_type(type, v, extype) do - fnum = type.__message_props__.field_props[1].encoded_fnum - encoded = Protobuf.Encoder.encode_type(extype, v) - [[fnum, encoded]] + {module, option_value} = validate_options!(type, options) + module.do_encode_type(type, v, option_value) end def decode_type(val, type, options) do - extype = validate_options!(type, options) - do_decode_type(type, val, extype) - end - - defp do_decode_type(_type, val, extype) do - require Logger - require Protobuf.Decoder - import Protobuf.Decoder, only: [decode_zigzag: 1] - - [_tag, _wire, val | _rest] = Protobuf.Decoder.decode_raw(val) - Protobuf.Decoder.decode_type_m(extype, :value, val) + {module, option_value} = validate_options!(type, options) + module.do_decode_type(type, val, option_value) end end diff --git a/test/protobuf/builder_test.exs b/test/protobuf/builder_test.exs index 1030a266..2c704fdd 100644 --- a/test/protobuf/builder_test.exs +++ b/test/protobuf/builder_test.exs @@ -82,9 +82,14 @@ defmodule Protobuf.BuilderTest do fn -> DualUseCase.new(a: "s1", b: "s2") end end - test "new/2 build for custom_field_options, doesn't type check value" do - # Should be just string - assert %DualUseCase{a: %Google.Protobuf.StringValue{value: "s1"}} = - DualUseCase.new!(a: %Google.Protobuf.StringValue{value: "s1"}) + test "new/2 build for custom_field_options shape checks" do + assert_raise RuntimeError, + "When extype option is present, new expects unwrapped value, not struct.", + fn -> DualUseCase.new!(a: %Google.Protobuf.StringValue{value: "s1"}) end + end + + test "new/2 build for custom_field_options doesn't type check" do + # Should be string + assert %DualUseCase{a: 1} = DualUseCase.new!(a: 1) end end diff --git a/test/protobuf/encoder_validation_test.exs b/test/protobuf/encoder_validation_test.exs index d17f6934..846f476b 100644 --- a/test/protobuf/encoder_validation_test.exs +++ b/test/protobuf/encoder_validation_test.exs @@ -164,7 +164,8 @@ defmodule Protobuf.EncoderTest.Validation do end test "field with custom options, bad values" do - msg = TestMsg.Ext.DualUseCase.new(a: Google.Protobuf.StringValue.new(value: "s1"), b: Google.Protobuf.StringValue.new(value: "s2")) + # should be string + msg = TestMsg.Ext.DualUseCase.new(a: 11) assert_raise Protobuf.EncodeError, fn -> Protobuf.Encoder.encode(msg) end end diff --git a/test/protobuf/field_options_processor_test.exs b/test/protobuf/field_options_processor_test.exs index 6454c626..2ba566a7 100644 --- a/test/protobuf/field_options_processor_test.exs +++ b/test/protobuf/field_options_processor_test.exs @@ -23,8 +23,8 @@ defmodule Protobuf.FieldOptionsProcessorTest do test "type_to_spec invalid extype" do extype = "integer" - assert_raise RuntimeError, "The custom field option is invalid. " <> - "Options: [extype: \"integer\"] incompatible with type: Google.Protobuf.StringValue", + assert_raise RuntimeError, "Invalid extype pairing, " <> + "integer not compatible with Google.Protobuf.StringValue", fn -> FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", false, [extype: extype]) end @@ -32,8 +32,8 @@ defmodule Protobuf.FieldOptionsProcessorTest do test "type_to_spec invalid type" do extype = "String.t()" - assert_raise RuntimeError, "The custom field option is invalid. " <> - "Options: [extype: \"String.t()\"] incompatible with type: Google.Protobuf.UnrealValue", + assert_raise RuntimeError, + "Sorry Google.Protobuf.UnrealValue does not support the field option extype", fn -> FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.UnrealValue", false, [extype: extype]) end From c78710c2d3dd814258b11929442013688525a7aa Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Wed, 22 Apr 2020 11:45:55 -0700 Subject: [PATCH 12/15] add timestamps --- lib/protobuf/extype/timestamp.ex | 67 +++++++++++++++++++ lib/protobuf/field_options_processor.ex | 2 + .../protobuf/field_options_processor_test.exs | 38 +++++++++++ 3 files changed, 107 insertions(+) create mode 100644 lib/protobuf/extype/timestamp.ex diff --git a/lib/protobuf/extype/timestamp.ex b/lib/protobuf/extype/timestamp.ex new file mode 100644 index 00000000..89af333b --- /dev/null +++ b/lib/protobuf/extype/timestamp.ex @@ -0,0 +1,67 @@ +defmodule Protobuf.Extype.Timestamp do + @moduledoc """ + Implement DateTime and NaiveDateTime casting for Google Timestamp. + """ + + def validate_extype!(Google.Protobuf.Timestamp, "NaiveDateTime.t"), do: :naivedatetime + def validate_extype!(Google.Protobuf.Timestamp, "NaiveDateTime.t()"), do: :naivedatetime + def validate_extype!(Google.Protobuf.Timestamp, "DateTime.t"), do: :datetime + def validate_extype!(Google.Protobuf.Timestamp, "DateTime.t()"), do: :datetime + def validate_extype!(type, extype) do + raise "Invalid extype pairing, #{extype} not compatible with #{type}. " <> + "Supported types are DateTime.t() or NaiveDateTime.t()" + end + + def validate_extype_string!("Google.Protobuf.Timestamp", "NaiveDateTime.t"), do: "NaiveDateTime.t" + def validate_extype_string!("Google.Protobuf.Timestamp", "NaiveDateTime.t()"), do: "NaiveDateTime.t()" + def validate_extype_string!("Google.Protobuf.Timestamp", "DateTime.t"), do: "DateTime.t" + def validate_extype_string!("Google.Protobuf.Timestamp", "DateTime.t()"), do: "DateTime.t()" + def validate_extype_string!(type, extype) do + raise "Invalid extype pairing, #{extype} not compatible with #{type}. " <> + "Supported types are DateTime.t() or NaiveDateTime.t()" + end + + def do_type_default(type, extype) do + validate_extype!(type, extype) + nil + end + + def do_type_to_spec(type, extype) do + string_type = validate_extype_string!(type, extype) + string_type <> " | nil" + end + + def do_new(type, value, extype) do + validate_extype!(type, extype) + value + end + + def do_encode_type(type, v, extype) do + atom_type = validate_extype!(type, extype) + + v = if atom_type == :naivedatetime, do: DateTime.from_naive!(v, "Etc/UTC"), else: v + + unix = DateTime.to_unix(v, :nanosecond) + + seconds = System.convert_time_unit(unix, :nanosecond, :second) + nanos = unix - System.convert_time_unit(seconds, :second, :nanosecond) + + value = Google.Protobuf.Timestamp.new(seconds: seconds, nanos: nanos) + + Protobuf.encode(value) + end + + def do_decode_type(type, val, extype) do + atom_type = validate_extype!(type, extype) + + protobuf_timestamp = Protobuf.decode(val, type) + + value = + protobuf_timestamp.seconds + |> System.convert_time_unit(:second, :nanosecond) + |> Kernel.+(protobuf_timestamp.nanos) + |> DateTime.from_unix!(:nanosecond) + + if atom_type == :naivedatetime, do: DateTime.to_naive(value), else: value + end +end diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex index f2138263..396f003d 100644 --- a/lib/protobuf/field_options_processor.ex +++ b/lib/protobuf/field_options_processor.ex @@ -50,6 +50,7 @@ defmodule Protobuf.FieldOptionsProcessor do def get_extype_mod(type) do cond do type in @wrappers -> Protobuf.Extype.Wrappers + type == Google.Protobuf.Timestamp -> Protobuf.Extype.Timestamp true -> raise "Sorry #{type} does not support the field option extype" end end @@ -57,6 +58,7 @@ defmodule Protobuf.FieldOptionsProcessor do def get_extype_mod_string(:TYPE_MESSAGE, type) do cond do type in @wrappers_str -> Protobuf.Extype.Wrappers + type == "Google.Protobuf.Timestamp" -> Protobuf.Extype.Timestamp true -> raise "Sorry #{type} does not support the field option extype" end end diff --git a/test/protobuf/field_options_processor_test.exs b/test/protobuf/field_options_processor_test.exs index 2ba566a7..e7b99371 100644 --- a/test/protobuf/field_options_processor_test.exs +++ b/test/protobuf/field_options_processor_test.exs @@ -1,4 +1,6 @@ defmodule Protobuf.FieldOptionsProcessorTest do + @moduledoc false + use ExUnit.Case, async: true alias Protobuf.FieldOptionsProcessor @@ -38,4 +40,40 @@ defmodule Protobuf.FieldOptionsProcessorTest do FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.UnrealValue", false, [extype: extype]) end end + + test "type_default" do + + assert is_nil(FieldOptionsProcessor.type_default(Google.Protobuf.BoolValue, extype: "boolean")) + + assert is_nil(FieldOptionsProcessor.type_default(Google.Protobuf.Timestamp, extype: "DateTime.t()")) + assert is_nil(FieldOptionsProcessor.type_default(Google.Protobuf.Timestamp, extype: "DateTime.t")) + assert is_nil(FieldOptionsProcessor.type_default(Google.Protobuf.Timestamp, extype: "NaiveDateTime.t()")) + + # Typo + assert_raise RuntimeError, "Invalid extype pairing, Datetime.t not compatible with " <> + "Elixir.Google.Protobuf.Timestamp. Supported types are DateTime.t() or NaiveDateTime.t()", + fn -> FieldOptionsProcessor.type_default(Google.Protobuf.Timestamp, extype: "Datetime.t") end + end + + test "encoding and decoding timestamp" do + dt = DateTime.utc_now() + ndt = DateTime.to_naive(dt) + + output = FieldOptionsProcessor.encode_type(Google.Protobuf.Timestamp, dt, extype: "DateTime.t") + output1 = FieldOptionsProcessor.encode_type(Google.Protobuf.Timestamp, ndt, extype: "NaiveDateTime.t") + + assert output == output1 + + assert FieldOptionsProcessor.decode_type(output, Google.Protobuf.Timestamp, extype: "DateTime.t") == dt + + assert FieldOptionsProcessor.decode_type(output, Google.Protobuf.Timestamp, extype: "NaiveDateTime.t") == ndt + + # DateTime.from_naive accepts DateTime.t as well + assert FieldOptionsProcessor.encode_type(Google.Protobuf.Timestamp, dt, extype: "NaiveDateTime.t") == output + + # Cannot encode NaiveDateTime as DateTime, missing timezone info. + assert_raise FunctionClauseError, fn -> + FieldOptionsProcessor.encode_type(Google.Protobuf.Timestamp, ndt, extype: "DateTime.t") + end + end end From 5f8eaf908a5145076a1cdf24fbf3e35a3f364d16 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Thu, 23 Apr 2020 13:37:33 -0700 Subject: [PATCH 13/15] remove ? from cli arg, add timestamps tests & complex tests --- Makefile | 3 +- lib/protobuf/extype/timestamp.ex | 9 ++- lib/protobuf/extype/wrappers.ex | 11 ++- lib/protobuf/field_options_processor.ex | 8 +-- lib/protobuf/protoc/cli.ex | 2 +- .../protobuf/field_options_processor_test.exs | 48 ++++++++++++- test/protobuf/protoc/cli_test.exs | 2 +- test/protobuf/protoc/integration_test.exs | 23 ++++++ test/protobuf/protoc/proto/extension2.proto | 35 +++++++++ .../protoc/proto_gen/extension2.pb.ex | 72 +++++++++++++++++++ 10 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 test/protobuf/protoc/proto/extension2.proto create mode 100644 test/protobuf/protoc/proto_gen/extension2.pb.ex diff --git a/Makefile b/Makefile index deb0b548..3df1587d 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,8 @@ gen-google-protos: protoc-gen-elixir gen-protos: protoc-gen-elixir protoc -I src -I test/protobuf/protoc/proto --elixir_out=test/protobuf/protoc/proto_gen --plugin=./protoc-gen-elixir test/protobuf/protoc/proto/*.proto - protoc -I src -I test/protobuf/protoc/proto --elixir_out=custom_field_options?=true:test/protobuf/protoc/proto_gen --plugin=./protoc-gen-elixir test/protobuf/protoc/proto/extension.proto + protoc -I src -I test/protobuf/protoc/proto --elixir_out=custom_field_options=true:test/protobuf/protoc/proto_gen --plugin=./protoc-gen-elixir test/protobuf/protoc/proto/extension.proto + protoc -I src -I test/protobuf/protoc/proto --elixir_out=custom_field_options=true:test/protobuf/protoc/proto_gen --plugin=./protoc-gen-elixir test/protobuf/protoc/proto/extension2.proto protoc -I src --elixir_out=lib --plugin=./protoc-gen-elixir elixirpb.proto .PHONY: clean gen_google_proto gen_test_protos diff --git a/lib/protobuf/extype/timestamp.ex b/lib/protobuf/extype/timestamp.ex index 89af333b..b2bbfd8f 100644 --- a/lib/protobuf/extype/timestamp.ex +++ b/lib/protobuf/extype/timestamp.ex @@ -26,9 +26,14 @@ defmodule Protobuf.Extype.Timestamp do nil end - def do_type_to_spec(type, extype) do + def do_type_to_spec(type, repeated, extype) do string_type = validate_extype_string!(type, extype) - string_type <> " | nil" + + if repeated do + "[#{string_type}]" + else + string_type <> " | nil" + end end def do_new(type, value, extype) do diff --git a/lib/protobuf/extype/wrappers.ex b/lib/protobuf/extype/wrappers.ex index 4568c674..9ea7a36a 100644 --- a/lib/protobuf/extype/wrappers.ex +++ b/lib/protobuf/extype/wrappers.ex @@ -42,15 +42,20 @@ defmodule Protobuf.Extype.Wrappers do nil end - def do_type_to_spec(type, extype) do + def do_type_to_spec(type, repeated, extype) do string_type = validate_extype_string!(type, extype) - string_type <> " | nil" + + if repeated do + "[#{string_type}]" + else + string_type <> " | nil" + end end def do_new(type, value, extype) do validate_extype!(type, extype) # No type check, just shape check. - if is_map(value) or is_list(value) do + if is_map(value) do raise "When extype option is present, new expects unwrapped value, not struct." else value diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex index 396f003d..7627d949 100644 --- a/lib/protobuf/field_options_processor.ex +++ b/lib/protobuf/field_options_processor.ex @@ -78,13 +78,7 @@ defmodule Protobuf.FieldOptionsProcessor do def type_to_spec(type_enum, type, repeated, options) do {module, option_value} = validate_options_str!(type_enum, type, options) - type_str = module.do_type_to_spec(type, option_value) - - if repeated do - "[#{type_str}]" - else - type_str - end + module.do_type_to_spec(type, repeated, option_value) end def type_default(type, options) do diff --git a/lib/protobuf/protoc/cli.ex b/lib/protobuf/protoc/cli.ex index 1f014264..f6f5734c 100644 --- a/lib/protobuf/protoc/cli.ex +++ b/lib/protobuf/protoc/cli.ex @@ -66,7 +66,7 @@ defmodule Protobuf.Protoc.CLI do parse_params(ctx, t) end - def parse_params(ctx, ["custom_field_options?=true" | t]) do + def parse_params(ctx, ["custom_field_options=true" | t]) do ctx = %{ctx | custom_field_options?: true} parse_params(ctx, t) end diff --git a/test/protobuf/field_options_processor_test.exs b/test/protobuf/field_options_processor_test.exs index e7b99371..2100c9d1 100644 --- a/test/protobuf/field_options_processor_test.exs +++ b/test/protobuf/field_options_processor_test.exs @@ -20,7 +20,7 @@ defmodule Protobuf.FieldOptionsProcessorTest do test "type_to_spec repeated" do extype = "String.t()" assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", true, [extype: extype]) == - "[String.t() | nil]" + "[String.t()]" end test "type_to_spec invalid extype" do @@ -76,4 +76,50 @@ defmodule Protobuf.FieldOptionsProcessorTest do FieldOptionsProcessor.encode_type(Google.Protobuf.Timestamp, ndt, extype: "DateTime.t") end end + + test "encoding and decoding timestamp different timezone" do + dt = %DateTime{year: 2017, month: 11, day: 7, zone_abbr: "CET", + hour: 11, minute: 45, second: 18, microsecond: {123456, 6}, + utc_offset: 3600, std_offset: 0, time_zone: "Europe/Paris" + } + + result = + Google.Protobuf.Timestamp + |> FieldOptionsProcessor.encode_type(dt, extype: "DateTime.t") + |> FieldOptionsProcessor.decode_type(Google.Protobuf.Timestamp, extype: "DateTime.t") + + # They are not equal - timezone has been converted to utc + refute dt == result + + assert "Europe/Paris" = dt.time_zone() + assert "Etc/UTC" = result.time_zone() + + # But they are the equivalent time (at least in the past) + assert DateTime.diff(dt, result) == 0 + + dt1 = DateTime.from_unix!(1_464_096_368) + + result1 = + Google.Protobuf.Timestamp + |> FieldOptionsProcessor.encode_type(dt1, extype: "DateTime.t") + |> FieldOptionsProcessor.decode_type(Google.Protobuf.Timestamp, extype: "DateTime.t") + + # They are not equal result1 has more precision than dt1 + refute dt1 == result1 + + # Are equivalent + assert DateTime.diff(dt1, result1) == 0 + + # Set precision to microseconds + dt2 = DateTime.from_unix!(1_464_096_368, :microsecond) + + result2 = + Google.Protobuf.Timestamp + |> FieldOptionsProcessor.encode_type(dt2, extype: "DateTime.t") + |> FieldOptionsProcessor.decode_type(Google.Protobuf.Timestamp, extype: "DateTime.t") + + # They are equal + assert dt2 == result2 + end + end diff --git a/test/protobuf/protoc/cli_test.exs b/test/protobuf/protoc/cli_test.exs index 290a3c00..8675f88b 100644 --- a/test/protobuf/protoc/cli_test.exs +++ b/test/protobuf/protoc/cli_test.exs @@ -16,7 +16,7 @@ defmodule Protobuf.Protoc.CLITest do test "parse_params/2 parse custom_field_options" do ctx = %Context{} - ctx = parse_params(ctx, "plugins=grpc,custom_field_options?=true") + ctx = parse_params(ctx, "plugins=grpc,custom_field_options=true") assert ctx == %Context{plugins: ["grpc"], custom_field_options?: true} end diff --git a/test/protobuf/protoc/integration_test.exs b/test/protobuf/protoc/integration_test.exs index 077e7ff3..86d58a7b 100644 --- a/test/protobuf/protoc/integration_test.exs +++ b/test/protobuf/protoc/integration_test.exs @@ -65,4 +65,27 @@ defmodule Protobuf.Protoc.IntegrationTest do assert Protobuf.Protoc.ExtTest.Dual.decode(output) == dual end + + test "extension use case 2" do + dt = DateTime.from_unix!(1_464_096_368, :microsecond) + + msg = Ext.MyMessage.new( + f1: 1.0, + f2: 2.0, + f3: 3, + f4: 4, + f5: 5, + f6: 6, + f7: true, + f8: "8", + f9: "9", + nested: Ext.Nested.new(my_timestamp: {:dt, dt}), + no_extype: %Google.Protobuf.StringValue{value: "none"}, + normal1: 1234, + normal2: "hello", + repeated_field: ["r1", "r2"] + ) + + assert msg |> Ext.MyMessage.encode() |> Ext.MyMessage.decode() == msg + end end diff --git a/test/protobuf/protoc/proto/extension2.proto b/test/protobuf/protoc/proto/extension2.proto new file mode 100644 index 00000000..a4417017 --- /dev/null +++ b/test/protobuf/protoc/proto/extension2.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package ext; + +import "brex_elixirpb.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/timestamp.proto"; + +// To run +// protoc -I src -I test/protobuf/protoc/proto --elixir_out=custom_field_options=true:test/protobuf/protoc/proto_gen --plugin=./protoc-gen-elixir test/protobuf/protoc/proto/extension2.proto + +message Nested{ + oneof my_timestamp { + google.protobuf.Timestamp dt = 1 [(brex.elixirpb.field).extype="DateTime.t"]; + google.protobuf.Timestamp ndt = 2 [(brex.elixirpb.field).extype="NaiveDateTime.t"]; + } +} + +message MyMessage { + google.protobuf.DoubleValue f1 = 1 [(brex.elixirpb.field).extype="float"]; + google.protobuf.FloatValue f2 = 2 [(brex.elixirpb.field).extype="float"]; + google.protobuf.Int64Value f3 = 3 [(brex.elixirpb.field).extype="integer"]; + google.protobuf.UInt64Value f4 = 4 [(brex.elixirpb.field).extype="non_neg_integer"]; + google.protobuf.Int32Value f5 = 5 [(brex.elixirpb.field).extype="integer"]; + google.protobuf.UInt32Value f6 = 6 [(brex.elixirpb.field).extype="non_neg_integer"]; + google.protobuf.BoolValue f7 = 7 [(brex.elixirpb.field).extype="boolean"]; + google.protobuf.StringValue f8 = 8 [(brex.elixirpb.field).extype="String.t"]; + google.protobuf.BytesValue f9 = 9 [(brex.elixirpb.field).extype="String.t()"]; + + google.protobuf.StringValue no_extype = 10; + repeated google.protobuf.StringValue repeated_field = 11 [(brex.elixirpb.field).extype="String.t"]; + uint64 normal1 = 12; + string normal2 = 13; + Nested nested = 14; +} \ No newline at end of file diff --git a/test/protobuf/protoc/proto_gen/extension2.pb.ex b/test/protobuf/protoc/proto_gen/extension2.pb.ex new file mode 100644 index 00000000..15bb50e8 --- /dev/null +++ b/test/protobuf/protoc/proto_gen/extension2.pb.ex @@ -0,0 +1,72 @@ +defmodule Ext.Nested do + @moduledoc false + use Protobuf, custom_field_options?: true, syntax: :proto3 + + @type t :: %__MODULE__{ + my_timestamp: {atom, any} + } + defstruct [:my_timestamp] + + oneof :my_timestamp, 0 + + field :dt, 1, type: Google.Protobuf.Timestamp, oneof: 0, options: [extype: "DateTime.t"] + field :ndt, 2, type: Google.Protobuf.Timestamp, oneof: 0, options: [extype: "NaiveDateTime.t"] +end + +defmodule Ext.MyMessage do + @moduledoc false + use Protobuf, custom_field_options?: true, syntax: :proto3 + + @type t :: %__MODULE__{ + f1: float | nil, + f2: float | nil, + f3: integer | nil, + f4: non_neg_integer | nil, + f5: integer | nil, + f6: non_neg_integer | nil, + f7: boolean | nil, + f8: String.t() | nil, + f9: String.t() | nil, + no_extype: Google.Protobuf.StringValue.t() | nil, + repeated_field: [String.t()], + normal1: non_neg_integer, + normal2: String.t(), + nested: Ext.Nested.t() | nil + } + defstruct [ + :f1, + :f2, + :f3, + :f4, + :f5, + :f6, + :f7, + :f8, + :f9, + :no_extype, + :repeated_field, + :normal1, + :normal2, + :nested + ] + + field :f1, 1, type: Google.Protobuf.DoubleValue, options: [extype: "float"] + field :f2, 2, type: Google.Protobuf.FloatValue, options: [extype: "float"] + field :f3, 3, type: Google.Protobuf.Int64Value, options: [extype: "integer"] + field :f4, 4, type: Google.Protobuf.UInt64Value, options: [extype: "non_neg_integer"] + field :f5, 5, type: Google.Protobuf.Int32Value, options: [extype: "integer"] + field :f6, 6, type: Google.Protobuf.UInt32Value, options: [extype: "non_neg_integer"] + field :f7, 7, type: Google.Protobuf.BoolValue, options: [extype: "boolean"] + field :f8, 8, type: Google.Protobuf.StringValue, options: [extype: "String.t"] + field :f9, 9, type: Google.Protobuf.BytesValue, options: [extype: "String.t()"] + field :no_extype, 10, type: Google.Protobuf.StringValue + + field :repeated_field, 11, + repeated: true, + type: Google.Protobuf.StringValue, + options: [extype: "String.t"] + + field :normal1, 12, type: :uint64 + field :normal2, 13, type: :string + field :nested, 14, type: Ext.Nested +end From d81e7cac741fc2e34867940080e07ad05eccdc8d Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Mon, 27 Apr 2020 08:32:04 -0700 Subject: [PATCH 14/15] an attempt to tip the scales --- lib/protobuf/extype/extype_protocol.ex | 85 +++++++++++++++++++ lib/protobuf/extype/timestamp.ex | 51 +++-------- lib/protobuf/extype/wrappers.ex | 80 +++++++---------- lib/protobuf/field_options_processor.ex | 78 +++-------------- .../protobuf/field_options_processor_test.exs | 23 +---- 5 files changed, 140 insertions(+), 177 deletions(-) create mode 100644 lib/protobuf/extype/extype_protocol.ex diff --git a/lib/protobuf/extype/extype_protocol.ex b/lib/protobuf/extype/extype_protocol.ex new file mode 100644 index 00000000..1d12b8d1 --- /dev/null +++ b/lib/protobuf/extype/extype_protocol.ex @@ -0,0 +1,85 @@ +defprotocol Extype.Protocol do + # Imagine registering an extension by defining a protocol + + @type extype :: atom + + @typedoc """ + The existing type of the field. Often the module name of the struct. + """ + @type type :: atom + + @typedoc """ + A value with type extype. + """ + @type value :: struct | any + + @spec validate_and_to_atom_extype!(type, option :: String.t) :: atom + def validate_and_to_atom_extype!(type, option) + + @spec do_type_default(type, extype) :: any + def do_type_default(type, extype) + + @spec do_new(type, value, extype) :: value + def do_new(type, value, extype) + + @spec do_encode_type(type, value, extype) :: binary + def do_encode_type(type, v, extype) + + @spec do_decode_type(type, val :: binary, extype) :: value + def do_decode_type(val, type, extype) +end + +defmodule Extype do + @moduledoc "Extype" + + @type extype :: Extype.Protocol.extype + @type type :: Extype.Protocol.type + @type value :: Extype.Protocol.value + + # A serious trick + def get_mod(type) when is_atom(type) do + try do + Extype.Protocol.impl_for!(%{__struct__: type}) + rescue + _exception -> + reraise "Sorry #{type} does not support the field option extype", __STACKTRACE__ + end + end + + @spec type_to_spec(type :: String.t(), repeated :: boolean, extype) :: String.t() + def type_to_spec(_type, repeated, extype) do + if repeated do + "[#{extype}]" + else + extype <> " | nil" + end + end + + @spec type_default(type, extype) :: any + def type_default(type, extype) do + mod = get_mod(type) + atom_extype = mod.validate_and_to_atom_extype!(type, extype) + mod.do_type_default(type, atom_extype) + end + + @spec new(type, value, extype) :: value + def new(type, value, extype) do + mod = get_mod(type) + atom_extype = mod.validate_and_to_atom_extype!(type, extype) + mod.do_new(type, value, atom_extype) + end + + @spec encode_type(type, value, extype) :: binary + def encode_type(type, v, extype) do + mod = get_mod(type) + atom_extype = mod.validate_and_to_atom_extype!(type, extype) + mod.do_encode_type(type, v, atom_extype) + end + + @spec decode_type(val :: binary, type, extype) :: value + def decode_type(val, type, extype) do + mod = get_mod(type) + atom_extype = mod.validate_and_to_atom_extype!(type, extype) + mod.do_decode_type(type, val, atom_extype) + end +end diff --git a/lib/protobuf/extype/timestamp.ex b/lib/protobuf/extype/timestamp.ex index b2bbfd8f..3a38db19 100644 --- a/lib/protobuf/extype/timestamp.ex +++ b/lib/protobuf/extype/timestamp.ex @@ -1,50 +1,23 @@ -defmodule Protobuf.Extype.Timestamp do +defimpl Extype.Protocol, for: [Google.Protobuf.Timestamp] do @moduledoc """ Implement DateTime and NaiveDateTime casting for Google Timestamp. """ - def validate_extype!(Google.Protobuf.Timestamp, "NaiveDateTime.t"), do: :naivedatetime - def validate_extype!(Google.Protobuf.Timestamp, "NaiveDateTime.t()"), do: :naivedatetime - def validate_extype!(Google.Protobuf.Timestamp, "DateTime.t"), do: :datetime - def validate_extype!(Google.Protobuf.Timestamp, "DateTime.t()"), do: :datetime - def validate_extype!(type, extype) do + def validate_and_to_atom_extype!(Google.Protobuf.Timestamp, "NaiveDateTime.t"), do: :naivedatetime + def validate_and_to_atom_extype!(Google.Protobuf.Timestamp, "NaiveDateTime.t()"), do: :naivedatetime + def validate_and_to_atom_extype!(Google.Protobuf.Timestamp, "DateTime.t"), do: :datetime + def validate_and_to_atom_extype!(Google.Protobuf.Timestamp, "DateTime.t()"), do: :datetime + def validate_and_to_atom_extype!(type, extype) do raise "Invalid extype pairing, #{extype} not compatible with #{type}. " <> "Supported types are DateTime.t() or NaiveDateTime.t()" end - def validate_extype_string!("Google.Protobuf.Timestamp", "NaiveDateTime.t"), do: "NaiveDateTime.t" - def validate_extype_string!("Google.Protobuf.Timestamp", "NaiveDateTime.t()"), do: "NaiveDateTime.t()" - def validate_extype_string!("Google.Protobuf.Timestamp", "DateTime.t"), do: "DateTime.t" - def validate_extype_string!("Google.Protobuf.Timestamp", "DateTime.t()"), do: "DateTime.t()" - def validate_extype_string!(type, extype) do - raise "Invalid extype pairing, #{extype} not compatible with #{type}. " <> - "Supported types are DateTime.t() or NaiveDateTime.t()" - end + def do_type_default(_type, _extype), do: nil - def do_type_default(type, extype) do - validate_extype!(type, extype) - nil - end - - def do_type_to_spec(type, repeated, extype) do - string_type = validate_extype_string!(type, extype) + def do_new(_type, value, _extype), do: value - if repeated do - "[#{string_type}]" - else - string_type <> " | nil" - end - end - - def do_new(type, value, extype) do - validate_extype!(type, extype) - value - end - - def do_encode_type(type, v, extype) do - atom_type = validate_extype!(type, extype) - - v = if atom_type == :naivedatetime, do: DateTime.from_naive!(v, "Etc/UTC"), else: v + def do_encode_type(_type, v, extype) do + v = if extype == :naivedatetime, do: DateTime.from_naive!(v, "Etc/UTC"), else: v unix = DateTime.to_unix(v, :nanosecond) @@ -57,8 +30,6 @@ defmodule Protobuf.Extype.Timestamp do end def do_decode_type(type, val, extype) do - atom_type = validate_extype!(type, extype) - protobuf_timestamp = Protobuf.decode(val, type) value = @@ -67,6 +38,6 @@ defmodule Protobuf.Extype.Timestamp do |> Kernel.+(protobuf_timestamp.nanos) |> DateTime.from_unix!(:nanosecond) - if atom_type == :naivedatetime, do: DateTime.to_naive(value), else: value + if extype == :naivedatetime, do: DateTime.to_naive(value), else: value end end diff --git a/lib/protobuf/extype/wrappers.ex b/lib/protobuf/extype/wrappers.ex index 9ea7a36a..b2ffaba4 100644 --- a/lib/protobuf/extype/wrappers.ex +++ b/lib/protobuf/extype/wrappers.ex @@ -1,59 +1,41 @@ -defmodule Protobuf.Extype.Wrappers do +defimpl Extype.Protocol, for: [ + Google.Protobuf.DoubleValue, + Google.Protobuf.FloatValue, + Google.Protobuf.Int64Value, + Google.Protobuf.UInt64Value, + Google.Protobuf.Int32Value, + Google.Protobuf.UInt32Value, + Google.Protobuf.BoolValue, + Google.Protobuf.StringValue, + Google.Protobuf.BytesValue + ] do + @moduledoc """ - Implement value unwrapping for Google Wrappers. + Implement value unwrapping for Google Wrappers. """ require Protobuf.Decoder require Logger import Protobuf.Decoder, only: [decode_zigzag: 1] - def validate_extype!(Google.Protobuf.DoubleValue, "float"), do: :double - def validate_extype!(Google.Protobuf.FloatValue, "float"), do: :float - def validate_extype!(Google.Protobuf.Int64Value, "integer"), do: :int64 - def validate_extype!(Google.Protobuf.UInt64Value, "non_neg_integer"), do: :uint64 - def validate_extype!(Google.Protobuf.Int32Value, "integer"), do: :int32 - def validate_extype!(Google.Protobuf.UInt32Value, "non_neg_integer"), do: :uint32 - def validate_extype!(Google.Protobuf.BoolValue, "boolean"), do: :bool - def validate_extype!(Google.Protobuf.StringValue, "String.t"), do: :string - def validate_extype!(Google.Protobuf.StringValue, "String.t()"), do: :string - def validate_extype!(Google.Protobuf.BytesValue, "String.t"), do: :bytes - def validate_extype!(Google.Protobuf.BytesValue, "String.t()"), do: :bytes - def validate_extype!(type, extype) do + def validate_and_to_atom_extype!(Google.Protobuf.DoubleValue, "float"), do: :double + def validate_and_to_atom_extype!(Google.Protobuf.FloatValue, "float"), do: :float + def validate_and_to_atom_extype!(Google.Protobuf.Int64Value, "integer"), do: :int64 + def validate_and_to_atom_extype!(Google.Protobuf.UInt64Value, "non_neg_integer"), do: :uint64 + def validate_and_to_atom_extype!(Google.Protobuf.Int32Value, "integer"), do: :int32 + def validate_and_to_atom_extype!(Google.Protobuf.UInt32Value, "non_neg_integer"), do: :uint32 + def validate_and_to_atom_extype!(Google.Protobuf.BoolValue, "boolean"), do: :bool + def validate_and_to_atom_extype!(Google.Protobuf.StringValue, "String.t"), do: :string + def validate_and_to_atom_extype!(Google.Protobuf.StringValue, "String.t()"), do: :string + def validate_and_to_atom_extype!(Google.Protobuf.BytesValue, "String.t"), do: :bytes + def validate_and_to_atom_extype!(Google.Protobuf.BytesValue, "String.t()"), do: :bytes + def validate_and_to_atom_extype!(type, extype) do raise "Invalid extype pairing, #{extype} not compatible with #{type}" end - def validate_extype_string!("Google.Protobuf.DoubleValue", "float"), do: "float" - def validate_extype_string!("Google.Protobuf.FloatValue", "float"), do: "float" - def validate_extype_string!("Google.Protobuf.Int64Value", "integer"), do: "integer" - def validate_extype_string!("Google.Protobuf.UInt64Value", "non_neg_integer"), do: "non_neg_integer" - def validate_extype_string!("Google.Protobuf.Int32Value", "integer"), do: "integer" - def validate_extype_string!("Google.Protobuf.UInt32Value", "non_neg_integer"), do: "non_neg_integer" - def validate_extype_string!("Google.Protobuf.BoolValue", "boolean"), do: "boolean" - def validate_extype_string!("Google.Protobuf.StringValue", "String.t"), do: "String.t" - def validate_extype_string!("Google.Protobuf.StringValue", "String.t()"), do: "String.t()" - def validate_extype_string!("Google.Protobuf.BytesValue", "String.t"), do: "String.t" - def validate_extype_string!("Google.Protobuf.BytesValue", "String.t()"), do: "String.t()" - def validate_extype_string!(type, extype) do - raise "Invalid extype pairing, #{extype} not compatible with #{type}" - end - - def do_type_default(type, extype) do - validate_extype!(type, extype) - nil - end - - def do_type_to_spec(type, repeated, extype) do - string_type = validate_extype_string!(type, extype) - - if repeated do - "[#{string_type}]" - else - string_type <> " | nil" - end - end + def do_type_default(_type, _extype), do: nil - def do_new(type, value, extype) do - validate_extype!(type, extype) + def do_new(_type, value, _extype) do # No type check, just shape check. if is_map(value) do raise "When extype option is present, new expects unwrapped value, not struct." @@ -63,15 +45,13 @@ defmodule Protobuf.Extype.Wrappers do end def do_encode_type(type, v, extype) do - atom_type = validate_extype!(type, extype) fnum = type.__message_props__.field_props[1].encoded_fnum - encoded = Protobuf.Encoder.encode_type(atom_type, v) + encoded = Protobuf.Encoder.encode_type(extype, v) IO.iodata_to_binary([[fnum, encoded]]) end - def do_decode_type(type, val, extype) do - atom_type = validate_extype!(type, extype) + def do_decode_type(_type, val, extype) do [_tag, _wire, val | _rest] = Protobuf.Decoder.decode_raw(val) - Protobuf.Decoder.decode_type_m(atom_type, :value, val) + Protobuf.Decoder.decode_type_m(extype, :value, val) end end diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex index 7627d949..ef686949 100644 --- a/lib/protobuf/field_options_processor.ex +++ b/lib/protobuf/field_options_processor.ex @@ -23,81 +23,23 @@ defmodule Protobuf.FieldOptionsProcessor do @callback encode_type(type, value, options) :: binary @callback decode_type(val :: binary, type, options) :: value - @wrappers [ - Google.Protobuf.DoubleValue, - Google.Protobuf.FloatValue, - Google.Protobuf.Int64Value, - Google.Protobuf.UInt64Value, - Google.Protobuf.Int32Value, - Google.Protobuf.UInt32Value, - Google.Protobuf.BoolValue, - Google.Protobuf.StringValue, - Google.Protobuf.BytesValue - ] - - @wrappers_str [ - "Google.Protobuf.DoubleValue", - "Google.Protobuf.FloatValue", - "Google.Protobuf.Int64Value", - "Google.Protobuf.UInt64Value", - "Google.Protobuf.Int32Value", - "Google.Protobuf.UInt32Value", - "Google.Protobuf.BoolValue", - "Google.Protobuf.StringValue", - "Google.Protobuf.BytesValue" - ] - - def get_extype_mod(type) do - cond do - type in @wrappers -> Protobuf.Extype.Wrappers - type == Google.Protobuf.Timestamp -> Protobuf.Extype.Timestamp - true -> raise "Sorry #{type} does not support the field option extype" - end - end - - def get_extype_mod_string(:TYPE_MESSAGE, type) do - cond do - type in @wrappers_str -> Protobuf.Extype.Wrappers - type == "Google.Protobuf.Timestamp" -> Protobuf.Extype.Timestamp - true -> raise "Sorry #{type} does not support the field option extype" - end - end - - def validate_options_str!(type_enum, type, extype: extype) do - {get_extype_mod_string(type_enum, type), extype} - end - def validate_options_str!(_, type, options) do - raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" - end - - def validate_options!(type, extype: extype), do: {get_extype_mod(type), extype} - def validate_options!(type, options) do - raise "The custom field option is invalid. Options: #{inspect(options)} incompatible with type: #{type}" - end - - - def type_to_spec(type_enum, type, repeated, options) do - {module, option_value} = validate_options_str!(type_enum, type, options) - module.do_type_to_spec(type, repeated, option_value) + def type_to_spec(_type_enum, type, repeated, [extype: extype]) do + Extype.type_to_spec(type, repeated, extype) end - def type_default(type, options) do - {module, option_value} = validate_options!(type, options) - module.do_type_default(type, option_value) + def type_default(type, [extype: extype]) do + Extype.type_default(type, extype) end - def new(type, value, options) do - {module, option_value} = validate_options!(type, options) - module.do_new(type, value, option_value) + def new(type, value, [extype: extype]) do + Extype.new(type, value, extype) end - def encode_type(type, v, options) do - {module, option_value} = validate_options!(type, options) - module.do_encode_type(type, v, option_value) + def encode_type(type, v, [extype: extype]) do + Extype.encode_type(type, v, extype) end - def decode_type(val, type, options) do - {module, option_value} = validate_options!(type, options) - module.do_decode_type(type, val, option_value) + def decode_type(val, type, [extype: extype]) do + Extype.decode_type(val, type, extype) end end diff --git a/test/protobuf/field_options_processor_test.exs b/test/protobuf/field_options_processor_test.exs index 2100c9d1..b2c66851 100644 --- a/test/protobuf/field_options_processor_test.exs +++ b/test/protobuf/field_options_processor_test.exs @@ -18,27 +18,12 @@ defmodule Protobuf.FieldOptionsProcessorTest do end test "type_to_spec repeated" do - extype = "String.t()" - assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", true, [extype: extype]) == + assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", true, [extype: "String.t()"]) == "[String.t()]" - end - test "type_to_spec invalid extype" do - extype = "integer" - assert_raise RuntimeError, "Invalid extype pairing, " <> - "integer not compatible with Google.Protobuf.StringValue", - fn -> - FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", false, [extype: extype]) - end - end - - test "type_to_spec invalid type" do - extype = "String.t()" - assert_raise RuntimeError, - "Sorry Google.Protobuf.UnrealValue does not support the field option extype", - fn -> - FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.UnrealValue", false, [extype: extype]) - end + # Note: Doesn't check against bad values + FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.UnrealValue", false, [extype: "vfdkhnlim"]) == + "vfdkhnlim | nil" end test "type_default" do From c3c2cd2edaaa5d8b88f64a6f52ec8e1a737713e4 Mon Sep 17 00:00:00 2001 From: Lizzie Paquette Date: Wed, 29 Apr 2020 10:27:54 -0700 Subject: [PATCH 15/15] address comments: ' --- lib/protobuf/builder.ex | 2 + lib/protobuf/decoder.ex | 40 +++++++--------- lib/protobuf/extype/extype_protocol.ex | 47 +++++++++++++------ lib/protobuf/extype/timestamp.ex | 12 ++--- lib/protobuf/extype/wrappers.ex | 10 ++-- lib/protobuf/field_options_processor.ex | 23 +++++---- lib/protobuf/protoc/cli.ex | 5 ++ .../protobuf/field_options_processor_test.exs | 13 +++-- .../protoc/generator/message_test.exs | 2 +- 9 files changed, 86 insertions(+), 68 deletions(-) diff --git a/lib/protobuf/builder.ex b/lib/protobuf/builder.ex index 3dc02de6..33d196f5 100644 --- a/lib/protobuf/builder.ex +++ b/lib/protobuf/builder.ex @@ -74,6 +74,8 @@ defmodule Protobuf.Builder do v = cond do not is_nil(f_props.options) -> Protobuf.FieldOptionsProcessor.new(f_props.type, v, f_props.options) + not is_nil(f_props.options) and f_props.repeated? -> + Enum.map(v, fn i -> Protobuf.FieldOptionsProcessor.new(f_props.type, i, f_props.options) end) f_props.embedded? and f_props.repeated? -> Enum.map(v, fn i -> f_props.type.new(i) end) f_props.embedded? -> f_props.type.new(v) true -> v diff --git a/lib/protobuf/decoder.ex b/lib/protobuf/decoder.ex index 9a24f752..97642024 100644 --- a/lib/protobuf/decoder.ex +++ b/lib/protobuf/decoder.ex @@ -149,36 +149,32 @@ defmodule Protobuf.Decoder do key = if oneof, do: oneof_field(prop, msg_props), else: name_atom struct = - if not is_nil(options) do - embedded_msg = Protobuf.FieldOptionsProcessor.decode_type(val, type, options) - val = if is_map, do: %{embedded_msg.key => embedded_msg.value}, else: embedded_msg + if is_nil(options) and not embedded do + val = decode_type_m(type, key, val) val = if oneof, do: {name_atom, val}, else: val - val = merge_embedded_value(struct, key, val, is_repeated) + val = + if is_repeated do + merge_simple_repeated_value(struct, key, val) + else + val + end Map.put(struct, key, val) else - if embedded do - embedded_msg = decode(val, type) - val = if is_map, do: %{embedded_msg.key => embedded_msg.value}, else: embedded_msg - val = if oneof, do: {name_atom, val}, else: val - - val = merge_embedded_value(struct, key, val, is_repeated) + embedded_msg = + if is_nil(options) do + decode(val, type) + else + Protobuf.FieldOptionsProcessor.decode_type(val, type, options) + end - Map.put(struct, key, val) - else - val = decode_type_m(type, key, val) - val = if oneof, do: {name_atom, val}, else: val + val = if is_map, do: %{embedded_msg.key => embedded_msg.value}, else: embedded_msg + val = if oneof, do: {name_atom, val}, else: val - val = - if is_repeated do - merge_simple_repeated_value(struct, key, val) - else - val - end + val = merge_embedded_value(struct, key, val, is_repeated) - Map.put(struct, key, val) - end + Map.put(struct, key, val) end build_struct(rest, msg_props, struct) diff --git a/lib/protobuf/extype/extype_protocol.ex b/lib/protobuf/extype/extype_protocol.ex index 1d12b8d1..b5de27e7 100644 --- a/lib/protobuf/extype/extype_protocol.ex +++ b/lib/protobuf/extype/extype_protocol.ex @@ -1,7 +1,11 @@ defprotocol Extype.Protocol do - # Imagine registering an extension by defining a protocol - - @type extype :: atom + @moduledoc """ + Protocol for defining an elixir type for a protobuf type. + """ + @typedoc """ + An elixir type. + """ + @type extype :: String.t() @typedoc """ The existing type of the field. Often the module name of the struct. @@ -16,17 +20,17 @@ defprotocol Extype.Protocol do @spec validate_and_to_atom_extype!(type, option :: String.t) :: atom def validate_and_to_atom_extype!(type, option) - @spec do_type_default(type, extype) :: any - def do_type_default(type, extype) + @spec type_default(type, extype) :: any + def type_default(type, extype) - @spec do_new(type, value, extype) :: value - def do_new(type, value, extype) + @spec new(type, value, extype) :: value + def new(type, value, extype) - @spec do_encode_type(type, value, extype) :: binary - def do_encode_type(type, v, extype) + @spec encode_type(type, value, extype) :: binary + def encode_type(type, v, extype) - @spec do_decode_type(type, val :: binary, extype) :: value - def do_decode_type(val, type, extype) + @spec decode_type(type, val :: binary, extype) :: value + def decode_type(val, type, extype) end defmodule Extype do @@ -48,6 +52,7 @@ defmodule Extype do @spec type_to_spec(type :: String.t(), repeated :: boolean, extype) :: String.t() def type_to_spec(_type, repeated, extype) do + extype = pad_parens(extype) if repeated do "[#{extype}]" else @@ -58,28 +63,40 @@ defmodule Extype do @spec type_default(type, extype) :: any def type_default(type, extype) do mod = get_mod(type) + extype = pad_parens(extype) atom_extype = mod.validate_and_to_atom_extype!(type, extype) - mod.do_type_default(type, atom_extype) + mod.type_default(type, atom_extype) end @spec new(type, value, extype) :: value def new(type, value, extype) do mod = get_mod(type) + extype = pad_parens(extype) atom_extype = mod.validate_and_to_atom_extype!(type, extype) - mod.do_new(type, value, atom_extype) + mod.new(type, value, atom_extype) end @spec encode_type(type, value, extype) :: binary def encode_type(type, v, extype) do mod = get_mod(type) + extype = pad_parens(extype) atom_extype = mod.validate_and_to_atom_extype!(type, extype) - mod.do_encode_type(type, v, atom_extype) + mod.encode_type(type, v, atom_extype) end @spec decode_type(val :: binary, type, extype) :: value def decode_type(val, type, extype) do mod = get_mod(type) + extype = pad_parens(extype) atom_extype = mod.validate_and_to_atom_extype!(type, extype) - mod.do_decode_type(type, val, atom_extype) + mod.decode_type(type, val, atom_extype) + end + + defp pad_parens(extype) do + if String.ends_with?(extype, ".t") do + extype <> "()" + else + extype + end end end diff --git a/lib/protobuf/extype/timestamp.ex b/lib/protobuf/extype/timestamp.ex index 3a38db19..0dfc44c9 100644 --- a/lib/protobuf/extype/timestamp.ex +++ b/lib/protobuf/extype/timestamp.ex @@ -1,22 +1,20 @@ -defimpl Extype.Protocol, for: [Google.Protobuf.Timestamp] do +defimpl Extype.Protocol, for: Google.Protobuf.Timestamp do @moduledoc """ Implement DateTime and NaiveDateTime casting for Google Timestamp. """ - def validate_and_to_atom_extype!(Google.Protobuf.Timestamp, "NaiveDateTime.t"), do: :naivedatetime def validate_and_to_atom_extype!(Google.Protobuf.Timestamp, "NaiveDateTime.t()"), do: :naivedatetime - def validate_and_to_atom_extype!(Google.Protobuf.Timestamp, "DateTime.t"), do: :datetime def validate_and_to_atom_extype!(Google.Protobuf.Timestamp, "DateTime.t()"), do: :datetime def validate_and_to_atom_extype!(type, extype) do raise "Invalid extype pairing, #{extype} not compatible with #{type}. " <> "Supported types are DateTime.t() or NaiveDateTime.t()" end - def do_type_default(_type, _extype), do: nil + def type_default(_type, _extype), do: nil - def do_new(_type, value, _extype), do: value + def new(_type, value, _extype), do: value - def do_encode_type(_type, v, extype) do + def encode_type(_type, v, extype) do v = if extype == :naivedatetime, do: DateTime.from_naive!(v, "Etc/UTC"), else: v unix = DateTime.to_unix(v, :nanosecond) @@ -29,7 +27,7 @@ defimpl Extype.Protocol, for: [Google.Protobuf.Timestamp] do Protobuf.encode(value) end - def do_decode_type(type, val, extype) do + def decode_type(type, val, extype) do protobuf_timestamp = Protobuf.decode(val, type) value = diff --git a/lib/protobuf/extype/wrappers.ex b/lib/protobuf/extype/wrappers.ex index b2ffaba4..2a6ca8ba 100644 --- a/lib/protobuf/extype/wrappers.ex +++ b/lib/protobuf/extype/wrappers.ex @@ -25,17 +25,15 @@ defimpl Extype.Protocol, for: [ def validate_and_to_atom_extype!(Google.Protobuf.Int32Value, "integer"), do: :int32 def validate_and_to_atom_extype!(Google.Protobuf.UInt32Value, "non_neg_integer"), do: :uint32 def validate_and_to_atom_extype!(Google.Protobuf.BoolValue, "boolean"), do: :bool - def validate_and_to_atom_extype!(Google.Protobuf.StringValue, "String.t"), do: :string def validate_and_to_atom_extype!(Google.Protobuf.StringValue, "String.t()"), do: :string - def validate_and_to_atom_extype!(Google.Protobuf.BytesValue, "String.t"), do: :bytes def validate_and_to_atom_extype!(Google.Protobuf.BytesValue, "String.t()"), do: :bytes def validate_and_to_atom_extype!(type, extype) do raise "Invalid extype pairing, #{extype} not compatible with #{type}" end - def do_type_default(_type, _extype), do: nil + def type_default(_type, _extype), do: nil - def do_new(_type, value, _extype) do + def new(_type, value, _extype) do # No type check, just shape check. if is_map(value) do raise "When extype option is present, new expects unwrapped value, not struct." @@ -44,13 +42,13 @@ defimpl Extype.Protocol, for: [ end end - def do_encode_type(type, v, extype) do + def encode_type(type, v, extype) do fnum = type.__message_props__.field_props[1].encoded_fnum encoded = Protobuf.Encoder.encode_type(extype, v) IO.iodata_to_binary([[fnum, encoded]]) end - def do_decode_type(_type, val, extype) do + def decode_type(_type, val, extype) do [_tag, _wire, val | _rest] = Protobuf.Decoder.decode_raw(val) Protobuf.Decoder.decode_type_m(extype, :value, val) end diff --git a/lib/protobuf/field_options_processor.ex b/lib/protobuf/field_options_processor.ex index ef686949..dd3d6ee7 100644 --- a/lib/protobuf/field_options_processor.ex +++ b/lib/protobuf/field_options_processor.ex @@ -23,23 +23,22 @@ defmodule Protobuf.FieldOptionsProcessor do @callback encode_type(type, value, options) :: binary @callback decode_type(val :: binary, type, options) :: value + def type_to_spec(type_enum, type, repeated, []) do + Protobuf.TypeUtil.enum_to_spec(type_enum, type, repeated) + end def type_to_spec(_type_enum, type, repeated, [extype: extype]) do Extype.type_to_spec(type, repeated, extype) end - def type_default(type, [extype: extype]) do - Extype.type_default(type, extype) - end + def type_default(type, []), do: Protobuf.Builder.type_default(type) + def type_default(type, [extype: extype]), do: Extype.type_default(type, extype) - def new(type, value, [extype: extype]) do - Extype.new(type, value, extype) - end + def new(type, value, []), do: type.new(value) + def new(type, value, [extype: extype]), do: Extype.new(type, value, extype) - def encode_type(type, v, [extype: extype]) do - Extype.encode_type(type, v, extype) - end + def encode_type(type, v, []), do: Protobuf.Encoder.encode(type, v, []) + def encode_type(type, v, [extype: extype]), do: Extype.encode_type(type, v, extype) - def decode_type(val, type, [extype: extype]) do - Extype.decode_type(val, type, extype) - end + def decode_type(val, type, []), do: Protobuf.Decoder.decode(val, type) + def decode_type(val, type, [extype: extype]), do: Extype.decode_type(val, type, extype) end diff --git a/lib/protobuf/protoc/cli.ex b/lib/protobuf/protoc/cli.ex index f6f5734c..0b0c63ae 100644 --- a/lib/protobuf/protoc/cli.ex +++ b/lib/protobuf/protoc/cli.ex @@ -71,6 +71,11 @@ defmodule Protobuf.Protoc.CLI do parse_params(ctx, t) end + def parse_params(ctx, ["custom_field_options=false" | t]) do + ctx = %{ctx | custom_field_options?: false} + parse_params(ctx, t) + end + def parse_params(ctx, _), do: ctx @doc false diff --git a/test/protobuf/field_options_processor_test.exs b/test/protobuf/field_options_processor_test.exs index b2c66851..f5f27041 100644 --- a/test/protobuf/field_options_processor_test.exs +++ b/test/protobuf/field_options_processor_test.exs @@ -8,7 +8,7 @@ defmodule Protobuf.FieldOptionsProcessorTest do test "type_to_spec String.t and StringValue" do extype = "String.t" assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", false, [extype: extype]) == - extype <> " | nil" + extype <> "() | nil" end test "type_to_spec String.t() and StringValue" do @@ -22,7 +22,7 @@ defmodule Protobuf.FieldOptionsProcessorTest do "[String.t()]" # Note: Doesn't check against bad values - FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.UnrealValue", false, [extype: "vfdkhnlim"]) == + assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.UnrealValue", false, [extype: "vfdkhnlim"]) == "vfdkhnlim | nil" end @@ -34,10 +34,14 @@ defmodule Protobuf.FieldOptionsProcessorTest do assert is_nil(FieldOptionsProcessor.type_default(Google.Protobuf.Timestamp, extype: "DateTime.t")) assert is_nil(FieldOptionsProcessor.type_default(Google.Protobuf.Timestamp, extype: "NaiveDateTime.t()")) - # Typo - assert_raise RuntimeError, "Invalid extype pairing, Datetime.t not compatible with " <> + # Typo in extype + assert_raise RuntimeError, "Invalid extype pairing, Datetime.t() not compatible with " <> "Elixir.Google.Protobuf.Timestamp. Supported types are DateTime.t() or NaiveDateTime.t()", fn -> FieldOptionsProcessor.type_default(Google.Protobuf.Timestamp, extype: "Datetime.t") end + + # Unsupported struct and bad type + assert_raise RuntimeError, "Sorry Elixir.Google.Protobuf.UnrealValue does not support the field option extype", + fn -> FieldOptionsProcessor.type_default(Google.Protobuf.UnrealValue, [extype: "vfdkhnlim"]) end end test "encoding and decoding timestamp" do @@ -106,5 +110,4 @@ defmodule Protobuf.FieldOptionsProcessorTest do # They are equal assert dt2 == result2 end - end diff --git a/test/protobuf/protoc/generator/message_test.exs b/test/protobuf/protoc/generator/message_test.exs index 5cdd3b48..68c2fa5d 100644 --- a/test/protobuf/protoc/generator/message_test.exs +++ b/test/protobuf/protoc/generator/message_test.exs @@ -273,7 +273,7 @@ defmodule Protobuf.Protoc.Generator.MessageTest do {[], [msg]} = Generator.generate(ctx, desc) assert msg =~ "use Protobuf, custom_field_options?: true" - assert msg =~ "a: String.t | nil" + assert msg =~ "a: String.t() | nil" assert msg =~ "b: Google.Protobuf.StringValue.t | nil" assert msg =~ "field :a, 1, optional: true, type: Google.Protobuf.StringValue, options: [extype: \"String.t\"]\n" assert msg =~ "field :b, 1, optional: true, type: Google.Protobuf.StringValue\n\nend"