diff --git a/Makefile b/Makefile index 6fbacd16..3df1587d 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +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/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/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/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 diff --git a/lib/protobuf/builder.ex b/lib/protobuf/builder.ex index e13124a5..33d196f5 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,13 @@ 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) + 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 end Map.put(acc, k, v) diff --git a/lib/protobuf/decoder.ex b/lib/protobuf/decoder.ex index 8cd2b9d4..97642024 100644 --- a/lib/protobuf/decoder.ex +++ b/lib/protobuf/decoder.ex @@ -142,21 +142,14 @@ 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) - 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) - - Map.put(struct, key, val) - else + if is_nil(options) and not embedded do val = decode_type_m(type, key, val) val = if oneof, do: {name_atom, val}, else: val @@ -167,6 +160,20 @@ defmodule Protobuf.Decoder do val end + Map.put(struct, key, val) + else + embedded_msg = + if is_nil(options) do + decode(val, type) + else + Protobuf.FieldOptionsProcessor.decode_type(val, type, options) + end + + 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) + Map.put(struct, key, val) end 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/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/extype/extype_protocol.ex b/lib/protobuf/extype/extype_protocol.ex new file mode 100644 index 00000000..b5de27e7 --- /dev/null +++ b/lib/protobuf/extype/extype_protocol.ex @@ -0,0 +1,102 @@ +defprotocol Extype.Protocol do + @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. + """ + @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 type_default(type, extype) :: any + def type_default(type, extype) + + @spec new(type, value, extype) :: value + def new(type, value, extype) + + @spec encode_type(type, value, extype) :: binary + def encode_type(type, v, extype) + + @spec decode_type(type, val :: binary, extype) :: value + def 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 + extype = pad_parens(extype) + 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) + extype = pad_parens(extype) + atom_extype = mod.validate_and_to_atom_extype!(type, 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.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.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.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 new file mode 100644 index 00000000..0dfc44c9 --- /dev/null +++ b/lib/protobuf/extype/timestamp.ex @@ -0,0 +1,41 @@ +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, "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 type_default(_type, _extype), do: nil + + def new(_type, value, _extype), do: value + + 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) + + 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 decode_type(type, val, extype) do + 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 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 new file mode 100644 index 00000000..2a6ca8ba --- /dev/null +++ b/lib/protobuf/extype/wrappers.ex @@ -0,0 +1,55 @@ +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. + """ + + require Protobuf.Decoder + require Logger + import Protobuf.Decoder, only: [decode_zigzag: 1] + + 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.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 type_default(_type, _extype), do: nil + + 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." + else + value + end + end + + 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 decode_type(_type, val, extype) do + [_tag, _wire, val | _rest] = Protobuf.Decoder.decode_raw(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 new file mode 100644 index 00000000..dd3d6ee7 --- /dev/null +++ b/lib/protobuf/field_options_processor.ex @@ -0,0 +1,44 @@ +defmodule Protobuf.FieldOptionsProcessor do + @moduledoc """ + 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, options) :: value + @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, []), do: Protobuf.Builder.type_default(type) + def type_default(type, [extype: extype]), do: Extype.type_default(type, extype) + + 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, []), 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, []), 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/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/lib/protobuf/protoc/cli.ex b/lib/protobuf/protoc/cli.ex index 58a15047..0b0c63ae 100644 --- a/lib/protobuf/protoc/cli.ex +++ b/lib/protobuf/protoc/cli.ex @@ -66,6 +66,16 @@ 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, ["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/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 33d631a5..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, @@ -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 @@ -102,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) @@ -118,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 = @@ -146,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 @@ -177,7 +188,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 +249,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 +298,25 @@ 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) + |> 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 + + defp merge_field_options(_ctx, opts, f) do opts |> Map.put(:packed, f.options.packed) |> Map.put(:deprecated, f.options.deprecated) 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/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 diff --git a/test/protobuf/builder_test.exs b/test/protobuf/builder_test.exs index 0eee47cc..2c704fdd 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,28 @@ 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 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/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/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/encoder_test.exs b/test/protobuf/encoder_test.exs index c7f988a7..bd86d7c3 100644 --- a/test/protobuf/encoder_test.exs +++ b/test/protobuf/encoder_test.exs @@ -181,4 +181,14 @@ 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 + + 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/encoder_validation_test.exs b/test/protobuf/encoder_validation_test.exs index bdc124c2..846f476b 100644 --- a/test/protobuf/encoder_validation_test.exs +++ b/test/protobuf/encoder_validation_test.exs @@ -148,4 +148,25 @@ 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 + # should be string + msg = TestMsg.Ext.DualUseCase.new(a: 11) + + assert_raise Protobuf.EncodeError, fn -> Protobuf.Encoder.encode(msg) end + end 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..f5f27041 --- /dev/null +++ b/test/protobuf/field_options_processor_test.exs @@ -0,0 +1,113 @@ +defmodule Protobuf.FieldOptionsProcessorTest do + @moduledoc false + + 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 + assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.StringValue", true, [extype: "String.t()"]) == + "[String.t()]" + + # Note: Doesn't check against bad values + assert FieldOptionsProcessor.type_to_spec(:TYPE_MESSAGE, "Google.Protobuf.UnrealValue", false, [extype: "vfdkhnlim"]) == + "vfdkhnlim | nil" + 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 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 + 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 + + 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 0a627a40..8675f88b 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 605b1780..68c2fa5d 100644 --- a/test/protobuf/protoc/generator/message_test.exs +++ b/test/protobuf/protoc/generator/message_test.exs @@ -173,6 +173,112 @@ 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 =~ "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" + 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: "", + custom_field_options?: true + } + + 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, custom_field_options?: true" + 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 + test "generete/2 supports message type field" do ctx = %Context{ package: "", diff --git a/test/protobuf/protoc/integration_test.exs b/test/protobuf/protoc/integration_test.exs index f4f70498..86d58a7b 100644 --- a/test/protobuf/protoc/integration_test.exs +++ b/test/protobuf/protoc/integration_test.exs @@ -54,5 +54,38 @@ 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: "s1", b: Google.Protobuf.StringValue.new(value: "s2")) + + assert dual.a == "s1" + 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 + + 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/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/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/extension.pb.ex b/test/protobuf/protoc/proto_gen/extension.pb.ex index 8fbddfe9..dd66190c 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() @@ -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, custom_field_options?: true, syntax: :proto2 + + @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 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 diff --git a/test/support/test_msg.ex b/test/support/test_msg.ex index 9d60154d..858abb12 100644 --- a/test/support/test_msg.ex +++ b/test/support/test_msg.ex @@ -212,4 +212,32 @@ 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 + + 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