diff --git a/.formatter.exs b/.formatter.exs index 8351d69..f22de05 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,4 @@ [ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"], + inputs: ["mix.exs", "{config,lib,priv,test}/**/*.{ex,exs}"], import_deps: [:protobuf] ] diff --git a/README.md b/README.md index 8b05aa7..ece4691 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,27 @@ # Google Protos -For Elixir files generated from [Google's protobuf files](https://github.com/google/protobuf/tree/master/src/google/protobuf) using [protobuf-elixir](https://github.com/tony612/protobuf-elixir). +For Elixir files generated from [Google's protobuf files](https://github.com/google/protobuf/tree/master/src/google/protobuf) using [protobuf-elixir](https://github.com/elixir-protobuf/protobuf). -## How-To +By including `google_protos` in your `mix.exs`, you'll have access to _some_ of the `google.protobuf.*` modules already compiled to Elixir. This can help simplify your protobuf compiling pipeline. For your benefit, we also include some helper functions for converting the protobuf structs to native Elixir modules. See the documentation for more details. -### Convert `Google.Protobuf.Timestamp` to `DateTime` +## Documentation -1. Convert timestamp seconds to nanoseconds. -2. Add the timestamp nanos. -3. Convert to `DateTime` using `DateTime.from_unix!/2`. +Documentation is available on [hex.pm](https://hexdocs.pm/google_protos/). + +## Usage + +Include the `:google_protos` package in your `mix.exs` dependency list: ```elixir -timestamp = Google.Protobuf.Timestamp.new(seconds: 5, nanos: 100) -DateTime.from_unix!(timestamp.seconds * 1_000_000_000 + timestamp.nanos, :nanosecond) +defp deps do + [ + {:google_protos, "~> 0.3"} + ] +end ``` + +Use the `Google.Protobuf` modules. See [google_protos](https://hexdocs.pm/google_protos/) and [Elixir protobuf](https://hexdocs.pm/protobuf) documentation for more details. + +## Limits + +The Google Protos repository will only compile official Google protobuf messages, and will only include helper functions for _native_ Elixir modules. We do not accept PRs adding support for third party libraries (ecto, decimal, money, etc.) If there is a Google Protobuf message that maps nicely to an Elixir module where we don't include helpers, please feel free to open a PR. diff --git a/generate_google_protos.sh b/generate_google_protos.sh index b537c26..f434d2d 100755 --- a/generate_google_protos.sh +++ b/generate_google_protos.sh @@ -10,8 +10,26 @@ PROTOS=(" protobuf/src/google/protobuf/wrappers.proto ") -rm -rf ./lib/google_protos/* +rm -rf ./lib/google_protos/*.pb.ex for file in $PROTOS; do protoc -I ./protobuf/src/google/protobuf/ --elixir_out=plugins=grpc:./lib/google_protos $file done + +for template_file in priv/templates/*.pb.ex; do + file_name=$(basename "$template_file") + replacement_file="lib/google_protos/$file_name" + + if [ -f "$replacement_file" ]; then + for module in $(sed -n -E "s/defmodule (.+) do/\1/p" "$template_file"); do + replacement_text=$(cat "$template_file" | sed -n "/^defmodule $module do$/,/^end$/p" | sed 1,2d) + line_to_replace=$(cat -n "$replacement_file" | sed -n "/defmodule $module do$/,/end$/p" | grep "end" | awk '{print $1}') + + export REPLACEMENT_TEXT=$replacement_text + perl -i -pe "s/end/\$ENV{\"REPLACEMENT_TEXT\"}/g if $. == $line_to_replace" "$replacement_file" + unset REPLACEMENT_TEXT + done + fi +done + +mix format diff --git a/lib/google_protos/any.pb.ex b/lib/google_protos/any.pb.ex index e7fc918..7472c29 100644 --- a/lib/google_protos/any.pb.ex +++ b/lib/google_protos/any.pb.ex @@ -1,6 +1,7 @@ defmodule Google.Protobuf.Any do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :type_url, 1, type: :string, json_name: "typeUrl" field :value, 2, type: :bytes diff --git a/lib/google_protos/duration.pb.ex b/lib/google_protos/duration.pb.ex index 0de562c..ad98262 100644 --- a/lib/google_protos/duration.pb.ex +++ b/lib/google_protos/duration.pb.ex @@ -1,7 +1,69 @@ defmodule Google.Protobuf.Duration do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :seconds, 1, type: :int64 field :nanos, 2, type: :int32 + + @doc """ + Converts a `Google.Protobuf.Duration` to a value and `System.time_unit()`. + + ## Examples + + iex> Duration.to_time_unit(%Duration{seconds: 10}) + {10, :second} + + iex> Duration.to_time_unit(%Duration{seconds: 20, nanos: 100}) + {20_000_000_100, :nanosecond} + + """ + @spec to_time_unit(__MODULE__.t()) :: {integer(), System.time_unit()} + def to_time_unit(%{seconds: seconds, nanos: 0}) do + {seconds, :second} + end + + def to_time_unit(%{seconds: seconds, nanos: nanos}) do + {seconds * 1_000_000_000 + nanos, :nanosecond} + end + + @doc """ + Converts a value and `System.time_unit()` to a `Google.Protobuf.Duration`. + + ## Examples + + iex> Duration.from_time_unit(11, :second) + %Duration{seconds: 11} + + iex> Duration.from_time_unit(11_111_111, :microsecond) + %Duration{seconds: 11, nanos: 111_111_000} + + """ + @spec from_time_unit(integer(), System.time_unit()) :: __MODULE__.t() + def from_time_unit(seconds, :second) do + struct(__MODULE__, %{ + seconds: seconds + }) + end + + def from_time_unit(millisecond, :millisecond) do + struct(__MODULE__, %{ + seconds: div(millisecond, 1_000), + nanos: rem(millisecond, 1_000) * 1_000_000 + }) + end + + def from_time_unit(millisecond, :microsecond) do + struct(__MODULE__, %{ + seconds: div(millisecond, 1_000_000), + nanos: rem(millisecond, 1_000_000) * 1_000 + }) + end + + def from_time_unit(millisecond, :nanosecond) do + struct(__MODULE__, %{ + seconds: div(millisecond, 1_000_000_000), + nanos: rem(millisecond, 1_000_000_000) + }) + end end diff --git a/lib/google_protos/empty.pb.ex b/lib/google_protos/empty.pb.ex index 74f7005..334a639 100644 --- a/lib/google_protos/empty.pb.ex +++ b/lib/google_protos/empty.pb.ex @@ -1,4 +1,5 @@ defmodule Google.Protobuf.Empty do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" end diff --git a/lib/google_protos/field_mask.pb.ex b/lib/google_protos/field_mask.pb.ex index 045d7d1..6950164 100644 --- a/lib/google_protos/field_mask.pb.ex +++ b/lib/google_protos/field_mask.pb.ex @@ -1,6 +1,7 @@ defmodule Google.Protobuf.FieldMask do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :paths, 1, repeated: true, type: :string end diff --git a/lib/google_protos/struct.pb.ex b/lib/google_protos/struct.pb.ex index 6053127..3835e1e 100644 --- a/lib/google_protos/struct.pb.ex +++ b/lib/google_protos/struct.pb.ex @@ -1,13 +1,15 @@ defmodule Google.Protobuf.NullValue do @moduledoc false - use Protobuf, enum: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :NULL_VALUE, 0 end defmodule Google.Protobuf.Struct.FieldsEntry do @moduledoc false - use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :key, 1, type: :string field :value, 2, type: Google.Protobuf.Value @@ -15,14 +17,92 @@ end defmodule Google.Protobuf.Struct do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :fields, 1, repeated: true, type: Google.Protobuf.Struct.FieldsEntry, map: true + + @doc """ + Converts a `Google.Protobuf.Struct` to a `map()`. + + ## Examples + + iex> Struct.to_map(%Struct{}) + %{} + + """ + @spec to_map(__MODULE__.t()) :: map() + def to_map(struct) do + Map.new(struct.fields, fn {k, v} -> + {k, to_map_value(v)} + end) + end + + defp to_map_value(%{kind: {:null_value, :NULL_VALUE}}), do: nil + defp to_map_value(%{kind: {:number_value, value}}), do: value + defp to_map_value(%{kind: {:string_value, value}}), do: value + defp to_map_value(%{kind: {:bool_value, value}}), do: value + + defp to_map_value(%{kind: {:struct_value, struct}}), + do: to_map(struct) + + defp to_map_value(%{kind: {:list_value, %{values: values}}}), + do: Enum.map(values, &to_map_value/1) + + @doc """ + Converts a `map()` to a `Google.Protobuf.Struct`. + + ## Examples + + iex> Struct.from_map(%{key: "value"}) + %Struct{} + + """ + @spec from_map(map()) :: __MODULE__.t() + def from_map(map) do + struct(__MODULE__, %{ + fields: + Map.new(map, fn {k, v} -> + {to_string(k), from_map_value(v)} + end) + }) + end + + defp from_map_value(nil) do + struct(Google.Protobuf.Value, %{kind: {:null_value, :NULL_VALUE}}) + end + + defp from_map_value(value) when is_number(value) do + struct(Google.Protobuf.Value, %{kind: {:number_value, value}}) + end + + defp from_map_value(value) when is_binary(value) do + struct(Google.Protobuf.Value, %{kind: {:string_value, value}}) + end + + defp from_map_value(value) when is_boolean(value) do + struct(Google.Protobuf.Value, %{kind: {:bool_value, value}}) + end + + defp from_map_value(value) when is_map(value) do + struct(Google.Protobuf.Value, %{kind: {:struct_value, from_map(value)}}) + end + + defp from_map_value(value) when is_list(value) do + struct(Google.Protobuf.Value, %{ + kind: + {:list_value, + struct(Google.Protobuf.ListValue, %{ + values: Enum.map(value, &from_map_value/1) + })} + }) + end end defmodule Google.Protobuf.Value do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" oneof :kind, 0 @@ -41,7 +121,8 @@ end defmodule Google.Protobuf.ListValue do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :values, 1, repeated: true, type: Google.Protobuf.Value end diff --git a/lib/google_protos/timestamp.pb.ex b/lib/google_protos/timestamp.pb.ex index 96945b3..9ef9d82 100644 --- a/lib/google_protos/timestamp.pb.ex +++ b/lib/google_protos/timestamp.pb.ex @@ -1,7 +1,50 @@ defmodule Google.Protobuf.Timestamp do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :seconds, 1, type: :int64 field :nanos, 2, type: :int32 + + @doc """ + Converts a `DateTime` struct to a `Google.Protobuf.Timestamp` struct. + + Note: Elixir `DateTime.from_unix!/2` will convert units to + microseconds internally. Nanosecond precision is not guaranteed. + See examples for details. + + ## Examples + + iex> Timestamp.to_datetime(%Timestamp{seconds: 5, nanos: 0}) + ~U[1970-01-01 00:00:05.000000Z] + + iex> one = Timestamp.to_datetime(%Timestamp{seconds: 10, nanos: 100}) + ...> two = Timestamp.to_datetime(%Timestamp{seconds: 10, nanos: 105}) + ...> DateTime.diff(one, two, :nanosecond) + 0 + + """ + @spec to_datetime(__MODULE__.t()) :: DateTime.t() + def to_datetime(%{seconds: seconds, nanos: nanos}) do + DateTime.from_unix!(seconds * 1_000_000_000 + nanos, :nanosecond) + end + + @doc """ + Converts a `Google.Protobuf.Timestamp` struct to a `DateTime` struct. + + ## Examples + + iex> Timestamp.from_datetime(~U[1970-01-01 00:00:05.000000Z]) + %Timestamp{seconds: 5, nanos: 0} + + """ + @spec from_datetime(DateTime.t()) :: __MODULE__.t() + def from_datetime(%DateTime{} = datetime) do + nanoseconds = DateTime.to_unix(datetime, :nanosecond) + + struct(__MODULE__, %{ + seconds: div(nanoseconds, 1_000_000_000), + nanos: rem(nanoseconds, 1_000_000_000) + }) + end end diff --git a/lib/google_protos/wrappers.pb.ex b/lib/google_protos/wrappers.pb.ex index 8bdd62a..bd6eef2 100644 --- a/lib/google_protos/wrappers.pb.ex +++ b/lib/google_protos/wrappers.pb.ex @@ -1,62 +1,71 @@ defmodule Google.Protobuf.DoubleValue do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :double end defmodule Google.Protobuf.FloatValue do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :float end defmodule Google.Protobuf.Int64Value do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :int64 end defmodule Google.Protobuf.UInt64Value do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :uint64 end defmodule Google.Protobuf.Int32Value do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :int32 end defmodule Google.Protobuf.UInt32Value do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :uint32 end defmodule Google.Protobuf.BoolValue do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :bool end defmodule Google.Protobuf.StringValue do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :string end defmodule Google.Protobuf.BytesValue do @moduledoc false - use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" field :value, 1, type: :bytes end diff --git a/mix.exs b/mix.exs index 037d830..cf18b08 100644 --- a/mix.exs +++ b/mix.exs @@ -9,6 +9,7 @@ defmodule GoogleProtos.MixProject do elixir: "~> 1.6", start_permanent: Mix.env() == :prod, deps: deps(), + docs: docs(), description: "Protos by Google", package: package() ] @@ -20,8 +21,18 @@ defmodule GoogleProtos.MixProject do defp deps do [ - {:protobuf, "~> 0.10"}, - {:ex_doc, ">= 0.0.0", only: :dev} + {:protobuf, "~> 0.12"}, + {:ex_doc, "~> 0.29", only: :dev}, + {:jason, "~> 1.4", only: :test} + ] + end + + defp docs do + [ + extras: [ + "README.md": [filename: "overview", title: "Overview"] + ], + main: "overview" ] end diff --git a/mix.lock b/mix.lock index 13cf6ea..7c64595 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,10 @@ %{ - "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm", "1b34655872366414f69dd987cb121c049f76984b6ac69f52fff6d8fd64d29cfd"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"}, - "ex_doc": {:hex, :ex_doc, "0.25.5", "ac3c5425a80b4b7c4dfecdf51fa9c23a44877124dd8ca34ee45ff608b1c6deb9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "688cfa538cdc146bc4291607764a7f1fcfa4cce8009ecd62de03b27197528350"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, + "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, - "protobuf": {:hex, :protobuf, "0.10.0", "4e8e3cf64c5be203b329f88bb8b916cb8d00fb3a12b2ac1f545463ae963c869f", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4ae21a386142357aa3d31ccf5f7d290f03f3fa6f209755f6e87fc2c58c147893"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "protobuf": {:hex, :protobuf, "0.12.0", "58c0dfea5f929b96b5aa54ec02b7130688f09d2de5ddc521d696eec2a015b223", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "75fa6cbf262062073dd51be44dd0ab940500e18386a6c4e87d5819a58964dc45"}, } diff --git a/priv/templates/README.md b/priv/templates/README.md new file mode 100644 index 0000000..382d2c1 --- /dev/null +++ b/priv/templates/README.md @@ -0,0 +1,9 @@ +# Templates + +These files are placed in the Protobuf generated Elixir modules. For them to match and be inserted: + +1) The template file must match the file in `lib/google_protos`. + +2) The module name must match what module you want the data inserted into. + +3) The template file must include `@moduledoc false` (we remove the first two lines.) diff --git a/priv/templates/duration.pb.ex b/priv/templates/duration.pb.ex new file mode 100644 index 0000000..c7e11a1 --- /dev/null +++ b/priv/templates/duration.pb.ex @@ -0,0 +1,64 @@ +defmodule Google.Protobuf.Duration do + @moduledoc false + + @doc """ + Converts a `Google.Protobuf.Duration` to a value and `System.time_unit()`. + + ## Examples + + iex> Duration.to_time_unit(%Duration{seconds: 10}) + {10, :second} + + iex> Duration.to_time_unit(%Duration{seconds: 20, nanos: 100}) + {20_000_000_100, :nanosecond} + + """ + @spec to_time_unit(__MODULE__.t()) :: {integer(), System.time_unit()} + def to_time_unit(%{seconds: seconds, nanos: 0}) do + {seconds, :second} + end + + def to_time_unit(%{seconds: seconds, nanos: nanos}) do + {seconds * 1_000_000_000 + nanos, :nanosecond} + end + + @doc """ + Converts a value and `System.time_unit()` to a `Google.Protobuf.Duration`. + + ## Examples + + iex> Duration.from_time_unit(11, :second) + %Duration{seconds: 11} + + iex> Duration.from_time_unit(11_111_111, :microsecond) + %Duration{seconds: 11, nanos: 111_111_000} + + """ + @spec from_time_unit(integer(), System.time_unit()) :: __MODULE__.t() + def from_time_unit(seconds, :second) do + struct(__MODULE__, %{ + seconds: seconds + }) + end + + def from_time_unit(millisecond, :millisecond) do + struct(__MODULE__, %{ + seconds: div(millisecond, 1_000), + nanos: rem(millisecond, 1_000) * 1_000_000 + }) + end + + def from_time_unit(millisecond, :microsecond) do + struct(__MODULE__, %{ + seconds: div(millisecond, 1_000_000), + nanos: rem(millisecond, 1_000_000) * 1_000 + }) + end + + def from_time_unit(millisecond, :nanosecond) do + struct(__MODULE__, %{ + seconds: div(millisecond, 1_000_000_000), + nanos: rem(millisecond, 1_000_000_000) + }) + end +end diff --git a/priv/templates/struct.pb.ex b/priv/templates/struct.pb.ex new file mode 100644 index 0000000..0184387 --- /dev/null +++ b/priv/templates/struct.pb.ex @@ -0,0 +1,79 @@ +defmodule Google.Protobuf.Struct do + @moduledoc false + + @doc """ + Converts a `Google.Protobuf.Struct` to a `map()`. + + ## Examples + + iex> Struct.to_map(%Struct{}) + %{} + + """ + @spec to_map(__MODULE__.t()) :: map() + def to_map(struct) do + Map.new(struct.fields, fn {k, v} -> + {k, to_map_value(v)} + end) + end + + defp to_map_value(%{kind: {:null_value, :NULL_VALUE}}), do: nil + defp to_map_value(%{kind: {:number_value, value}}), do: value + defp to_map_value(%{kind: {:string_value, value}}), do: value + defp to_map_value(%{kind: {:bool_value, value}}), do: value + + defp to_map_value(%{kind: {:struct_value, struct}}), + do: to_map(struct) + + defp to_map_value(%{kind: {:list_value, %{values: values}}}), + do: Enum.map(values, &to_map_value/1) + + @doc """ + Converts a `map()` to a `Google.Protobuf.Struct`. + + ## Examples + + iex> Struct.from_map(%{key: "value"}) + %Struct{} + + """ + @spec from_map(map()) :: __MODULE__.t() + def from_map(map) do + struct(__MODULE__, %{ + fields: + Map.new(map, fn {k, v} -> + {to_string(k), from_map_value(v)} + end) + }) + end + + defp from_map_value(nil) do + struct(Google.Protobuf.Value, %{kind: {:null_value, :NULL_VALUE}}) + end + + defp from_map_value(value) when is_number(value) do + struct(Google.Protobuf.Value, %{kind: {:number_value, value}}) + end + + defp from_map_value(value) when is_binary(value) do + struct(Google.Protobuf.Value, %{kind: {:string_value, value}}) + end + + defp from_map_value(value) when is_boolean(value) do + struct(Google.Protobuf.Value, %{kind: {:bool_value, value}}) + end + + defp from_map_value(value) when is_map(value) do + struct(Google.Protobuf.Value, %{kind: {:struct_value, from_map(value)}}) + end + + defp from_map_value(value) when is_list(value) do + struct(Google.Protobuf.Value, %{ + kind: + {:list_value, + struct(Google.Protobuf.ListValue, %{ + values: Enum.map(value, &from_map_value/1) + })} + }) + end +end diff --git a/priv/templates/timestamp.pb.ex b/priv/templates/timestamp.pb.ex new file mode 100644 index 0000000..5887054 --- /dev/null +++ b/priv/templates/timestamp.pb.ex @@ -0,0 +1,45 @@ +defmodule Google.Protobuf.Timestamp do + @moduledoc false + + @doc """ + Converts a `DateTime` struct to a `Google.Protobuf.Timestamp` struct. + + Note: Elixir `DateTime.from_unix!/2` will convert units to + microseconds internally. Nanosecond precision is not guaranteed. + See examples for details. + + ## Examples + + iex> Timestamp.to_datetime(%Timestamp{seconds: 5, nanos: 0}) + ~U[1970-01-01 00:00:05.000000Z] + + iex> one = Timestamp.to_datetime(%Timestamp{seconds: 10, nanos: 100}) + ...> two = Timestamp.to_datetime(%Timestamp{seconds: 10, nanos: 105}) + ...> DateTime.diff(one, two, :nanosecond) + 0 + + """ + @spec to_datetime(__MODULE__.t()) :: DateTime.t() + def to_datetime(%{seconds: seconds, nanos: nanos}) do + DateTime.from_unix!(seconds * 1_000_000_000 + nanos, :nanosecond) + end + + @doc """ + Converts a `Google.Protobuf.Timestamp` struct to a `DateTime` struct. + + ## Examples + + iex> Timestamp.from_datetime(~U[1970-01-01 00:00:05.000000Z]) + %Timestamp{seconds: 5, nanos: 0} + + """ + @spec from_datetime(DateTime.t()) :: __MODULE__.t() + def from_datetime(%DateTime{} = datetime) do + nanoseconds = DateTime.to_unix(datetime, :nanosecond) + + struct(__MODULE__, %{ + seconds: div(nanoseconds, 1_000_000_000), + nanos: rem(nanoseconds, 1_000_000_000) + }) + end +end diff --git a/test/google_protos/duration_test.exs b/test/google_protos/duration_test.exs new file mode 100644 index 0000000..906983e --- /dev/null +++ b/test/google_protos/duration_test.exs @@ -0,0 +1,37 @@ +defmodule Google.Protobuf.DurationTest do + use ExUnit.Case + + alias Google.Protobuf.Duration + + describe "to_time_unit/1" do + test "converts to total seconds if no nanoseconds specified" do + assert {4200, :second} == Duration.to_time_unit(%Duration{seconds: 4200}) + end + + test "converts to total nanoseconds if specified" do + assert {20_000_000_100, :nanosecond} == + Duration.to_time_unit(%Duration{seconds: 20, nanos: 100}) + end + end + + describe "from_time_unit/2" do + test "converts :second to duration" do + assert %Duration{seconds: 11} == Duration.from_time_unit(11, :second) + end + + test "converts :millisecond to duration" do + assert %Duration{seconds: 11, nanos: 111_000_000} == + Duration.from_time_unit(11111, :millisecond) + end + + test "converts :microsecond to duration" do + assert %Duration{seconds: 11, nanos: 111_111_000} == + Duration.from_time_unit(11_111_111, :microsecond) + end + + test "converts :nanosecond to duration" do + assert %Duration{seconds: 11, nanos: 111_111_111} == + Duration.from_time_unit(11_111_111_111, :nanosecond) + end + end +end diff --git a/test/google_protos/struct_test.exs b/test/google_protos/struct_test.exs new file mode 100644 index 0000000..c688f86 --- /dev/null +++ b/test/google_protos/struct_test.exs @@ -0,0 +1,69 @@ +defmodule Google.Protobuf.StructTest do + use ExUnit.Case + + alias Google.Protobuf.Struct + + @basic_json """ + { + "key_one": "value_one", + "key_two": 1234, + "key_three": null, + "key_four": true + } + """ + + @basic_elixir %{ + "key_one" => "value_one", + "key_two" => 1234, + "key_three" => nil, + "key_four" => true + } + + @advanced_json """ + { + "key_two": [1, 2, 3, null, true, "value"], + "key_three": { + "key_four": "value_four", + "key_five": { + "key_six": 99, + "key_seven": { + "key_eight": "value_eight" + } + } + } + } + """ + + @advanced_elixir %{ + "key_two" => [1, 2, 3, nil, true, "value"], + "key_three" => %{ + "key_four" => "value_four", + "key_five" => %{ + "key_six" => 99, + "key_seven" => %{ + "key_eight" => "value_eight" + } + } + } + } + + describe "to_map/1" do + test "converts basic json to map" do + assert @basic_elixir == Struct.to_map(Protobuf.JSON.decode!(@basic_json, Struct)) + end + + test "converts advanced json to map" do + assert @advanced_elixir == Struct.to_map(Protobuf.JSON.decode!(@advanced_json, Struct)) + end + end + + describe "from_map/1" do + test "converts basic elixir to struct" do + assert Protobuf.JSON.decode!(@basic_json, Struct) == Struct.from_map(@basic_elixir) + end + + test "converts advanced elixir to struct" do + assert Protobuf.JSON.decode!(@advanced_json, Struct) == Struct.from_map(@advanced_elixir) + end + end +end diff --git a/test/google_protos/timestamp_test.exs b/test/google_protos/timestamp_test.exs new file mode 100644 index 0000000..6e3ae84 --- /dev/null +++ b/test/google_protos/timestamp_test.exs @@ -0,0 +1,25 @@ +defmodule Google.Protobuf.TimestampTest do + use ExUnit.Case + + alias Google.Protobuf.Timestamp + + describe "to_datetime/1" do + test "converts to DateTime" do + assert ~U[1970-01-01 00:00:05.000000Z] == + Timestamp.to_datetime(%Timestamp{seconds: 5, nanos: 0}) + end + + test "nanosecond precision" do + one = Timestamp.to_datetime(%Timestamp{seconds: 10, nanos: 100}) + two = Timestamp.to_datetime(%Timestamp{seconds: 10, nanos: 105}) + assert 0 == DateTime.diff(one, two, :nanosecond) + end + end + + describe "from_datetime/1" do + test "converts from DateTime" do + assert %Timestamp{seconds: 5, nanos: 0} == + Timestamp.from_datetime(~U[1970-01-01 00:00:05.000000Z]) + end + end +end