Skip to content

Commit 4656a52

Browse files
author
Ulisses Almeida
committed
Add Encodable/Decodable protocols
1 parent 242be79 commit 4656a52

File tree

19 files changed

+357
-26
lines changed

19 files changed

+357
-26
lines changed

lib/elixirpb.pb.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,25 @@ defmodule Elixirpb.FileOptions do
1010
field :module_prefix, 1, optional: true, type: :string
1111
end
1212

13+
defmodule Elixirpb.MessageOptions do
14+
@moduledoc false
15+
use Protobuf, syntax: :proto2
16+
17+
@type t :: %__MODULE__{
18+
typespec: String.t()
19+
}
20+
defstruct [:typespec]
21+
22+
field :typespec, 1, optional: true, type: :string
23+
end
24+
1325
defmodule Elixirpb.PbExtension do
1426
@moduledoc false
1527
use Protobuf, syntax: :proto2
1628

1729
extend Google.Protobuf.FileOptions, :file, 1047, optional: true, type: Elixirpb.FileOptions
30+
31+
extend Google.Protobuf.MessageOptions, :message, 1047,
32+
optional: true,
33+
type: Elixirpb.MessageOptions
1834
end

lib/protobuf/builder.ex

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ defmodule Protobuf.Builder do
7171
v =
7272
if f_props.embedded? do
7373
if f_props.repeated? do
74-
Enum.map(v, fn i -> f_props.type.new(i) end)
74+
Enum.map(v, &protobuf_or_term(&1, f_props.type))
7575
else
76-
f_props.type.new(v)
76+
protobuf_or_term(v, f_props.type)
7777
end
7878
else
7979
v
@@ -86,4 +86,9 @@ defmodule Protobuf.Builder do
8686
end
8787
end)
8888
end
89+
90+
defp protobuf_or_term(value, type),
91+
do: if(encodable?(value), do: value, else: type.new(value))
92+
93+
defp encodable?(v), do: Protobuf.Encodable.impl_for(v) != Protobuf.Encodable.Any
8994
end

lib/protobuf/decodable.ex

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defprotocol Protobuf.Decodable do
2+
@moduledoc """
3+
Defines the contract for transformations after decode a message.
4+
5+
Implementing this protocol is useful to translate protobuf structs to Elixir
6+
terms.
7+
8+
## Examples
9+
10+
defimpl Protobuf.Decodable, for: MyApp.Protobuf.Date do
11+
def to_elixir(%MyApp.Protobuf.Date{year: year, month: month, day: day}) do
12+
{:ok, date} = Date.new(year, month, day)
13+
date
14+
end
15+
end
16+
17+
# later in a decoded message
18+
proto_message.birthday
19+
~D[1988-10-29]
20+
"""
21+
@fallback_to_any true
22+
23+
@doc """
24+
This function will be called after decode the protobuf message binary. The
25+
returning value will be used in place of current `term` struct.
26+
"""
27+
@spec to_elixir(t) :: any
28+
def to_elixir(term)
29+
end
30+
31+
defimpl Protobuf.Decodable, for: Any do
32+
def to_elixir(term), do: term
33+
end

lib/protobuf/decoder.ex

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ defmodule Protobuf.Decoder do
22
@moduledoc false
33
import Protobuf.WireTypes
44
import Bitwise, only: [bsl: 2, bsr: 2, band: 2]
5+
6+
alias Protobuf.Decodable
7+
58
require Logger
69

710
@max_bits 64
@@ -14,7 +17,10 @@ defmodule Protobuf.Decoder do
1417
kvs = raw_decode_key(data, [])
1518
%{repeated_fields: repeated_fields} = msg_props = module.__message_props__()
1619
struct = build_struct(kvs, msg_props, module.new())
17-
reverse_repeated(struct, repeated_fields)
20+
21+
struct
22+
|> Decodable.to_elixir()
23+
|> reverse_repeated(repeated_fields)
1824
end
1925

2026
@doc false

lib/protobuf/encodable.ex

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
defprotocol Protobuf.Encodable do
2+
@moduledoc """
3+
Defines the contract for Elixir terms transformations before encode a message.
4+
5+
Implementing this protocol is useful to translate Elixir terms to protobuf
6+
structs, works in combination with `Protobuf.Decodable`.
7+
8+
## Examples
9+
10+
defimpl Protobuf.Encodable, for: Date do
11+
def to_protobuf(%Date{year: year, month: month, day: day}, MyApp.Protobuf.Date) do
12+
MyApp.Protobuf.Date.new(year: year, month: month, day: day)
13+
end
14+
end
15+
16+
# later, you can use Elixir terms in your fields and those will be
17+
# converted to protobuf structs before binary encoding
18+
%{protobuf_message | birthday: ~D[1988-10-29]}
19+
20+
"""
21+
@fallback_to_any true
22+
23+
@doc """
24+
This function will invoked before encode a term and only if encoding target is
25+
a protobuf message. The returning value will be used in place of current
26+
Elixir `term` struct.
27+
"""
28+
@spec to_protobuf(t, module) :: struct
29+
def to_protobuf(term, target_protobuf_module)
30+
end
31+
32+
defimpl Protobuf.Encodable, for: Any do
33+
def to_protobuf(term, _target_protobuf_module), do: term
34+
end

lib/protobuf/encoder.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Protobuf.Encoder do
33
import Protobuf.WireTypes
44
import Bitwise, only: [bsr: 2, band: 2, bsl: 2, bor: 2]
55

6-
alias Protobuf.{MessageProps, FieldProps}
6+
alias Protobuf.{Encodable, MessageProps, FieldProps}
77

88
@spec encode(atom, map | struct, keyword) :: iodata
99
def encode(mod, msg, opts) do
@@ -111,7 +111,9 @@ defmodule Protobuf.Encoder do
111111
) do
112112
repeated = is_repeated || is_map
113113

114-
repeated_or_not(val, repeated, fn v ->
114+
val
115+
|> Encodable.to_protobuf(type)
116+
|> repeated_or_not(repeated, fn v ->
115117
v = if is_map, do: struct(prop.type, %{key: elem(v, 0), value: elem(v, 1)}), else: v
116118
# so that oneof {:atom, v} can be encoded
117119
encoded = encode(type, v, [])

lib/protobuf/protoc/cli.ex

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,26 +105,39 @@ defmodule Protobuf.Protoc.CLI do
105105
new_ctx = append_ns(ctx, name)
106106

107107
types
108-
|> update_types(ctx, name)
108+
|> update_types(ctx, desc)
109109
|> find_types_in_proto(new_ctx, desc.enum_type)
110110
|> find_types_in_proto(new_ctx, desc.nested_type)
111111
end
112112

113-
defp find_types_in_proto(types, ctx, %Google.Protobuf.EnumDescriptorProto{name: name}) do
114-
update_types(types, ctx, name)
113+
defp find_types_in_proto(types, ctx, desc) do
114+
update_types(types, ctx, desc)
115115
end
116116

117117
defp append_ns(%{namespace: ns} = ctx, name) do
118118
new_ns = ns ++ [name]
119119
Map.put(ctx, :namespace, new_ns)
120120
end
121121

122-
defp update_types(types, %{namespace: ns, package: pkg, module_prefix: prefix}, name) do
123-
type_name =
124-
join_names(prefix || pkg, ns, name)
125-
|> Protobuf.Protoc.Generator.Util.normalize_type_name()
122+
defp update_types(types, %{namespace: ns, package: pkg, module_prefix: prefix}, desc) do
123+
name = desc.name
124+
module_name = gen_module_name(prefix, pkg, ns, name)
126125

127-
Map.put(types, "." <> join_names(pkg, ns, name), %{type_name: type_name})
126+
typespec =
127+
desc.options
128+
|> get_msg_options()
129+
|> Map.get(:typespec)
130+
131+
Map.put(types, "." <> join_names(pkg, ns, name), %{
132+
type_name: module_name,
133+
typespec: typespec
134+
})
135+
end
136+
137+
defp gen_module_name(prefix, pkg, ns, name) do
138+
(prefix || pkg)
139+
|> join_names(ns, name)
140+
|> Protobuf.Protoc.Generator.Util.normalize_type_name()
128141
end
129142

130143
defp join_names(pkg, ns, name) do
@@ -134,4 +147,16 @@ defmodule Protobuf.Protoc.CLI do
134147
|> Enum.filter(&(&1 && &1 != ""))
135148
|> Enum.join(".")
136149
end
150+
151+
defp get_msg_options(nil), do: %{}
152+
153+
defp get_msg_options(options) do
154+
case Google.Protobuf.MessageOptions.get_extension(options, Elixirpb.PbExtension, :message) do
155+
nil ->
156+
%{}
157+
158+
opts ->
159+
opts
160+
end
161+
end
137162
end

lib/protobuf/protoc/generator/message.ex

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,21 @@ defmodule Protobuf.Protoc.Generator.Message do
152152
"%{#{k_type} => #{v_type}}"
153153
end
154154

155-
defp fmt_type(%{label: "repeated", type_enum: type_enum, type: type}) do
156-
"[#{type_to_spec(type_enum, type, true)}]"
155+
defp fmt_type(%{label: "repeated", type_enum: type_enum, type: type, typespec: typespec}) do
156+
"[#{typespec || type_to_spec(type_enum, type, true)}]"
157157
end
158158

159-
defp fmt_type(%{type_enum: type_enum, type: type}) do
160-
"#{type_to_spec(type_enum, type)}"
159+
defp fmt_type(%{type_enum: type_enum, type: type, typespec: typespec}) do
160+
cond do
161+
type_enum == :TYPE_MESSAGE and typespec ->
162+
typespec <> " | nil"
163+
164+
typespec ->
165+
typespec
166+
167+
true ->
168+
"#{type_to_spec(type_enum, type)}"
169+
end
161170
end
162171

163172
defp type_to_spec(enum, type, repeated \\ false)
@@ -188,12 +197,14 @@ defmodule Protobuf.Protoc.Generator.Message do
188197
opts_str = if opts_str == "", do: "", else: ", " <> opts_str
189198

190199
type = field_type_name(ctx, f)
200+
typespec = field_typespec(ctx, f)
191201

192202
%{
193203
name: f.name,
194204
number: f.number,
195205
label: label_name(f.label),
196206
type: type,
207+
typespec: typespec,
197208
type_enum: f.type,
198209
opts: opts,
199210
opts_str: opts_str,
@@ -212,12 +223,17 @@ defmodule Protobuf.Protoc.Generator.Message do
212223
type = TypeUtil.from_enum(f.type)
213224

214225
if f.type_name && (type == :enum || type == :message) do
215-
Util.type_from_type_name(ctx, f.type_name)
226+
Util.get_metadata_from_type_name(ctx, f.type_name)[:type_name]
216227
else
217228
":#{type}"
218229
end
219230
end
220231

232+
defp field_typespec(_ctx, %{type_name: nil} = _field), do: nil
233+
234+
defp field_typespec(ctx, %{type_name: type_name} = _field),
235+
do: Util.get_metadata_from_type_name(ctx, type_name)[:typespec]
236+
221237
# Map of protobuf are actually nested(one level) messages
222238
defp nested_maps(ctx, desc) do
223239
full_name = Util.join_name([ctx.package | ctx.namespace] ++ [desc.name])

lib/protobuf/protoc/generator/util.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,20 @@ defmodule Protobuf.Protoc.Generator.Util do
3131
|> Enum.join(", ")
3232
end
3333

34-
def type_from_type_name(ctx, type_name) do
34+
def module_from_type_name(ctx, type_name),
35+
do: get_metadata_from_type_name(ctx, type_name)[:module_name]
36+
37+
def type_from_type_name(ctx, type_name),
38+
do: get_metadata_from_type_name(ctx, type_name)[:type_name]
39+
40+
def get_metadata_from_type_name(ctx, type_name) do
3541
# The doc says there's a situation where type_name begins without a `.`, but I never got that.
3642
# Handle that later.
3743
metadata =
3844
ctx.dep_type_mapping[type_name] ||
3945
raise "There's something wrong to get #{type_name}'s type, please contact with the lib author."
4046

41-
metadata[:type_name]
47+
metadata
4248
end
4349

4450
def normalize_type_name(name) do

src/elixirpb.proto

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,35 @@ message FileOptions {
2525
optional string module_prefix = 1;
2626
}
2727

28+
// Message level options
29+
//
30+
// For example,
31+
// option (elixirpb.message).typespec = "Date.t";
32+
message MessageOptions {
33+
// Specify a typespec that will used when a message reference this as field.
34+
// For example, let's say you have in your message:
35+
//
36+
// package MyApp.Protobuf;
37+
//
38+
// message Date {
39+
// option (elixirpb.message).typespec = "Date.t";
40+
// int iso_days = 1;
41+
// }
42+
//
43+
// message User {
44+
// Date birthday = 1;
45+
// }
46+
//
47+
// Then in `MyApp.Protobuf.User`, the type notation will for `birthday` will
48+
// be `Date.t()` and not `MyApp.Protobuf.Date.t()`. This is useulf when
49+
// combined with `Protobuf.Encodable` and `Protobuf.Decodable` mechanism.
50+
optional string typespec = 1;
51+
}
52+
2853
extend google.protobuf.FileOptions {
2954
optional FileOptions file = 1047;
3055
}
56+
57+
extend google.protobuf.MessageOptions {
58+
optional MessageOptions message = 1047;
59+
}

0 commit comments

Comments
 (0)