Skip to content

Commit b31b509

Browse files
committed
feat: add type parameters
1 parent deeca64 commit b31b509

File tree

4 files changed

+138
-10
lines changed

4 files changed

+138
-10
lines changed

.formatter.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
locals_without_parens = [field: 2, field: 3, plugin: 1, plugin: 2]
1+
locals_without_parens = [parameter: 1, field: 2, field: 3, plugin: 1, plugin: 2]
22

33
[
44
inputs: [

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,34 @@ defmodule MyModule do
182182
end
183183
```
184184

185+
You can also add type parameters:
186+
```elixir
187+
defmodule User do
188+
use TypedStruct
189+
190+
typedstruct do
191+
parameter :state
192+
193+
field :state, state, enforce: true
194+
field :name, String.t()
195+
end
196+
end
197+
```
198+
199+
this equals to:
200+
```elixir
201+
defmodule User do
202+
@enforce_keys [:state]
203+
defstruct state: nil,
204+
name: nil
205+
206+
@type t(state) :: %__MODULE__{
207+
state: state,
208+
name: String.t() | nil
209+
}
210+
end
211+
```
212+
185213
### Documentation
186214

187215
To add a `@typedoc` to the struct type, just add the attribute in the

lib/typed_struct.ex

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule TypedStruct do
88
@accumulating_attrs [
99
:ts_plugins,
1010
:ts_plugin_fields,
11+
:ts_parameters,
1112
:ts_fields,
1213
:ts_types,
1314
:ts_enforce_keys
@@ -74,6 +75,18 @@ defmodule TypedStruct do
7475
field :field_four, atom(), default: :hey
7576
end
7677
end
78+
79+
You can also add type parameters:
80+
81+
defmodule MyModule do
82+
use TypedStruct
83+
84+
typedstruct do
85+
parameter :parameter
86+
87+
field :field, parameter
88+
end
89+
end
7790
"""
7891
defmacro typedstruct(opts \\ [], do: block) do
7992
ast = TypedStruct.__typedstruct__(block, opts)
@@ -110,19 +123,23 @@ defmodule TypedStruct do
110123
@enforce_keys @ts_enforce_keys
111124
defstruct @ts_fields
112125

113-
TypedStruct.__type__(@ts_types, unquote(opts))
126+
TypedStruct.__type__(@ts_parameters, @ts_types, unquote(opts))
114127
end
115128
end
116129

117130
@doc false
118-
defmacro __type__(types, opts) do
131+
defmacro __type__(parameters, types, opts) do
119132
if Keyword.get(opts, :opaque, false) do
120-
quote bind_quoted: [types: types] do
121-
@opaque t() :: %__MODULE__{unquote_splicing(types)}
133+
quote bind_quoted: [parameters: parameters, types: types] do
134+
@opaque t(unquote_splicing(parameters)) :: %__MODULE__{
135+
unquote_splicing(types)
136+
}
122137
end
123138
else
124-
quote bind_quoted: [types: types] do
125-
@type t() :: %__MODULE__{unquote_splicing(types)}
139+
quote bind_quoted: [parameters: parameters, types: types] do
140+
@type t(unquote_splicing(parameters)) :: %__MODULE__{
141+
unquote_splicing(types)
142+
}
126143
end
127144
end
128145
end
@@ -155,6 +172,22 @@ defmodule TypedStruct do
155172
end
156173
end
157174

175+
@doc """
176+
Defines a type parameter for a typed struct.
177+
178+
## Example
179+
180+
# A type parameter named int
181+
parameter :int
182+
183+
field :number, int # not int()
184+
"""
185+
defmacro parameter(name) do
186+
quote bind_quoted: [name: name] do
187+
TypedStruct.__parameter__(name, __ENV__)
188+
end
189+
end
190+
158191
@doc """
159192
Defines a field in a typed struct.
160193
@@ -175,6 +208,16 @@ defmodule TypedStruct do
175208
end
176209
end
177210

211+
@doc false
212+
def __parameter__(name, %Macro.Env{module: mod}) when is_atom(name) do
213+
Module.put_attribute(mod, :ts_parameters, Macro.var(name, mod))
214+
end
215+
216+
def __parameter__(name, _env) do
217+
raise ArgumentError,
218+
"a parameter name must be an atom, got #{inspect(name)}"
219+
end
220+
178221
@doc false
179222
def __field__(name, type, opts, %Macro.Env{module: mod} = env)
180223
when is_atom(name) do

test/typed_struct_test.exs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ defmodule TypedStructTest do
2020
def enforce_keys, do: @enforce_keys
2121
end
2222

23+
{:module, _name, bytecode_parameters, _exports} =
24+
defmodule ParameterTestStruct do
25+
use TypedStruct
26+
27+
@typep int() :: integer()
28+
29+
typedstruct do
30+
parameter :str
31+
32+
field :int, int()
33+
field :string, str
34+
field :mandatory_string, str, enforce: true
35+
end
36+
end
37+
2338
{:module, _name, bytecode_opaque, _exports} =
2439
defmodule OpaqueTestStruct do
2540
use TypedStruct
@@ -61,6 +76,7 @@ defmodule TypedStructTest do
6176
end
6277

6378
@bytecode bytecode
79+
@bytecode_parameters bytecode_parameters
6480
@bytecode_opaque bytecode_opaque
6581
@bytecode_noalias bytecode_noalias
6682

@@ -130,6 +146,38 @@ defmodule TypedStructTest do
130146
assert type1 == type2
131147
end
132148

149+
test "generates a parameterized type for the struct", context do
150+
# Define a second struct with the type expected for ParameterTestStruct.
151+
{:module, _name, bytecode2, _exports} =
152+
defmodule ParameterTestStruct2 do
153+
@typep int() :: integer()
154+
155+
@enforce_keys [:mandatory_string]
156+
157+
defstruct [:int, :string, :mandatory_string]
158+
159+
@type t(str) :: %__MODULE__{
160+
int: int() | nil,
161+
string: str | nil,
162+
mandatory_string: str
163+
}
164+
end
165+
166+
# Get both types and standardise them (remove line numbers and rename
167+
# the second struct with the name of the first one).
168+
type1 =
169+
@bytecode_parameters
170+
|> extract_first_type()
171+
|> standardise(TypedStructTest.ParameterTestStruct)
172+
173+
type2 =
174+
bytecode2
175+
|> extract_first_type()
176+
|> standardise(TypedStructTest.ParameterTestStruct2)
177+
178+
assert type1 == type2
179+
end
180+
133181
test "generates an opaque type if `opaque: true` is set" do
134182
# Define a second struct with the type expected for TestStruct.
135183
{:module, _name, bytecode_expected, _exports} =
@@ -248,20 +296,29 @@ defmodule TypedStructTest do
248296
defp standardise(type_info, struct \\ @standard_struct_name)
249297

250298
defp standardise({name, type, params}, struct) when is_tuple(type),
251-
do: {name, standardise(type, struct), params}
299+
do: {name, standardise(type, struct), standardise_params(params)}
252300

253301
defp standardise({:type, _, type, params}, struct),
254302
do: {:type, :line, type, standardise(params, struct)}
255303

304+
defp standardise({:user_type, _, type, params}, struct),
305+
do: {:user_type, :line, type, standardise(params, struct)}
306+
256307
defp standardise({:remote_type, _, params}, struct),
257308
do: {:remote_type, :line, standardise(params, struct)}
258309

259310
defp standardise({:atom, _, struct}, struct),
260311
do: {:atom, :line, @standard_struct_name}
261312

262-
defp standardise({type, _, litteral}, _struct),
263-
do: {type, :line, litteral}
313+
defp standardise({type, _, literal}, _struct),
314+
do: {type, :line, literal}
264315

265316
defp standardise(list, struct) when is_list(list),
266317
do: Enum.map(list, &standardise(&1, struct))
318+
319+
defp standardise_params(params) when is_list(params),
320+
do: Enum.map(params, &standardise_params/1)
321+
322+
defp standardise_params({:var, _, name}), do: {:var, :id, name}
323+
defp standardise_params(params), do: params
267324
end

0 commit comments

Comments
 (0)