Skip to content

Commit cc93185

Browse files
authored
feat: support :defrecord and :defrecordp and custom definer (#21)
1 parent 800ca9e commit cc93185

File tree

15 files changed

+721
-82
lines changed

15 files changed

+721
-82
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,21 @@ defmodule HTTPException do
228228
end
229229
```
230230

231+
## Define records related macros
232+
233+
In Elixir, you can use the Record module to define and work with Erlang records,
234+
making interoperability between Elixir and Erlang more seamless.
235+
236+
```elixir
237+
defmodule TypedStructor.User do
238+
use TypedStructor
239+
240+
typed_structor definer: :defrecord, record_name: :user, record_tag: User, enforce: true do
241+
field :name, String.t()
242+
field :age, pos_integer()
243+
end
244+
end
245+
```
231246
## Documentation
232247

233248
To add a `@typedoc` to the struct type, just add the attribute in the typed_structor block:

guides/plugins/reflection.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ defmodule Guides.Plugins.Reflection do
1616

1717
enforced_fields =
1818
definition.fields
19-
|> Stream.filter(&Keyword.get(&1, :enforce, false))
19+
|> Stream.filter(fn field ->
20+
Keyword.get_lazy(field, :enforce, fn ->
21+
Keyword.get(definition.options, :enforce, false)
22+
end)
23+
end)
2024
|> Stream.map(&Keyword.fetch!(&1, :name))
2125
|> Enum.to_list()
2226

lib/typed_structor.ex

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ defmodule TypedStructor do
55
|> String.split("<!-- MODULEDOC -->", parts: 2)
66
|> Enum.fetch!(1)
77

8+
@built_in_definers [
9+
defstruct: TypedStructor.Definer.Defstruct,
10+
defexception: TypedStructor.Definer.Defexception,
11+
defrecord: TypedStructor.Definer.Defrecord,
12+
defrecordp: TypedStructor.Definer.Defrecordp
13+
]
14+
815
defmacro __using__(_opts) do
916
quote do
1017
import TypedStructor, only: [typed_structor: 1, typed_structor: 2]
@@ -28,24 +35,28 @@ defmodule TypedStructor do
2835
The available definers are:
2936
- `:defstruct`, which defines a struct and a type for a given definition
3037
- `:defexception`, which defines an exception and a type for a given definition
38+
- `:defrecord`, which defines record macros and a type for a given definition
39+
- `:defrecordp`, which defines private record macros and a type for a given definition
3140
3241
### `:defstruct` options
3342
34-
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
43+
#{TypedStructor.Definer.Defstruct.__additional_options__()}
3544
3645
### `:defexception` options
3746
38-
* `:define_struct` - if `false`, the type will be defined, but the exception struct will not be defined. Defaults to `true`.
47+
#{TypedStructor.Definer.Defexception.__additional_options__()}
48+
49+
### `:defrecord` and `:defrecordp` options
50+
51+
#{TypedStructor.Definer.Defrecord.__additional_options__()}
3952
4053
### custom definer
4154
4255
defmodule MyStruct do
43-
# you must require the definer module to use its define/1 macro
44-
require MyDefiner
45-
4656
use TypedStructor
4757
48-
typed_structor definer: &MyDefiner.define/1 do
58+
typed_structor definer: MyDefiner do
59+
4960
field :name, String.t()
5061
field :age, integer()
5162
end
@@ -91,7 +102,7 @@ defmodule TypedStructor do
91102
defmacro typed_structor(options \\ [], do: block) when is_list(options) do
92103
case Keyword.pop(options, :module) do
93104
{nil, options} ->
94-
__typed_structor__(__CALLER__.module, options, block)
105+
__typed_structor__(__CALLER__, options, block)
95106

96107
{module, options} ->
97108
quote do
@@ -106,16 +117,18 @@ defmodule TypedStructor do
106117
end
107118
end
108119

109-
defp __typed_structor__(mod, options, block) do
110-
Module.register_attribute(mod, :__ts_options__, accumulate: false)
111-
Module.register_attribute(mod, :__ts_struct_fields__, accumulate: true)
112-
Module.register_attribute(mod, :__ts_struct_parameters__, accumulate: true)
113-
Module.register_attribute(mod, :__ts_struct_plugins__, accumulate: true)
114-
Module.register_attribute(mod, :__ts_definition____, accumulate: false)
120+
defp __typed_structor__(caller, options, block) when is_list(options) do
121+
case fetch_definer!(caller, options) do
122+
:error -> :ok
123+
{:ok, definer} -> Module.put_attribute(caller.module, :__ts_definer__, definer)
124+
end
115125

116-
quote do
117-
@__ts_options__ unquote(options)
126+
Module.register_attribute(caller.module, :__ts_struct_fields__, accumulate: true)
127+
Module.register_attribute(caller.module, :__ts_struct_parameters__, accumulate: true)
128+
Module.register_attribute(caller.module, :__ts_struct_plugins__, accumulate: true)
129+
Module.register_attribute(caller.module, :__ts_definition____, accumulate: false)
118130

131+
quote do
119132
# create a lexical scope
120133
try do
121134
import TypedStructor,
@@ -132,15 +145,14 @@ defmodule TypedStructor do
132145
try do
133146
definition =
134147
TypedStructor.__call_plugins_before_definitions__(%TypedStructor.Definition{
135-
options: @__ts_options__,
148+
options: unquote(options),
136149
fields: Enum.reverse(@__ts_struct_fields__),
137150
parameters: Enum.reverse(@__ts_struct_parameters__)
138151
})
139152

140153
@__ts_definition__ definition
141154
after
142155
# cleanup
143-
Module.delete_attribute(__MODULE__, :__ts_options__)
144156
Module.delete_attribute(__MODULE__, :__ts_struct_fields__)
145157
Module.delete_attribute(__MODULE__, :__ts_struct_parameters__)
146158
end
@@ -154,10 +166,30 @@ defmodule TypedStructor do
154166
# cleanup
155167
Module.delete_attribute(__MODULE__, :__ts_struct_plugins__)
156168
Module.delete_attribute(__MODULE__, :__ts_definition__)
169+
Module.delete_attribute(__MODULE__, :__ts_definer__)
157170
end
158171
end
159172
end
160173

174+
defp fetch_definer!(caller, options) when is_list(options) do
175+
case Keyword.fetch(options, :definer) do
176+
:error ->
177+
:error
178+
179+
{:ok, definer} ->
180+
case Macro.expand(definer, caller) do
181+
built_in_or_mod when is_atom(built_in_or_mod) ->
182+
{:ok, built_in_or_mod}
183+
184+
other ->
185+
raise ArgumentError, """
186+
Definer must be one of :defstruct, :defexception, :defrecord, :defrecordp or a module that defines a `define/1` macro,
187+
got: #{inspect(other)}
188+
"""
189+
end
190+
end
191+
end
192+
161193
# register global plugins
162194
defp register_global_plugins do
163195
:typed_structor
@@ -219,7 +251,7 @@ defmodule TypedStructor do
219251
options = Keyword.merge(options, name: name, type: Macro.escape(type))
220252

221253
quote do
222-
@__ts_struct_fields__ Keyword.merge(@__ts_options__, unquote(options))
254+
@__ts_struct_fields__ unquote(options)
223255
end
224256
end
225257

@@ -272,21 +304,30 @@ defmodule TypedStructor do
272304
end
273305

274306
defmacro __define__(definition) do
275-
quote bind_quoted: [definition: definition] do
276-
case Keyword.get(definition.options, :definer, :defstruct) do
277-
:defstruct ->
278-
require TypedStructor.Definer.Defstruct
279-
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
280-
TypedStructor.Definer.Defstruct.define(definition)
281-
282-
:defexception ->
283-
require TypedStructor.Definer.Defexception
284-
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
285-
TypedStructor.Definer.Defexception.define(definition)
286-
287-
fun when is_function(fun) ->
288-
then(definition, fun)
307+
definer = Module.get_attribute(__CALLER__.module, :__ts_definer__, :defstruct)
308+
definer_mod = Keyword.get(@built_in_definers, definer, definer)
309+
310+
quote do
311+
case Keyword.fetch(unquote(definition).options, :definer) do
312+
:error ->
313+
:ok
314+
315+
{:ok, unquote(definer)} ->
316+
:ok
317+
318+
{:ok, other} ->
319+
IO.warn("""
320+
The definer option set in the `typed_structor` block is different from the definer option in the definition.
321+
We will ignore the definer option in the definition and use the one set in the `typed_structor` block.
322+
323+
Note: The definer option in the definition may be changed by a plugin.
324+
325+
The effective definer is: #{inspect(unquote(definer))}, the ignored definer from the definition is: #{inspect(other)}.
326+
""")
289327
end
328+
329+
require unquote(definer_mod)
330+
unquote(definer_mod).define(unquote(definition))
290331
end
291332
end
292333

lib/typed_structor/definer/defexception.ex

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
defmodule TypedStructor.Definer.Defexception do
2+
additional_options = """
3+
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
4+
"""
5+
26
@moduledoc """
37
A definer to define an exception and a type for a given definition.
48
59
## Additional options for `typed_structor`
610
7-
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
11+
#{additional_options}
812
913
## Usage
1014
@@ -18,6 +22,7 @@ defmodule TypedStructor.Definer.Defexception do
1822
"""
1923

2024
alias TypedStructor.Definer.Defstruct
25+
alias TypedStructor.Definer.Utils
2126

2227
@doc """
2328
Defines an exception and a type for a given definition.
@@ -35,12 +40,14 @@ defmodule TypedStructor.Definer.Defexception do
3540
defmacro __exception_ast__(definition) do
3641
quote bind_quoted: [definition: definition] do
3742
if Keyword.get(definition.options, :define_struct, true) do
38-
{fields, enforce_keys} =
39-
Defstruct.__extract_fields_and_enforce_keys__(definition)
43+
{fields, enforce_keys} = Utils.fields_and_enforce_keys(definition)
4044

4145
@enforce_keys Enum.reverse(enforce_keys)
4246
defexception fields
4347
end
4448
end
4549
end
50+
51+
@doc false
52+
def __additional_options__, do: unquote(additional_options)
4653
end

0 commit comments

Comments
 (0)