Skip to content

Commit ab5daed

Browse files
authored
Fix multiple conflicting PbExtension modules (#332)
Closes #88.
1 parent 2f2efaf commit ab5daed

File tree

20 files changed

+442
-233
lines changed

20 files changed

+442
-233
lines changed

lib/elixirpb.pb.ex

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,3 @@ defmodule Elixirpb.FileOptions do
55

66
field :module_prefix, 1, optional: true, type: :string
77
end
8-
9-
defmodule Elixirpb.PbExtension do
10-
@moduledoc false
11-
use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto2
12-
13-
extend Google.Protobuf.FileOptions, :file, 1047, optional: true, type: Elixirpb.FileOptions
14-
end

lib/elixirpb/pb_extension.pb.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
defmodule Elixirpb.PbExtension do
2+
@moduledoc false
3+
use Protobuf, protoc_gen_elixir_version: "0.11.0"
4+
5+
extend Google.Protobuf.FileOptions, :file, 1047, optional: true, type: Elixirpb.FileOptions
6+
end

lib/google/protobuf/descriptor.pb.ex

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ defmodule Google.Protobuf.ExtensionRangeOptions do
154154

155155
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
156156

157-
extensions [{1000, 536_870_912}]
157+
extensions [{1000, Protobuf.Extension.max()}]
158158
end
159159

160160
defmodule Google.Protobuf.FieldDescriptorProto do
@@ -275,7 +275,7 @@ defmodule Google.Protobuf.FileOptions do
275275
field :ruby_package, 45, optional: true, type: :string
276276
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
277277

278-
extensions [{1000, 536_870_912}]
278+
extensions [{1000, Protobuf.Extension.max()}]
279279
end
280280

281281
defmodule Google.Protobuf.MessageOptions do
@@ -289,7 +289,7 @@ defmodule Google.Protobuf.MessageOptions do
289289
field :map_entry, 7, optional: true, type: :bool
290290
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
291291

292-
extensions [{1000, 536_870_912}]
292+
extensions [{1000, Protobuf.Extension.max()}]
293293
end
294294

295295
defmodule Google.Protobuf.FieldOptions do
@@ -317,7 +317,7 @@ defmodule Google.Protobuf.FieldOptions do
317317
field :weak, 10, optional: true, type: :bool, default: false
318318
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
319319

320-
extensions [{1000, 536_870_912}]
320+
extensions [{1000, Protobuf.Extension.max()}]
321321
end
322322

323323
defmodule Google.Protobuf.OneofOptions do
@@ -327,7 +327,7 @@ defmodule Google.Protobuf.OneofOptions do
327327

328328
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
329329

330-
extensions [{1000, 536_870_912}]
330+
extensions [{1000, Protobuf.Extension.max()}]
331331
end
332332

333333
defmodule Google.Protobuf.EnumOptions do
@@ -339,7 +339,7 @@ defmodule Google.Protobuf.EnumOptions do
339339
field :deprecated, 3, optional: true, type: :bool, default: false
340340
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
341341

342-
extensions [{1000, 536_870_912}]
342+
extensions [{1000, Protobuf.Extension.max()}]
343343
end
344344

345345
defmodule Google.Protobuf.EnumValueOptions do
@@ -350,7 +350,7 @@ defmodule Google.Protobuf.EnumValueOptions do
350350
field :deprecated, 1, optional: true, type: :bool, default: false
351351
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
352352

353-
extensions [{1000, 536_870_912}]
353+
extensions [{1000, Protobuf.Extension.max()}]
354354
end
355355

356356
defmodule Google.Protobuf.ServiceOptions do
@@ -361,7 +361,7 @@ defmodule Google.Protobuf.ServiceOptions do
361361
field :deprecated, 33, optional: true, type: :bool, default: false
362362
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
363363

364-
extensions [{1000, 536_870_912}]
364+
extensions [{1000, Protobuf.Extension.max()}]
365365
end
366366

367367
defmodule Google.Protobuf.MethodOptions do
@@ -379,7 +379,7 @@ defmodule Google.Protobuf.MethodOptions do
379379

380380
field :uninterpreted_option, 999, repeated: true, type: Google.Protobuf.UninterpretedOption
381381

382-
extensions [{1000, 536_870_912}]
382+
extensions [{1000, Protobuf.Extension.max()}]
383383
end
384384

385385
defmodule Google.Protobuf.UninterpretedOption.NamePart do

lib/protobuf.ex

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,25 @@ defmodule Protobuf do
282282
"""
283283
@spec load_extensions() :: :ok
284284
def load_extensions() do
285-
Protobuf.Extension.__cal_extensions__()
285+
for mod <- get_all_modules(),
286+
String.ends_with?(Atom.to_string(mod), ".PbExtension"),
287+
Code.ensure_loaded?(mod),
288+
function_exported?(mod, :__protobuf_info__, 1),
289+
%{extensions: extensions} = mod.__protobuf_info__(:extension_props) do
290+
Enum.each(extensions, fn {_, ext} ->
291+
fnum = ext.field_props.fnum
292+
fnum_key = {Protobuf.Extension, ext.extendee, fnum}
293+
:persistent_term.put(fnum_key, mod)
294+
end)
295+
end
296+
286297
:ok
287298
end
299+
300+
defp get_all_modules do
301+
Enum.flat_map(Application.loaded_applications(), fn {app, _desc, _vsn} ->
302+
{:ok, modules} = :application.get_key(app, :modules)
303+
modules
304+
end)
305+
end
288306
end

lib/protobuf/dsl.ex

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,50 @@ defmodule Protobuf.DSL do
2828

2929
@doc """
3030
Define extensions range in the message module to allow extensions for this module.
31+
32+
Extension ranges are defined as a list of tuples `{start, end}`, where each tuple is
33+
an **exclusive** range starting and `start` and ending at `end` (the equivalent
34+
of `start..end-1` in Elixir).
35+
36+
To simulate the Protobuf `max` keyword, you can use `Protobuf.Extension.max/0`.
37+
38+
## Examples
39+
40+
These Protobuf definition:
41+
42+
```protobuf
43+
message Foo {
44+
extensions 1, 10 to 20, 100 to max;
45+
}
46+
```
47+
48+
Would be translated in Elixir to:
49+
50+
extensions [{1, 2}, {10, 21}, {100, Protobuf.Extension.max()}]
51+
3152
"""
3253
defmacro extensions(ranges) do
3354
quote do
55+
ranges = unquote(ranges)
56+
57+
if not is_list(ranges) do
58+
raise ArgumentError, "expected a list of ranges, got: #{inspect(ranges)}"
59+
end
60+
61+
Enum.each(ranges, fn
62+
value when not is_tuple(value) or tuple_size(value) != 2 ->
63+
raise ArgumentError, "expected a range, got: #{inspect(value)}"
64+
65+
{left, right} when not is_integer(left) or not is_integer(right) ->
66+
raise ArgumentError, "expected an integer range, got: #{inspect({left, right})}"
67+
68+
{left, right} when left >= right ->
69+
raise ArgumentError, "expected an ordered range, got: #{inspect({left, right})}"
70+
71+
other ->
72+
:ok
73+
end)
74+
3475
@extensions unquote(ranges)
3576
end
3677
end

lib/protobuf/extension.ex

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,25 @@ defmodule Protobuf.Extension do
3535
3636
"""
3737

38+
import Bitwise, only: [<<<: 2]
39+
40+
# TODO: replace bitshift with Integer.pow/2 when we depend on Elixir 1.12+.
41+
# 2^29, see https://developers.google.com/protocol-buffers/docs/proto#extensions
42+
@max 1 <<< 29
43+
44+
@doc """
45+
Returns the maximum extension number.
46+
47+
## Examples
48+
49+
iex> Protobuf.Extension.max()
50+
#{inspect(@max)}
51+
52+
"""
53+
@doc since: "0.12.0"
54+
@spec max() :: pos_integer()
55+
def max, do: @max
56+
3857
@doc "The actual function for `put_extension`"
3958
@spec put(module, map, module, atom, any) :: map
4059
def put(mod, struct, extension_mod, field, value) do
@@ -108,26 +127,4 @@ defmodule Protobuf.Extension do
108127
end
109128
end
110129
end
111-
112-
@doc false
113-
def __cal_extensions__() do
114-
for mod <- get_all_modules(),
115-
String.ends_with?(Atom.to_string(mod), ".PbExtension"),
116-
Code.ensure_loaded?(mod),
117-
function_exported?(mod, :__protobuf_info__, 1),
118-
%{extensions: extensions} = mod.__protobuf_info__(:extension_props) do
119-
Enum.each(extensions, fn {_, ext} ->
120-
fnum = ext.field_props.fnum
121-
fnum_key = {Protobuf.Extension, ext.extendee, fnum}
122-
:persistent_term.put(fnum_key, mod)
123-
end)
124-
end
125-
end
126-
127-
defp get_all_modules do
128-
Enum.flat_map(Application.loaded_applications(), fn {app, _desc, _vsn} ->
129-
{:ok, modules} = :application.get_key(app, :modules)
130-
modules
131-
end)
132-
end
133130
end

lib/protobuf/message_props.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ defmodule Protobuf.MessageProps do
2222
enum?: boolean(),
2323
extendable?: boolean(),
2424
map?: boolean(),
25+
26+
# See Protobuf.DSL.extensions/1.
2527
extension_range: [{non_neg_integer(), non_neg_integer()}] | nil
2628
}
2729

lib/protobuf/protoc/cli.ex

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,33 @@ defmodule Protobuf.Protoc.CLI do
5151
|> parse_params(request.parameter || "")
5252
|> find_types(request.proto_file, request.file_to_generate)
5353

54-
files =
55-
Enum.flat_map(request.file_to_generate, fn file ->
54+
{files, package_level_extensions} =
55+
Enum.flat_map_reduce(request.file_to_generate, %{}, fn file, acc ->
5656
desc = Enum.find(request.proto_file, &(&1.name == file))
57-
Protobuf.Protoc.Generator.generate(ctx, desc)
57+
{package_level_extensions, files} = Protobuf.Protoc.Generator.generate(ctx, desc)
58+
59+
acc =
60+
case package_level_extensions do
61+
{mod_name, extensions} -> Map.update(acc, mod_name, extensions, &(&1 ++ extensions))
62+
nil -> acc
63+
end
64+
65+
{files, acc}
5866
end)
5967

68+
ext_files =
69+
for {mod_name, extensions} <- package_level_extensions do
70+
{mod_name, contents} =
71+
Protobuf.Protoc.Generator.Extension.generate_package_level(ctx, mod_name, extensions)
72+
73+
%Google.Protobuf.Compiler.CodeGeneratorResponse.File{
74+
name: Macro.underscore(mod_name) <> ".pb.ex",
75+
content: contents
76+
}
77+
end
78+
6079
%Google.Protobuf.Compiler.CodeGeneratorResponse{
61-
file: files,
80+
file: files ++ ext_files,
6281
supported_features: supported_features()
6382
}
6483
|> Protobuf.encode_to_iodata()

lib/protobuf/protoc/context.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ defmodule Protobuf.Protoc.Context do
6060
) do
6161
custom_file_opts =
6262
Google.Protobuf.FileOptions.get_extension(options, Elixirpb.PbExtension, :file) ||
63-
%Elixirpb.PbExtension{}
63+
%Elixirpb.FileOptions{}
6464

6565
%__MODULE__{
6666
ctx

lib/protobuf/protoc/generator.ex

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,32 @@ defmodule Protobuf.Protoc.Generator do
44
alias Protobuf.Protoc.Context
55
alias Protobuf.Protoc.Generator
66

7+
# TODO: improve spec
78
@spec generate(Context.t(), %Google.Protobuf.FileDescriptorProto{}) ::
8-
[Google.Protobuf.Compiler.CodeGeneratorResponse.File.t()]
9+
{term(), [Google.Protobuf.Compiler.CodeGeneratorResponse.File.t()]}
910
def generate(%Context{} = ctx, %Google.Protobuf.FileDescriptorProto{} = desc) do
10-
module_definitions =
11-
ctx
12-
|> generate_module_definitions(desc)
13-
|> Enum.reject(&is_nil/1)
14-
15-
if ctx.one_file_per_module? do
16-
Enum.map(module_definitions, fn {mod_name, content} ->
17-
file_name = Macro.underscore(mod_name) <> ".pb.ex"
18-
%Google.Protobuf.Compiler.CodeGeneratorResponse.File{name: file_name, content: content}
19-
end)
20-
else
21-
# desc.name is the filename, ending in ".proto".
22-
file_name = Path.rootname(desc.name) <> ".pb.ex"
23-
24-
content =
25-
module_definitions
26-
|> Enum.map(fn {_mod_name, contents} -> [contents, ?\n] end)
27-
|> IO.iodata_to_binary()
28-
|> Generator.Util.format()
29-
30-
[%Google.Protobuf.Compiler.CodeGeneratorResponse.File{name: file_name, content: content}]
31-
end
11+
{package_level_extensions, module_definitions} = generate_module_definitions(ctx, desc)
12+
13+
files =
14+
if ctx.one_file_per_module? do
15+
Enum.map(module_definitions, fn {mod_name, content} ->
16+
file_name = Macro.underscore(mod_name) <> ".pb.ex"
17+
%Google.Protobuf.Compiler.CodeGeneratorResponse.File{name: file_name, content: content}
18+
end)
19+
else
20+
# desc.name is the filename, ending in ".proto".
21+
file_name = Path.rootname(desc.name) <> ".pb.ex"
22+
23+
content =
24+
module_definitions
25+
|> Enum.map(fn {_mod_name, contents} -> [contents, ?\n] end)
26+
|> IO.iodata_to_binary()
27+
|> Generator.Util.format()
28+
29+
[%Google.Protobuf.Compiler.CodeGeneratorResponse.File{name: file_name, content: content}]
30+
end
31+
32+
{package_level_extensions, files}
3233
end
3334

3435
defp generate_module_definitions(ctx, %Google.Protobuf.FileDescriptorProto{} = desc) do
@@ -41,14 +42,12 @@ defmodule Protobuf.Protoc.Generator do
4142
}
4243
|> Protobuf.Protoc.Context.custom_file_options_from_file_desc(desc)
4344

44-
nested_extensions = Generator.Extension.get_nested_extensions(ctx, desc.message_type)
45-
4645
enum_defmodules = Enum.map(desc.enum_type, &Generator.Enum.generate(ctx, &1))
4746

4847
{nested_enum_defmodules, message_defmodules} =
4948
Generator.Message.generate_list(ctx, desc.message_type)
5049

51-
extension_defmodules = Generator.Extension.generate(ctx, desc, nested_extensions)
50+
{package_level_extensions, extension_defmodules} = Generator.Extension.generate(ctx, desc)
5251

5352
service_defmodules =
5453
if "grpc" in ctx.plugins do
@@ -57,13 +56,16 @@ defmodule Protobuf.Protoc.Generator do
5756
[]
5857
end
5958

60-
List.flatten([
61-
enum_defmodules,
62-
nested_enum_defmodules,
63-
message_defmodules,
64-
service_defmodules,
65-
extension_defmodules
66-
])
59+
defmodules =
60+
List.flatten([
61+
enum_defmodules,
62+
nested_enum_defmodules,
63+
message_defmodules,
64+
service_defmodules,
65+
extension_defmodules
66+
])
67+
68+
{package_level_extensions, defmodules}
6769
end
6870

6971
defp get_dep_type_mapping(%Context{global_type_mapping: global_mapping}, deps, file_name) do

0 commit comments

Comments
 (0)