Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helper functions #21

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"],
inputs: ["mix.exs", "{config,lib,priv,test}/**/*.{ex,exs}"],
import_deps: [:protobuf]
]
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 19 additions & 1 deletion generate_google_protos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion lib/google_protos/any.pb.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down
64 changes: 63 additions & 1 deletion lib/google_protos/duration.pb.ex
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion lib/google_protos/empty.pb.ex
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion lib/google_protos/field_mask.pb.ex
Original file line number Diff line number Diff line change
@@ -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
91 changes: 86 additions & 5 deletions lib/google_protos/struct.pb.ex
Original file line number Diff line number Diff line change
@@ -1,28 +1,108 @@
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
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

Expand All @@ -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
45 changes: 44 additions & 1 deletion lib/google_protos/timestamp.pb.ex
Original file line number Diff line number Diff line change
@@ -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
Loading