Skip to content

Commit ecaec7d

Browse files
authored
refactor: extract definer (#18)
1 parent ef208e8 commit ecaec7d

File tree

5 files changed

+240
-131
lines changed

5 files changed

+240
-131
lines changed

lib/typed_structor.ex

+39-79
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,31 @@ defmodule TypedStructor do
2020
2121
* `:module` - if provided, a new submodule will be created with the struct.
2222
* `:enforce` - if `true`, the struct will enforce the keys, see `field/3` options for more information.
23-
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
23+
* `:definer` - the definer module to use to define the struct, record or exception. Defaults to `:defstruct`. It also accepts a macro that receives the definition struct and returns the AST. See definer section below.
2424
* `:type_kind` - the kind of type to use for the struct. Defaults to `type`, can be `opaque` or `typep`.
2525
* `:type_name` - the name of the type to use for the struct. Defaults to `t`.
2626
27+
## Definer
28+
There are one available definer for now, `:defstruct`, which defines a struct and a type for a given definition.
29+
30+
### `:defstruct` options
31+
32+
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
33+
34+
### custom definer
35+
36+
defmodule MyStruct do
37+
# you must require the definer module to use its define/1 macro
38+
require MyDefiner
39+
40+
use TypedStructor
41+
42+
typed_structor definer: &MyDefiner.define/1 do
43+
field :name, String.t()
44+
field :age, integer()
45+
end
46+
end
47+
2748
## Examples
2849
2950
defmodule MyStruct do
@@ -111,22 +132,20 @@ defmodule TypedStructor do
111132
})
112133

113134
@__ts_definition__ definition
114-
@__ts_options__ definition.options
115135
after
116-
:ok
136+
# cleanup
137+
Module.delete_attribute(__MODULE__, :__ts_options__)
138+
Module.delete_attribute(__MODULE__, :__ts_struct_fields__)
139+
Module.delete_attribute(__MODULE__, :__ts_struct_parameters__)
117140
end
118141

119-
TypedStructor.__struct_ast__()
120-
TypedStructor.__type_ast__()
142+
TypedStructor.__define__(@__ts_definition__)
121143

122144
# create a lexical scope
123145
try do
124-
TypedStructor.__call_plugins_after_definitions__()
146+
TypedStructor.__call_plugins_after_definitions__(@__ts_definition__)
125147
after
126148
# cleanup
127-
Module.delete_attribute(__MODULE__, :__ts_options__)
128-
Module.delete_attribute(__MODULE__, :__ts_struct_fields__)
129-
Module.delete_attribute(__MODULE__, :__ts_struct_parameters__)
130149
Module.delete_attribute(__MODULE__, :__ts_struct_plugins__)
131150
Module.delete_attribute(__MODULE__, :__ts_definition__)
132151
end
@@ -246,75 +265,16 @@ defmodule TypedStructor do
246265
end
247266
end
248267

249-
@doc false
250-
defmacro __struct_ast__ do
251-
ast =
252-
quote do
253-
{fields, enforce_keys} =
254-
Enum.map_reduce(@__ts_definition__.fields, [], fn field, acc ->
255-
name = Keyword.fetch!(field, :name)
256-
default = Keyword.get(field, :default)
257-
258-
if Keyword.get(field, :enforce, false) and not Keyword.has_key?(field, :default) do
259-
{{name, default}, [name | acc]}
260-
else
261-
{{name, default}, acc}
262-
end
263-
end)
264-
265-
@enforce_keys Enum.reverse(enforce_keys)
266-
defstruct fields
267-
end
268-
269-
quote do
270-
if Keyword.get(@__ts_options__, :define_struct, true) do
271-
unquote(ast)
272-
end
273-
end
274-
end
275-
276-
@doc false
277-
defmacro __type_ast__ do
278-
quote unquote: false do
279-
fields =
280-
Enum.reduce(@__ts_definition__.fields, [], fn field, acc ->
281-
name = Keyword.fetch!(field, :name)
282-
type = Keyword.fetch!(field, :type)
283-
284-
if Keyword.get(field, :enforce, false) or Keyword.has_key?(field, :default) do
285-
[{name, type} | acc]
286-
else
287-
[{name, quote(do: unquote(type) | nil)} | acc]
288-
end
289-
end)
290-
291-
type_name = Keyword.get(@__ts_options__, :type_name, :t)
292-
293-
parameters =
294-
Enum.map(
295-
@__ts_definition__.parameters,
296-
fn parameter ->
297-
parameter
298-
|> Keyword.fetch!(:name)
299-
|> Macro.var(__MODULE__)
300-
end
301-
)
268+
defmacro __define__(definition) do
269+
quote bind_quoted: [definition: definition] do
270+
case Keyword.get(definition.options, :definer, :defstruct) do
271+
:defstruct ->
272+
require TypedStructor.Definer.Defstruct
273+
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
274+
TypedStructor.Definer.Defstruct.define(definition)
302275

303-
case Keyword.get(@__ts_options__, :type_kind, :type) do
304-
:type ->
305-
@type unquote(type_name)(unquote_splicing(parameters)) :: %__MODULE__{
306-
unquote_splicing(fields)
307-
}
308-
309-
:opaque ->
310-
@opaque unquote(type_name)(unquote_splicing(parameters)) :: %__MODULE__{
311-
unquote_splicing(fields)
312-
}
313-
314-
:typep ->
315-
@typep unquote(type_name)(unquote_splicing(parameters)) :: %__MODULE__{
316-
unquote_splicing(fields)
317-
}
276+
fun when is_function(fun) ->
277+
then(definition, fun)
318278
end
319279
end
320280
end
@@ -352,15 +312,15 @@ defmodule TypedStructor do
352312
end
353313

354314
@doc false
355-
defmacro __call_plugins_after_definitions__ do
315+
defmacro __call_plugins_after_definitions__(definition) do
356316
plugins = Module.get_attribute(__CALLER__.module, :__ts_struct_plugins__)
357317

358318
for {plugin, opts} <- plugins do
359319
quote do
360320
require unquote(plugin)
361321

362322
unquote(plugin).after_definition(
363-
@__ts_definition__,
323+
unquote(definition),
364324
unquote(Macro.escape(opts))
365325
)
366326
end
+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
defmodule TypedStructor.Definer.Defstruct do
2+
@moduledoc """
3+
A definer to define a struct and a type for a given definition.
4+
5+
## Additional options for `typed_structor`
6+
7+
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
8+
9+
## Usage
10+
11+
defmodule MyStruct do
12+
use TypedStructor
13+
14+
typed_structor definer: :defstruct, define_struct: false do
15+
field :name, String.t()
16+
field :age, integer()
17+
end
18+
end
19+
"""
20+
21+
@doc """
22+
Defines a struct and a type for a given definition.
23+
"""
24+
defmacro define(definition) do
25+
quote do
26+
unquote(__MODULE__).__struct_ast__(unquote(definition))
27+
unquote(__MODULE__).__type_ast__(unquote(definition))
28+
end
29+
end
30+
31+
@doc false
32+
defmacro __struct_ast__(definition) do
33+
ast =
34+
quote do
35+
{fields, enforce_keys} =
36+
Enum.map_reduce(unquote(definition).fields, [], fn field, acc ->
37+
name = Keyword.fetch!(field, :name)
38+
default = Keyword.get(field, :default)
39+
40+
if Keyword.get(field, :enforce, false) and not Keyword.has_key?(field, :default) do
41+
{{name, default}, [name | acc]}
42+
else
43+
{{name, default}, acc}
44+
end
45+
end)
46+
47+
@enforce_keys Enum.reverse(enforce_keys)
48+
defstruct fields
49+
end
50+
51+
quote do
52+
if Keyword.get(unquote(definition).options, :define_struct, true) do
53+
unquote(ast)
54+
end
55+
end
56+
end
57+
58+
@doc false
59+
defmacro __type_ast__(definition) do
60+
quote bind_quoted: [definition: definition] do
61+
fields =
62+
Enum.reduce(definition.fields, [], fn field, acc ->
63+
name = Keyword.fetch!(field, :name)
64+
type = Keyword.fetch!(field, :type)
65+
66+
if Keyword.get(field, :enforce, false) or Keyword.has_key?(field, :default) do
67+
[{name, type} | acc]
68+
else
69+
[{name, quote(do: unquote(type) | nil)} | acc]
70+
end
71+
end)
72+
73+
type_name = Keyword.get(definition.options, :type_name, :t)
74+
75+
parameters =
76+
Enum.map(
77+
definition.parameters,
78+
fn parameter ->
79+
parameter
80+
|> Keyword.fetch!(:name)
81+
|> Macro.var(__MODULE__)
82+
end
83+
)
84+
85+
case Keyword.get(definition.options, :type_kind, :type) do
86+
:type ->
87+
@type unquote(type_name)(unquote_splicing(parameters)) :: %__MODULE__{
88+
unquote_splicing(fields)
89+
}
90+
91+
:opaque ->
92+
@opaque unquote(type_name)(unquote_splicing(parameters)) :: %__MODULE__{
93+
unquote_splicing(fields)
94+
}
95+
96+
:typep ->
97+
@typep unquote(type_name)(unquote_splicing(parameters)) :: %__MODULE__{
98+
unquote_splicing(fields)
99+
}
100+
end
101+
end
102+
end
103+
end

mix.exs

+13-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ defmodule TypedStructor.MixProject do
6060
TypedStructor.GuideCase,
6161
TypedStructor.TestCase
6262
]
63-
]
63+
],
64+
aliases: aliases()
6465
]
6566
end
6667

@@ -83,4 +84,15 @@ defmodule TypedStructor.MixProject do
8384

8485
defp elixirc_paths(:test), do: ["lib", "test/support"]
8586
defp elixirc_paths(_), do: ["lib"]
87+
88+
defp aliases do
89+
[
90+
check: [
91+
"format",
92+
"compile --warning-as-errors",
93+
"credo --strict",
94+
"dialyzer"
95+
]
96+
]
97+
end
8698
end

test/definer_test.exs

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
defmodule DefinerTest do
2+
@compile {:no_warn_undefined, __MODULE__.Struct}
3+
4+
use TypedStructor.TestCase, async: true
5+
6+
describe "defstruct" do
7+
@tag :tmp_dir
8+
test "works", ctx do
9+
expected_types =
10+
with_tmpmodule Struct, ctx do
11+
@type t() :: %__MODULE__{
12+
age: integer() | nil,
13+
name: String.t() | nil
14+
}
15+
16+
defstruct [:age, :name]
17+
after
18+
fetch_types!(Struct)
19+
end
20+
21+
generated_types =
22+
with_tmpmodule Struct, ctx do
23+
use TypedStructor
24+
25+
typed_structor definer: :defstruct do
26+
field :name, String.t()
27+
field :age, integer()
28+
end
29+
after
30+
assert %{__struct__: Struct, name: nil, age: nil} === struct(Struct)
31+
32+
fetch_types!(Struct)
33+
end
34+
35+
assert_type expected_types, generated_types
36+
end
37+
38+
@tag :tmp_dir
39+
test "define_struct false", ctx do
40+
deftmpmodule Struct, ctx do
41+
use TypedStructor
42+
43+
typed_structor define_struct: false do
44+
parameter :age
45+
46+
field :name, String.t()
47+
field :age, age
48+
end
49+
50+
defstruct name: "Phil", age: 20
51+
end
52+
53+
assert %{__struct__: Struct, name: "Phil", age: 20} === struct(Struct)
54+
after
55+
cleanup_modules([__MODULE__.Struct], ctx.tmp_dir)
56+
end
57+
58+
@tag :tmp_dir
59+
test "works with Ecto.Schema", ctx do
60+
deftmpmodule Struct, ctx do
61+
use TypedStructor
62+
63+
typed_structor define_struct: false do
64+
parameter :age
65+
66+
field :name, String.t()
67+
field :age, age
68+
end
69+
70+
use Ecto.Schema
71+
72+
schema "source" do
73+
field :name, :string
74+
field :age, :integer, default: 20
75+
end
76+
end
77+
78+
assert [:id, :name, :age] === Struct.__schema__(:fields)
79+
80+
assert match?(%{__struct__: Struct, id: nil, name: nil, age: 20}, struct(Struct))
81+
after
82+
cleanup_modules([__MODULE__.Struct], ctx.tmp_dir)
83+
end
84+
end
85+
end

0 commit comments

Comments
 (0)