From 253e2aa289146a5f01d82ca5e5dbd5882a276cbc Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Sat, 17 Jun 2023 14:39:14 -0600 Subject: [PATCH 1/3] feat: add helper functions for Google.Protobuf modules --- lib/google/protobuf.ex | 187 ++++++++++++++++++++++++++++++++++ test/google/protobuf_test.exs | 138 +++++++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 lib/google/protobuf.ex create mode 100644 test/google/protobuf_test.exs diff --git a/lib/google/protobuf.ex b/lib/google/protobuf.ex new file mode 100644 index 00000000..6a9e1b78 --- /dev/null +++ b/lib/google/protobuf.ex @@ -0,0 +1,187 @@ +defmodule Google.Protobuf do + @moduledoc """ + Utility functions for working with Google Protobuf structs easier + in Elixir. + """ + + @doc """ + Converts a `Google.Protobuf.Duration` struct to a value and `System.time_unit()`. + + ## Examples + + iex> to_time_unit(%Google.Protobuf.Duration{seconds: 10}) + {10, :second} + + iex> to_time_unit(%Google.Protobuf.Duration{seconds: 20, nanos: 100}) + {20_000_000_100, :nanosecond} + + """ + @spec to_time_unit(Google.Protobuf.Duration.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` struct. + + ## Examples + + iex> from_time_unit(420, :second) + %Google.Protobuf.Duration{seconds: 420} + + iex> from_time_unit(11_111_111, :microsecond) + %Google.Protobuf.Duration{seconds: 11, nanos: 111_111_000} + + """ + @spec from_time_unit(integer(), System.time_unit()) :: Google.Protobuf.Duration.t() + def from_time_unit(seconds, :second) do + struct(Google.Protobuf.Duration, %{ + seconds: seconds + }) + end + + def from_time_unit(millisecond, :millisecond) do + struct(Google.Protobuf.Duration, %{ + seconds: div(millisecond, 1_000), + nanos: rem(millisecond, 1_000) * 1_000_000 + }) + end + + def from_time_unit(millisecond, :microsecond) do + struct(Google.Protobuf.Duration, %{ + seconds: div(millisecond, 1_000_000), + nanos: rem(millisecond, 1_000_000) * 1_000 + }) + end + + def from_time_unit(millisecond, :nanosecond) do + struct(Google.Protobuf.Duration, %{ + seconds: div(millisecond, 1_000_000_000), + nanos: rem(millisecond, 1_000_000_000) + }) + end + + @doc """ + Converts a `Google.Protobuf.Struct` struct to a `map()` recursively + converting values to their Elixir equivalents. + + ## Examples + + iex> to_map(%Google.Protobuf.Struct{}) + %{} + + """ + @spec to_map(Google.Protobuf.Struct.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` struct recursively + wrapping values in their `Google.Protobuf.Value` equivalents. + + ## Examples + + iex> from_map(%{}) + %Google.Protobuf.Struct{} + + """ + @spec from_map(map()) :: Google.Protobuf.Struct.t() + def from_map(map) do + struct(Google.Protobuf.Struct, %{ + 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 + + @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> to_datetime(%Google.Protobuf.Timestamp{seconds: 5, nanos: 0}) + ~U[1970-01-01 00:00:05.000000Z] + + iex> one = to_datetime(%Google.Protobuf.Timestamp{seconds: 10, nanos: 100}) + ...> two = to_datetime(%Google.Protobuf.Timestamp{seconds: 10, nanos: 105}) + ...> DateTime.diff(one, two, :nanosecond) + 0 + + """ + @spec to_datetime(Google.Protobuf.Timestamp.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> from_datetime(~U[1970-01-01 00:00:05.000000Z]) + %Google.Protobuf.Timestamp{seconds: 5, nanos: 0} + + """ + @spec from_datetime(DateTime.t()) :: Google.Protobuf.Timestamp.t() + def from_datetime(%DateTime{} = datetime) do + nanoseconds = DateTime.to_unix(datetime, :nanosecond) + + struct(Google.Protobuf.Timestamp, %{ + seconds: div(nanoseconds, 1_000_000_000), + nanos: rem(nanoseconds, 1_000_000_000) + }) + end +end diff --git a/test/google/protobuf_test.exs b/test/google/protobuf_test.exs new file mode 100644 index 00000000..81e2acf9 --- /dev/null +++ b/test/google/protobuf_test.exs @@ -0,0 +1,138 @@ +defmodule Google.ProtobufTest do + use ExUnit.Case, async: true + + import Google.Protobuf + + alias Google.Protobuf.{Duration, Struct, Timestamp} + + @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_time_unit/1" do + test "converts nil values to 0 seconds" do + assert {0, :second} == to_time_unit(%Duration{}) + end + + test "converts to total seconds if no nanoseconds specified" do + assert {4200, :second} == to_time_unit(%Duration{seconds: 4200}) + assert {-1234, :second} == to_time_unit(%Duration{seconds: -1234}) + end + + test "converts to total nanoseconds if specified" do + assert {20_000_000_100, :nanosecond} == + 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} == from_time_unit(11, :second) + end + + test "converts :millisecond to duration" do + assert %Duration{seconds: 11, nanos: 111_000_000} == + from_time_unit(11111, :millisecond) + end + + test "converts :microsecond to duration" do + assert %Duration{seconds: 11, nanos: 111_111_000} == + from_time_unit(11_111_111, :microsecond) + end + + test "converts :nanosecond to duration" do + assert %Duration{seconds: -14, nanos: -111_423_724} == + from_time_unit(-14_111_423_724, :nanosecond) + end + end + + describe "to_map/1" do + test "converts nil values to empty map" do + assert %{} == to_map(%Struct{}) + end + + test "converts basic json to map" do + assert @basic_elixir == to_map(Protobuf.JSON.decode!(@basic_json, Struct)) + end + + test "converts advanced json to map" do + assert @advanced_elixir == 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) == from_map(@basic_elixir) + end + + test "converts advanced elixir to struct" do + assert Protobuf.JSON.decode!(@advanced_json, Struct) == from_map(@advanced_elixir) + end + end + + describe "to_datetime/1" do + # This matches golang behaviour + # https://github.com/golang/protobuf/blob/5d5e8c018a13017f9d5b8bf4fad64aaa42a87308/ptypes/timestamp.go#L43 + test "converts nil values to unix time start" do + assert ~U[1970-01-01 00:00:00.000000Z] == to_datetime(%Timestamp{}) + end + + test "converts to DateTime" do + assert ~U[1970-01-01 00:00:05.000000Z] == + to_datetime(%Timestamp{seconds: 5, nanos: 0}) + end + + test "nanosecond precision" do + one = to_datetime(%Timestamp{seconds: 10, nanos: 100}) + two = 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} == + from_datetime(~U[1970-01-01 00:00:05.000000Z]) + end + end +end From f5d8f1586b962c61f481f97f76d5eb1e4852abcb Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Sat, 17 Jun 2023 14:43:37 -0600 Subject: [PATCH 2/3] update documentation --- lib/google/protobuf.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/google/protobuf.ex b/lib/google/protobuf.ex index 6a9e1b78..eb26c0a4 100644 --- a/lib/google/protobuf.ex +++ b/lib/google/protobuf.ex @@ -1,7 +1,6 @@ defmodule Google.Protobuf do @moduledoc """ - Utility functions for working with Google Protobuf structs easier - in Elixir. + Utility functions for working with Google Protobuf structs. """ @doc """ From bf840001b58a5ad47896242f049284a7625a62a3 Mon Sep 17 00:00:00 2001 From: v0idpwn Date: Thu, 7 Nov 2024 17:51:03 -0300 Subject: [PATCH 3/3] Address review comments - Remove `to_time_unit/1` and `from_time_unit/2` - Fix type reference in documentation - Add better example --- lib/google/protobuf.ex | 77 +++++++---------------------------- test/google/protobuf_test.exs | 39 +----------------- 2 files changed, 15 insertions(+), 101 deletions(-) diff --git a/lib/google/protobuf.ex b/lib/google/protobuf.ex index eb26c0a4..6e1812b2 100644 --- a/lib/google/protobuf.ex +++ b/lib/google/protobuf.ex @@ -4,68 +4,7 @@ defmodule Google.Protobuf do """ @doc """ - Converts a `Google.Protobuf.Duration` struct to a value and `System.time_unit()`. - - ## Examples - - iex> to_time_unit(%Google.Protobuf.Duration{seconds: 10}) - {10, :second} - - iex> to_time_unit(%Google.Protobuf.Duration{seconds: 20, nanos: 100}) - {20_000_000_100, :nanosecond} - - """ - @spec to_time_unit(Google.Protobuf.Duration.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` struct. - - ## Examples - - iex> from_time_unit(420, :second) - %Google.Protobuf.Duration{seconds: 420} - - iex> from_time_unit(11_111_111, :microsecond) - %Google.Protobuf.Duration{seconds: 11, nanos: 111_111_000} - - """ - @spec from_time_unit(integer(), System.time_unit()) :: Google.Protobuf.Duration.t() - def from_time_unit(seconds, :second) do - struct(Google.Protobuf.Duration, %{ - seconds: seconds - }) - end - - def from_time_unit(millisecond, :millisecond) do - struct(Google.Protobuf.Duration, %{ - seconds: div(millisecond, 1_000), - nanos: rem(millisecond, 1_000) * 1_000_000 - }) - end - - def from_time_unit(millisecond, :microsecond) do - struct(Google.Protobuf.Duration, %{ - seconds: div(millisecond, 1_000_000), - nanos: rem(millisecond, 1_000_000) * 1_000 - }) - end - - def from_time_unit(millisecond, :nanosecond) do - struct(Google.Protobuf.Duration, %{ - seconds: div(millisecond, 1_000_000_000), - nanos: rem(millisecond, 1_000_000_000) - }) - end - - @doc """ - Converts a `Google.Protobuf.Struct` struct to a `map()` recursively + Converts a `Google.Protobuf.Struct` struct to a `t:map()` recursively converting values to their Elixir equivalents. ## Examples @@ -73,6 +12,18 @@ defmodule Google.Protobuf do iex> to_map(%Google.Protobuf.Struct{}) %{} + iex> to_map(%Google.Protobuf.Struct{ + fields: %{ + "key_one" => %Google.Protobuf.Value{ + kind: {:string_value, "value_one"}, + }, + "key_two" => %Google.Protobuf.Value{ + kind: {:number_value, 1234.0}, + } + }, + }) + %{"key_one" => "value_one", "key_two" => 1234.0} + """ @spec to_map(Google.Protobuf.Struct.t()) :: map() def to_map(struct) do @@ -93,7 +44,7 @@ defmodule Google.Protobuf do do: Enum.map(values, &to_map_value/1) @doc """ - Converts a `map()` to a `Google.Protobuf.Struct` struct recursively + Converts a `t:map()` to a `Google.Protobuf.Struct` struct recursively wrapping values in their `Google.Protobuf.Value` equivalents. ## Examples diff --git a/test/google/protobuf_test.exs b/test/google/protobuf_test.exs index 81e2acf9..427e87f6 100644 --- a/test/google/protobuf_test.exs +++ b/test/google/protobuf_test.exs @@ -3,7 +3,7 @@ defmodule Google.ProtobufTest do import Google.Protobuf - alias Google.Protobuf.{Duration, Struct, Timestamp} + alias Google.Protobuf.{Struct, Timestamp} @basic_json """ { @@ -49,43 +49,6 @@ defmodule Google.ProtobufTest do } } - describe "to_time_unit/1" do - test "converts nil values to 0 seconds" do - assert {0, :second} == to_time_unit(%Duration{}) - end - - test "converts to total seconds if no nanoseconds specified" do - assert {4200, :second} == to_time_unit(%Duration{seconds: 4200}) - assert {-1234, :second} == to_time_unit(%Duration{seconds: -1234}) - end - - test "converts to total nanoseconds if specified" do - assert {20_000_000_100, :nanosecond} == - 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} == from_time_unit(11, :second) - end - - test "converts :millisecond to duration" do - assert %Duration{seconds: 11, nanos: 111_000_000} == - from_time_unit(11111, :millisecond) - end - - test "converts :microsecond to duration" do - assert %Duration{seconds: 11, nanos: 111_111_000} == - from_time_unit(11_111_111, :microsecond) - end - - test "converts :nanosecond to duration" do - assert %Duration{seconds: -14, nanos: -111_423_724} == - from_time_unit(-14_111_423_724, :nanosecond) - end - end - describe "to_map/1" do test "converts nil values to empty map" do assert %{} == to_map(%Struct{})