-
Notifications
You must be signed in to change notification settings - Fork 142
/
message.ex
346 lines (286 loc) · 10.2 KB
/
message.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
defmodule Protobuf.Protoc.Generator.Message do
@moduledoc false
alias Google.Protobuf.{DescriptorProto, FieldDescriptorProto}
alias Protobuf.Protoc.Context
alias Protobuf.Protoc.Generator.Util
alias Protobuf.Protoc.Generator.Enum, as: EnumGenerator
require EEx
EEx.function_from_file(
:defp,
:message_template,
Path.expand("./templates/message.ex.eex", :code.priv_dir(:protobuf)),
[:assigns]
)
@spec generate_list(Context.t(), [Google.Protobuf.DescriptorProto.t()]) ::
{enums :: [{mod_name :: String.t(), contents :: String.t()}],
messages :: [{mod_name :: String.t(), contents :: String.t()}]}
def generate_list(%Context{} = ctx, descs) when is_list(descs) do
descs
|> Enum.map(fn desc -> generate(ctx, desc) end)
|> Enum.unzip()
end
@spec generate(Context.t(), Google.Protobuf.DescriptorProto.t()) :: {any(), any()}
def generate(%Context{} = ctx, %Google.Protobuf.DescriptorProto{} = desc) do
new_ns = ctx.namespace ++ [Macro.camelize(desc.name)]
msg_name = Util.mod_name(ctx, new_ns)
fields = get_fields(ctx, desc)
extensions = get_extensions(desc)
descriptor_fun_body =
if ctx.gen_descriptors? do
Util.descriptor_fun_body(desc)
else
nil
end
ctx = %Context{ctx | namespace: new_ns}
{nested_enums, nested_msgs} = Enum.unzip(gen_nested_msgs(ctx, desc))
msg =
{msg_name,
Util.format(
message_template(
module: msg_name,
use_options: msg_opts_str(ctx, desc.options),
oneofs: desc.oneof_decl,
fields: gen_fields(ctx.syntax, fields),
descriptor_fun_body: descriptor_fun_body,
transform_module: ctx.transform_module,
extensions: extensions,
module_doc?: ctx.include_docs?
)
)}
{gen_nested_enums(ctx, desc) ++ nested_enums, nested_msgs ++ [msg]}
end
defp gen_nested_msgs(ctx, desc) do
Enum.map(desc.nested_type, fn msg_desc -> generate(ctx, msg_desc) end)
end
defp gen_nested_enums(ctx, desc) do
Enum.map(desc.enum_type, fn enum_desc -> EnumGenerator.generate(ctx, enum_desc) end)
end
defp gen_fields(syntax, fields) do
Enum.map(fields, fn %{opts_str: opts_str} = f ->
field(syntax, f, opts_str)
end)
end
defp field(:proto3, %{proto3_optional: true, label: "optional"} = f, opts_str) do
":#{f[:name]}, #{f[:number]}, proto3_optional: true, type: #{f[:type]}#{opts_str}"
end
defp field(syntax, f, opts_str) do
label_str =
if syntax == :proto3 && f[:label] != "repeated", do: "", else: "#{f[:label]}: true, "
":#{f[:name]}, #{f[:number]}, #{label_str}type: #{f[:type]}#{opts_str}"
end
defp msg_opts_str(%{syntax: syntax}, opts) do
msg_options = opts
opts = %{
syntax: syntax,
map: msg_options && msg_options.map_entry,
deprecated: msg_options && msg_options.deprecated,
protoc_gen_elixir_version: "\"#{Util.version()}\""
}
str = Util.options_to_str(opts)
if String.length(str) > 0, do: ", " <> str, else: ""
end
defp get_fields(ctx, desc) do
oneofs = get_real_oneofs(desc.oneof_decl)
nested_maps = nested_maps(ctx, desc)
for field <- desc.field, do: get_field(ctx, field, nested_maps, oneofs)
end
# Public and used by extensions.
@spec get_field(Context.t(), FieldDescriptorProto.t()) :: map()
def get_field(%Context{} = ctx, %FieldDescriptorProto{} = field) do
get_field(ctx, field, _nested_maps = %{}, _oneofs = [])
end
defp get_field(ctx, %FieldDescriptorProto{} = field_desc, nested_maps, oneofs) do
opts = field_options(field_desc, ctx.syntax)
# Check if the field is a map.
map = nested_maps[field_desc.type_name]
opts = if map, do: Keyword.put(opts, :map, true), else: opts
opts =
cond do
field_desc.oneof_index == nil -> opts
oneofs == [] or field_desc.proto3_optional -> opts
true -> Keyword.put(opts, :oneof, field_desc.oneof_index)
end
opts_str =
opts
|> sort_field_opts_to_reduce_changes()
|> Enum.map_join(", ", fn {key, val} -> "#{key}: #{inspect(val)}" end)
opts_str = if opts_str == "", do: "", else: ", " <> opts_str
type = field_type_name(ctx, field_desc)
%{
name: field_desc.name,
number: field_desc.number,
label: label_name(field_desc.label),
type: type,
type_enum: field_desc.type,
opts: Map.new(opts),
opts_str: opts_str,
map: map,
oneof: field_desc.oneof_index,
proto3_optional: field_desc.proto3_optional || false
}
end
defp get_real_oneofs(oneof_decl) do
Enum.flat_map(oneof_decl, fn oneof ->
if String.starts_with?(oneof.name, "_"), do: [], else: [oneof.name]
end)
end
# To avoid unnecessarily changing the files that users of this library generated with previous
# versions, we try to guarantee an order of field options in the generated files.
ordered_opts = [
:json_name,
:optional,
:repeated,
:map,
:type,
:default,
:enum,
:oneof,
:packed,
:deprecated
]
weights = Map.new(Enum.with_index(ordered_opts))
defp sort_field_opts_to_reduce_changes(opts) do
Enum.sort_by(opts, fn {key, _val} -> Map.fetch!(unquote(Macro.escape(weights)), key) end)
end
defp get_extensions(desc) do
max = Protobuf.Extension.max()
Enum.map(desc.extension_range, fn %DescriptorProto.ExtensionRange{start: start, end: end_} ->
if end_ == max do
{Integer.to_string(start), "Protobuf.Extension.max()"}
else
{Integer.to_string(start), Integer.to_string(end_)}
end
end)
end
defp field_type_name(ctx, %FieldDescriptorProto{type_name: type_name} = field_desc) do
case from_enum(field_desc.type) do
type when type in [:enum, :message] and not is_nil(type_name) ->
Util.type_from_type_name(ctx, type_name)
type ->
inspect(type)
end
end
# Map of protobuf are actually nested(one level) messages
defp nested_maps(ctx, desc) do
fully_qualified_name =
([ctx.package | ctx.namespace] ++ [desc.name])
|> Enum.reject(&is_nil/1)
|> Enum.join(".")
prefix = "." <> fully_qualified_name
Enum.reduce(desc.nested_type, %{}, fn desc, acc ->
if desc.options && desc.options.map_entry do
[k, v] = Enum.sort(desc.field, &(&1.number < &2.number))
pair = {{k.type, field_type_name(ctx, k)}, {v.type, field_type_name(ctx, v)}}
Map.put(acc, "#{prefix}.#{desc.name}", pair)
else
acc
end
end)
end
defp field_options(field_desc, syntax) do
(_starting_opts = [])
|> add_default_value_to_opts(field_desc)
|> add_enum_to_opts(field_desc)
|> add_json_name_to_opts(syntax, field_desc)
|> add_field_opts_if_present(field_desc)
end
defp add_field_opts_if_present(opts, %FieldDescriptorProto{options: field_opts}) do
opts =
if field_opts && is_boolean(field_opts.packed) do
Keyword.put(opts, :packed, field_opts.packed)
else
opts
end
opts =
if field_opts && is_boolean(field_opts.deprecated) do
Keyword.put(opts, :deprecated, field_opts.deprecated)
else
opts
end
opts
end
defp add_enum_to_opts(opts, %FieldDescriptorProto{type: type}) do
if type == :TYPE_ENUM do
Keyword.put(opts, :enum, true)
else
opts
end
end
defp label_name(:LABEL_OPTIONAL), do: "optional"
defp label_name(:LABEL_REQUIRED), do: "required"
defp label_name(:LABEL_REPEATED), do: "repeated"
defp add_default_value_to_opts(opts, %FieldDescriptorProto{
default_value: default_value
})
when default_value in [nil, ""] do
opts
end
defp add_default_value_to_opts(opts, %FieldDescriptorProto{
default_value: default_value,
type: type
}) do
value = cast_default_value(type, default_value)
if is_nil(value) do
opts
else
Keyword.put(opts, :default, value)
end
end
int_types = [
:TYPE_INT64,
:TYPE_UINT64,
:TYPE_INT32,
:TYPE_FIXED64,
:TYPE_FIXED32,
:TYPE_UINT32,
:TYPE_SFIXED32,
:TYPE_SFIXED64,
:TYPE_SINT32,
:TYPE_SINT64
]
float_types = [:TYPE_DOUBLE, :TYPE_FLOAT]
defp cast_default_value(:TYPE_BOOL, "true"), do: true
defp cast_default_value(:TYPE_BOOL, "false"), do: false
defp cast_default_value(:TYPE_ENUM, val), do: String.to_atom(val)
defp cast_default_value(type, val) when type in [:TYPE_STRING, :TYPE_BYTES], do: val
defp cast_default_value(type, val) when type in unquote(int_types), do: int_default(val)
defp cast_default_value(type, val) when type in unquote(float_types), do: float_default(val)
defp float_default(value) do
# A float can also be "inf", "NaN", and so on.
case Float.parse(value) do
{float, ""} -> float
:error -> value
{_float, _rest} -> raise "unparseable float/double default: #{inspect(value)}"
end
end
defp int_default(value) do
case Integer.parse(value) do
{int, ""} -> int
_other -> raise "unparseable number default: #{inspect(value)}"
end
end
# Omit `json_name` from the options list when it matches the original field
# name to keep the list small. Only Proto3 has JSON support for now.
defp add_json_name_to_opts(opts, :proto3, %{name: name, json_name: name}), do: opts
defp add_json_name_to_opts(opts, :proto3, %{json_name: json_name}),
do: Keyword.put(opts, :json_name, json_name)
defp add_json_name_to_opts(opts, _syntax, _props), do: opts
defp from_enum(:TYPE_DOUBLE), do: :double
defp from_enum(:TYPE_FLOAT), do: :float
defp from_enum(:TYPE_INT64), do: :int64
defp from_enum(:TYPE_UINT64), do: :uint64
defp from_enum(:TYPE_INT32), do: :int32
defp from_enum(:TYPE_FIXED64), do: :fixed64
defp from_enum(:TYPE_FIXED32), do: :fixed32
defp from_enum(:TYPE_BOOL), do: :bool
defp from_enum(:TYPE_STRING), do: :string
defp from_enum(:TYPE_GROUP), do: :group
defp from_enum(:TYPE_MESSAGE), do: :message
defp from_enum(:TYPE_BYTES), do: :bytes
defp from_enum(:TYPE_UINT32), do: :uint32
defp from_enum(:TYPE_ENUM), do: :enum
defp from_enum(:TYPE_SFIXED32), do: :sfixed32
defp from_enum(:TYPE_SFIXED64), do: :sfixed64
defp from_enum(:TYPE_SINT32), do: :sint32
defp from_enum(:TYPE_SINT64), do: :sint64
end