Skip to content

Commit 8cf602d

Browse files
authored
Add typespec/1 macro to transform module behaviour (#384)
* When module has transform_module, use it on typespec We detect that `transform_module/0` was defined through a on_definition hook. We then store the module in an attribute. This attribute is later used by the `before_compile` callback to set the typespec, which is set in the form of: ```elixir @t transform_module.t(__MODULE__) ``` Then, transform modules can implement proper typespecs. No changes if `transform_module` is not overriden. * Fix warning * Add type t/1 to transform modules * Implement new idea for typespecs on transformers * Update lib/protobuf/transform_module/infer_fields_from_enum.ex * Take a single argument in typespec macro Module is redundant. One can use __CALLER__ to retrieve the calling module env. * Address review comments * Remove comp-time error Wasn't able to make it work in the way I intended without significant refactoring, think the existing error is good enough for now.
1 parent 8e89af1 commit 8cf602d

File tree

5 files changed

+105
-33
lines changed

5 files changed

+105
-33
lines changed

lib/protobuf.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,14 @@ defmodule Protobuf do
8787
nil
8888
end
8989

90-
defoverridable transform_module: 0
91-
9290
@impl unquote(__MODULE__)
9391
def decode(data), do: Protobuf.Decoder.decode(data, __MODULE__)
9492

9593
@impl unquote(__MODULE__)
9694
def encode(struct), do: Protobuf.Encoder.encode(struct)
95+
96+
@on_definition {Protobuf.DSL, :on_def}
97+
defoverridable transform_module: 0
9798
end
9899
end
99100

lib/protobuf/dsl.ex

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,21 @@ defmodule Protobuf.DSL do
136136
alias Protobuf.MessageProps
137137
alias Protobuf.Wire
138138

139+
# Registered as the @on_definition compile callback for modules that call "use Protobuf"
140+
# Allow us to detect when `transform_module` is re-defined
141+
def on_def(_env, :def, :transform_module, [], [], do: nil) do
142+
:ok
143+
end
144+
145+
def on_def(env, :def, :transform_module, [], [], do: module_alias_ast) do
146+
Module.put_attribute(env.module, :transform_module, module_alias_ast)
147+
:ok
148+
end
149+
150+
def on_def(_, _, _, _, _, _) do
151+
:ok
152+
end
153+
139154
# Registered as the @before_compile callback for modules that call "use Protobuf".
140155
defmacro __before_compile__(env) do
141156
fields = Module.get_attribute(env.module, :fields)
@@ -151,6 +166,7 @@ defmodule Protobuf.DSL do
151166

152167
defines_t_type? = Module.defines_type?(env.module, {:t, 0})
153168
defines_defstruct? = Module.defines?(env.module, {:__struct__, 1})
169+
transform_module_ast = Module.get_attribute(env.module, :transform_module)
154170

155171
quote do
156172
@spec __message_props__() :: Protobuf.MessageProps.t()
@@ -208,7 +224,7 @@ defmodule Protobuf.DSL do
208224

209225
# Newest version of this library generate both the t/0 type as well as the struct.
210226
true ->
211-
unquote(def_t_typespec(msg_props, extension_props))
227+
unquote(def_t_typespec(msg_props, extension_props, transform_module_ast))
212228
unquote(gen_defstruct(msg_props))
213229
end
214230

@@ -226,19 +242,34 @@ defmodule Protobuf.DSL do
226242
end
227243
end
228244

229-
defp def_t_typespec(%MessageProps{enum?: true} = props, _extension_props) do
245+
defp def_t_typespec(props, extension_props, transform_module_ast)
246+
when not is_nil(transform_module_ast) do
247+
default_typespec = def_t_typespec(props, extension_props, nil)
248+
249+
quote do
250+
require unquote(transform_module_ast)
251+
252+
if macro_exported?(unquote(transform_module_ast), :typespec, 1) do
253+
unquote(transform_module_ast).typespec(unquote(default_typespec))
254+
else
255+
unquote(default_typespec)
256+
end
257+
end
258+
end
259+
260+
defp def_t_typespec(%MessageProps{enum?: true} = props, _extension_props, _) do
230261
quote do
231262
@type t() :: unquote(Protobuf.DSL.Typespecs.quoted_enum_typespec(props))
232263
end
233264
end
234265

235-
defp def_t_typespec(%MessageProps{} = props, _extension_props = nil) do
266+
defp def_t_typespec(%MessageProps{} = props, _extension_props = nil, _) do
236267
quote do
237268
@type t() :: unquote(Protobuf.DSL.Typespecs.quoted_message_typespec(props))
238269
end
239270
end
240271

241-
defp def_t_typespec(_props, _extension_props) do
272+
defp def_t_typespec(_props, _extension_props, _) do
242273
nil
243274
end
244275

lib/protobuf/transform_module.ex

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,22 @@ defmodule Protobuf.TransformModule do
2323
defmodule MyTransformModule do
2424
@behaviour Protobuf.TransformModule
2525
26+
defmacro typespec(_default_ast) do
27+
quote do
28+
@type t() :: String.t()
29+
end
30+
end
31+
2632
@impl true
27-
def encode(string, StringMessage) when is_binary(string), do: %StringMessage{value: string}
33+
def encode(string, StringMessage) when is_binary(string), do: struct(StringMessage, value: string)
2834
2935
@impl true
30-
def decode(%StringMessage{value: string}, StringMessage), do: string
36+
def decode(%{value: string}, StringMessage), do: string
3137
end
38+
39+
Notice that since the `c:typespec/1` macro was introduced, transform modules can't
40+
depend on the types that they transform anymore in compile time, meaning struct
41+
syntax can't be used.
3242
"""
3343

3444
@type value() :: term()
@@ -50,4 +60,13 @@ defmodule Protobuf.TransformModule do
5060
Called after a message is decoded.
5161
"""
5262
@callback decode(message(), type()) :: value()
63+
64+
@doc """
65+
Transforms the typespec for modules using this transformer.
66+
67+
If this callback is not present, the default typespec will be used.
68+
"""
69+
@macrocallback typespec(default_typespec :: Macro.t()) :: Macro.t()
70+
71+
@optional_callbacks [typespec: 1]
5372
end

test/protobuf/protoc/cli_integration_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do
4545
protoc!([
4646
"--proto_path=#{tmp_dir}",
4747
"--elixir_out=#{tmp_dir}",
48-
"--elixir_opt=transform_module=MyTransformer",
48+
"--elixir_opt=transform_module=Protobuf.TransformModule.InferFieldsFromEnum",
4949
"--plugin=./protoc-gen-elixir",
5050
proto_path
5151
])
5252

5353
assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/user.pb.ex")
5454
assert mod == Foo.User
5555

56-
assert mod.transform_module() == MyTransformer
56+
assert mod.transform_module() == Protobuf.TransformModule.InferFieldsFromEnum
5757
end
5858

5959
test "gen_descriptors option", %{tmp_dir: tmp_dir, proto_path: proto_path} do

test/support/test_msg.ex

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,39 @@ defmodule TestMsg do
249249
field :mapsi, 3, repeated: true, map: true, type: MapFoo
250250
end
251251

252+
defmodule TransformModule do
253+
@behaviour Protobuf.TransformModule
254+
255+
alias TestMsg.WithTransformModule
256+
257+
@impl true
258+
defmacro typespec(default_typespec) do
259+
case __CALLER__.module do
260+
WithTransformModule ->
261+
quote do
262+
@type t() :: integer()
263+
end
264+
265+
_ ->
266+
default_typespec
267+
end
268+
end
269+
270+
# In an actual implementation, one could write the implementations of
271+
# encode/2 and decode/2 in a separate module and use the structs
272+
# directly.
273+
274+
@impl true
275+
def encode(integer, WithTransformModule) when is_integer(integer) do
276+
struct(WithTransformModule, field: integer)
277+
end
278+
279+
@impl true
280+
def decode(%{__struct__: WithTransformModule, field: integer}, WithTransformModule) do
281+
integer
282+
end
283+
end
284+
252285
defmodule WithTransformModule do
253286
use Protobuf, syntax: :proto3
254287

@@ -277,17 +310,23 @@ defmodule TestMsg do
277310
field :field, 1, type: WithNewTransformModule
278311
end
279312

280-
defmodule TransformModule do
313+
defmodule TransformIntegerStrings do
281314
@behaviour Protobuf.TransformModule
282315

316+
alias TestMsg.ContainsIntegerStringTransformModule
317+
283318
@impl true
284-
def encode(integer, WithTransformModule) when is_integer(integer) do
285-
%WithTransformModule{field: integer}
319+
def encode(
320+
%{__struct__: ContainsIntegerStringTransformModule, field: str},
321+
ContainsIntegerStringTransformModule
322+
)
323+
when is_binary(str) do
324+
struct(ContainsIntegerStringTransformModule, field: String.to_integer(str))
286325
end
287326

288327
@impl true
289-
def decode(%WithTransformModule{field: integer}, WithTransformModule) do
290-
integer
328+
def decode(%{__struct__: ContainsIntegerStringTransformModule} = value, _) do
329+
value
291330
end
292331
end
293332

@@ -299,24 +338,6 @@ defmodule TestMsg do
299338
def transform_module(), do: TestMsg.TransformIntegerStrings
300339
end
301340

302-
defmodule TransformIntegerStrings do
303-
@behaviour Protobuf.TransformModule
304-
305-
@impl true
306-
def encode(
307-
%ContainsIntegerStringTransformModule{field: str},
308-
ContainsIntegerStringTransformModule
309-
)
310-
when is_binary(str) do
311-
%ContainsIntegerStringTransformModule{field: String.to_integer(str)}
312-
end
313-
314-
@impl true
315-
def decode(%ContainsIntegerStringTransformModule{} = value, _) do
316-
value
317-
end
318-
end
319-
320341
defmodule Ext.EnumFoo do
321342
@moduledoc false
322343
use Protobuf, enum: true, syntax: :proto2

0 commit comments

Comments
 (0)