-
Notifications
You must be signed in to change notification settings - Fork 36
/
Copy pathtyped_struct.ex
232 lines (185 loc) · 6.12 KB
/
typed_struct.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
defmodule TypedStruct do
@external_resource "README.md"
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- @moduledoc -->")
|> Enum.fetch!(1)
@accumulating_attrs [
:ts_plugins,
:ts_plugin_fields,
:ts_fields,
:ts_types,
:ts_enforce_keys
]
@attrs_to_delete [:ts_enforce? | @accumulating_attrs]
@doc false
defmacro __using__(_) do
quote do
import TypedStruct, only: [typedstruct: 1, typedstruct: 2]
end
end
@doc """
Defines a typed struct.
Inside a `typedstruct` block, each field is defined through the `field/2`
macro.
## Options
* `enforce` - if set to true, sets `enforce: true` to all fields by default.
This can be overridden by setting `enforce: false` or a default value on
individual fields.
* `opaque` - if set to true, creates an opaque type for the struct.
* `module` - if set, creates the struct in a submodule named `module`.
## Examples
defmodule MyStruct do
use TypedStruct
typedstruct do
field :field_one, String.t()
field :field_two, integer(), enforce: true
field :field_three, boolean(), enforce: true
field :field_four, atom(), default: :hey
end
end
The following is an equivalent using the *enforce by default* behaviour:
defmodule MyStruct do
use TypedStruct
typedstruct enforce: true do
field :field_one, String.t(), enforce: false
field :field_two, integer()
field :field_three, boolean()
field :field_four, atom(), default: :hey
end
end
You can create the struct in a submodule instead:
defmodule MyModule do
use TypedStruct
typedstruct module: Struct do
field :field_one, String.t()
field :field_two, integer(), enforce: true
field :field_three, boolean(), enforce: true
field :field_four, atom(), default: :hey
end
end
"""
defmacro typedstruct(opts \\ [], do: block) do
ast = TypedStruct.__typedstruct__(block, opts)
case opts[:module] do
nil ->
quote do
# Create a lexical scope.
(fn -> unquote(ast) end).()
end
module ->
quote do
defmodule unquote(module) do
unquote(ast)
end
end
end
end
@doc false
def __typedstruct__(block, opts) do
quote do
Enum.each(unquote(@accumulating_attrs), fn attr ->
Module.register_attribute(__MODULE__, attr, accumulate: true)
end)
Module.put_attribute(__MODULE__, :ts_enforce?, unquote(!!opts[:enforce]))
@before_compile {unquote(__MODULE__), :__plugin_callbacks__}
import TypedStruct
unquote(block)
@enforce_keys @ts_enforce_keys
defstruct @ts_fields
TypedStruct.__type__(@ts_types, unquote(opts))
end
end
@doc false
defmacro __type__(types, opts) do
if Keyword.get(opts, :opaque, false) do
quote bind_quoted: [types: types] do
@opaque t() :: %__MODULE__{unquote_splicing(types)}
end
else
quote bind_quoted: [types: types] do
@type t() :: %__MODULE__{unquote_splicing(types)}
end
end
end
@doc """
Registers a plugin for the currently defined struct.
## Example
typedstruct do
plugin MyPlugin
field :a_field, String.t()
end
For more information on how to define your own plugins, please see
`TypedStruct.Plugin`. To use a third-party plugin, please refer directly to
its documentation.
"""
defmacro plugin(plugin, opts \\ []) do
quote do
Module.put_attribute(
__MODULE__,
:ts_plugins,
{unquote(plugin), unquote(opts)}
)
require unquote(plugin)
unquote(plugin).init(unquote(opts))
end
end
@doc """
Defines a field in a typed struct.
## Example
# A field named :example of type String.t()
field :example, String.t()
## Options
* `default` - sets the default value for the field
* `enforce` - if set to true, enforces the field and makes its type
non-nullable
"""
defmacro field(name, type, opts \\ []) do
quote bind_quoted: [name: name, type: Macro.escape(type), opts: opts] do
TypedStruct.__field__(name, type, opts, __ENV__)
end
end
@doc false
def __field__(name, type, opts, %Macro.Env{module: mod} = env)
when is_atom(name) do
if mod |> Module.get_attribute(:ts_fields) |> Keyword.has_key?(name) do
raise ArgumentError, "the field #{inspect(name)} is already set"
end
has_default? = Keyword.has_key?(opts, :default)
enforce_by_default? = Module.get_attribute(mod, :ts_enforce?)
enforce? =
if is_nil(opts[:enforce]),
do: enforce_by_default? && !has_default?,
else: !!opts[:enforce]
nullable? = !has_default? && !enforce?
Module.put_attribute(mod, :ts_fields, {name, opts[:default]})
Module.put_attribute(mod, :ts_plugin_fields, {name, type, opts, env})
Module.put_attribute(mod, :ts_types, {name, type_for(type, nullable?)})
if enforce?, do: Module.put_attribute(mod, :ts_enforce_keys, name)
end
def __field__(name, _type, _opts, _env) do
raise ArgumentError, "a field name must be an atom, got #{inspect(name)}"
end
# Makes the type nullable if the key is not enforced.
defp type_for(type, false), do: type
defp type_for(type, _), do: quote(do: unquote(type) | nil)
@doc false
defmacro __plugin_callbacks__(%Macro.Env{module: module}) do
plugins = Module.get_attribute(module, :ts_plugins)
fields = Module.get_attribute(module, :ts_plugin_fields) |> Enum.reverse()
Enum.each(unquote(@attrs_to_delete), &Module.delete_attribute(module, &1))
fields_block =
for {plugin, plugin_opts} <- plugins,
{name, type, field_opts, env} <- fields do
plugin.field(name, type, field_opts ++ plugin_opts, env)
end
after_definition_block =
for {plugin, plugin_opts} <- plugins do
plugin.after_definition(plugin_opts)
end
quote do
unquote_splicing(fields_block)
unquote_splicing(after_definition_block)
end
end
end