|
1 | 1 | defmodule TypedStruct do
|
2 |
| - @moduledoc """ |
3 |
| - TypedStruct is a library for defining structs with a type without writing |
4 |
| - boilerplate code. |
5 |
| -
|
6 |
| - ## Rationale |
7 |
| -
|
8 |
| - To define a struct in Elixir, you probably want to define three things: |
9 |
| -
|
10 |
| - * the struct itself, with default values, |
11 |
| - * the list of enforced keys, |
12 |
| - * its associated type. |
13 |
| -
|
14 |
| - It ends up in something like this: |
15 |
| -
|
16 |
| - defmodule Person do |
17 |
| - @moduledoc \"\"\" |
18 |
| - A struct representing a person. |
19 |
| - \"\"\" |
20 |
| -
|
21 |
| - @enforce_keys [:name] |
22 |
| - defstruct name: nil, |
23 |
| - age: nil, |
24 |
| - happy?: true, |
25 |
| - phone: nil |
26 |
| -
|
27 |
| - @typedoc "A person" |
28 |
| - @type t() :: %__MODULE__{ |
29 |
| - name: String.t(), |
30 |
| - age: non_neg_integer() | nil, |
31 |
| - happy?: boolean(), |
32 |
| - phone: String.t() | nil |
33 |
| - } |
34 |
| - end |
35 |
| -
|
36 |
| - In the example above you can notice several points: |
37 |
| -
|
38 |
| - * the keys are present in both the `defstruct` and type definition, |
39 |
| - * enforced keys must also be written in `@enforce_keys`, |
40 |
| - * if a key has no default value and is not enforced, its type should be |
41 |
| - nullable. |
42 |
| -
|
43 |
| - If you want to add a field in the struct, you must therefore: |
44 |
| -
|
45 |
| - * add the key with its default value in the `defstruct` list, |
46 |
| - * add the key with its type in the type definition. |
47 |
| -
|
48 |
| - If the field is not optional, you should even add it to `@enforce_keys`. This |
49 |
| - is way too much work for lazy people like me, and moreover it can be |
50 |
| - error-prone. |
51 |
| -
|
52 |
| - It would be way better if we could write something like this: |
53 |
| -
|
54 |
| - defmodule Person do |
55 |
| - @moduledoc \"\"\" |
56 |
| - A struct representing a person. |
57 |
| - \"\"\" |
58 |
| -
|
59 |
| - use TypedStruct |
60 |
| -
|
61 |
| - typedstruct do |
62 |
| - @typedoc "A person" |
63 |
| -
|
64 |
| - field :name, String.t(), enforce: true |
65 |
| - field :age, non_neg_integer() |
66 |
| - field :happy?, boolean(), default: true |
67 |
| - field :phone, String.t() |
68 |
| - end |
69 |
| - end |
70 |
| -
|
71 |
| - Thanks to TypedStruct, this is now possible :) |
72 |
| -
|
73 |
| - ## Usage |
74 |
| -
|
75 |
| - ### Setup |
76 |
| -
|
77 |
| - To use TypedStruct in your project, add this to your Mix dependencies: |
78 |
| -
|
79 |
| - {:typed_struct, "~> #{Mix.Project.config()[:version]}"} |
80 |
| -
|
81 |
| - If you do not plan to compile modules using TypedStruct at runtime, you can |
82 |
| - add `runtime: false` to the dependency tuple as TypedStruct is only used at |
83 |
| - build time. |
84 |
| -
|
85 |
| - If you want to avoid `mix format` putting parentheses on field definitions, |
86 |
| - you can add to your `.formatter.exs`: |
87 |
| -
|
88 |
| - [ |
89 |
| - ..., |
90 |
| - import_deps: [:typed_struct] |
91 |
| - ] |
92 |
| -
|
93 |
| - ### General usage |
94 |
| -
|
95 |
| - To define a typed struct, use `TypedStruct`, then define your struct within a |
96 |
| - `typedstruct` block: |
97 |
| -
|
98 |
| - defmodule MyStruct do |
99 |
| - # Use TypedStruct to import the typedstruct macro. |
100 |
| - use TypedStruct |
101 |
| -
|
102 |
| - # Define your struct. |
103 |
| - typedstruct do |
104 |
| - # Define each field with the field macro. |
105 |
| - field :a_string, String.t() |
106 |
| -
|
107 |
| - # You can set a default value. |
108 |
| - field :string_with_default, String.t(), default: "default" |
109 |
| -
|
110 |
| - # You can enforce a field. |
111 |
| - field :enforced_field, integer(), enforce: true |
112 |
| - end |
113 |
| - end |
114 |
| -
|
115 |
| - Each field is defined through the `field/2` macro. |
116 |
| -
|
117 |
| - ### Options |
118 |
| -
|
119 |
| - If you want to enforce all the keys by default, you can do: |
120 |
| -
|
121 |
| - defmodule MyStruct do |
122 |
| - use TypedStruct |
123 |
| -
|
124 |
| - # Enforce keys by default. |
125 |
| - typedstruct enforce: true do |
126 |
| - # This key is enforced. |
127 |
| - field :enforced_by_default, term() |
128 |
| -
|
129 |
| - # You can override the default behaviour. |
130 |
| - field :not_enforced, term(), enforce: false |
131 |
| -
|
132 |
| - # A key with a default value is not enforced. |
133 |
| - field :not_enforced_either, integer(), default: 1 |
134 |
| - end |
135 |
| - end |
136 |
| -
|
137 |
| - You can also generate an opaque type for the struct: |
138 |
| -
|
139 |
| - defmodule MyOpaqueStruct do |
140 |
| - use TypedStruct |
141 |
| -
|
142 |
| - # Generate an opaque type for the struct. |
143 |
| - typedstruct opaque: true do |
144 |
| - field :name, String.t() |
145 |
| - end |
146 |
| - end |
147 |
| -
|
148 |
| - If you often define submodules containing only a struct, you can avoid |
149 |
| - boilerplate code: |
150 |
| -
|
151 |
| - defmodule MyModule do |
152 |
| - use TypedStruct |
153 |
| -
|
154 |
| - # You now have %MyModule.Struct{}. |
155 |
| - typedstruct module: Struct do |
156 |
| - field :field, term() |
157 |
| - end |
158 |
| - end |
159 |
| -
|
160 |
| - ### Documentation |
161 |
| -
|
162 |
| - To add a `@typedoc` to the struct type, just add the attribute in the |
163 |
| - `typedstruct` block: |
164 |
| -
|
165 |
| - typedstruct do |
166 |
| - @typedoc "A typed struct" |
167 |
| -
|
168 |
| - field :a_string, String.t() |
169 |
| - field :an_int, integer() |
170 |
| - end |
171 |
| -
|
172 |
| - You can also document submodules this way: |
173 |
| -
|
174 |
| - typedstruct module: MyStruct do |
175 |
| - @moduledoc "A submodule with a typed struct." |
176 |
| - @typedoc "A typed struct in a submodule" |
177 |
| -
|
178 |
| - field :a_string, String.t() |
179 |
| - field :an_int, integer() |
180 |
| - end |
181 |
| -
|
182 |
| - ### Plugins |
183 |
| -
|
184 |
| - It is possible to extend the scope of TypedStruct by using its plugin |
185 |
| - interface, as described in `TypedStruct.Plugin`. For instance, to |
186 |
| - automatically generate lenses with the [Lens](https://github.com/obrok/lens) |
187 |
| - library, you can use |
188 |
| - [`TypedStructLens`](https://github.com/ejpcmac/typed_struct_lens) and do: |
189 |
| -
|
190 |
| - defmodule MyStruct do |
191 |
| - use TypedStruct |
192 |
| -
|
193 |
| - typedstruct do |
194 |
| - plugin TypedStructLens |
195 |
| -
|
196 |
| - field :a_field, String.t() |
197 |
| - field :other_field, atom() |
198 |
| - end |
199 |
| -
|
200 |
| - @spec change(t()) :: t() |
201 |
| - def change(data) do |
202 |
| - # a_field/0 is generated by TypedStructLens. |
203 |
| - lens = a_field() |
204 |
| - put_in(data, [lens], "Changed") |
205 |
| - end |
206 |
| - end |
207 |
| -
|
208 |
| - ## What do I get? |
209 |
| -
|
210 |
| - When defining an empty `typedstruct` block: |
211 |
| -
|
212 |
| - defmodule Example do |
213 |
| - use TypedStruct |
214 |
| -
|
215 |
| - typedstruct do |
216 |
| - end |
217 |
| - end |
218 |
| -
|
219 |
| - you get an empty struct with its module type `t()`: |
220 |
| -
|
221 |
| - defmodule Example do |
222 |
| - @enforce_keys [] |
223 |
| - defstruct [] |
224 |
| -
|
225 |
| - @type t() :: %__MODULE__{} |
226 |
| - end |
227 |
| -
|
228 |
| - Each `field` call adds information to the struct, `@enforce_keys` and the type |
229 |
| - `t()`. |
230 |
| -
|
231 |
| - A field with no options adds the name to the `defstruct` list, with `nil` as |
232 |
| - default. The type itself is made nullable: |
233 |
| -
|
234 |
| - defmodule Example do |
235 |
| - use TypedStruct |
236 |
| -
|
237 |
| - typedstruct do |
238 |
| - field :name, String.t() |
239 |
| - end |
240 |
| - end |
241 |
| -
|
242 |
| - becomes: |
243 |
| -
|
244 |
| - defmodule Example do |
245 |
| - @enforce_keys [] |
246 |
| - defstruct name: nil |
247 |
| -
|
248 |
| - @type t() :: %__MODULE__{ |
249 |
| - name: String.t() | nil |
250 |
| - } |
251 |
| - end |
252 |
| -
|
253 |
| - The `default` option adds the default value to the `defstruct`: |
254 |
| -
|
255 |
| - field :name, String.t(), default: "John Smith" |
256 |
| -
|
257 |
| - # Becomes |
258 |
| - defstruct name: "John Smith" |
259 |
| -
|
260 |
| - When set to `true`, the `enforce` option enforces the key by adding it to the |
261 |
| - `@enforce_keys` attribute. |
262 |
| -
|
263 |
| - field :name, String.t(), enforce: true |
264 |
| -
|
265 |
| - # Becomes |
266 |
| - @enforce_keys [:name] |
267 |
| - defstruct name: nil |
268 |
| -
|
269 |
| - In both cases, the type has no reason to be nullable anymore by default. In |
270 |
| - one case the field is filled with its default value and not `nil`, and in the |
271 |
| - other case it is enforced. Both options would generate the following type: |
272 |
| -
|
273 |
| - @type t() :: %__MODULE__{ |
274 |
| - name: String.t() # Not nullable |
275 |
| - } |
276 |
| -
|
277 |
| - Passing `opaque: true` replaces `@type` with `@opaque` in the struct type |
278 |
| - specification: |
279 |
| -
|
280 |
| - typedstruct opaque: true do |
281 |
| - field :name, String.t() |
282 |
| - end |
283 |
| -
|
284 |
| - generates the following type: |
285 |
| -
|
286 |
| - @opaque t() :: %__MODULE__{ |
287 |
| - name: String.t() |
288 |
| - } |
289 |
| -
|
290 |
| - When passing `module: ModuleName`, the whole `typedstruct` block is wrapped in |
291 |
| - a module definition. This way, the following definition: |
292 |
| -
|
293 |
| - defmodule MyModule do |
294 |
| - use TypedStruct |
295 |
| -
|
296 |
| - typedstruct module: Struct do |
297 |
| - field :field, term() |
298 |
| - end |
299 |
| - end |
300 |
| -
|
301 |
| - becomes: |
302 |
| -
|
303 |
| - defmodule MyModule do |
304 |
| - defmodule Struct do |
305 |
| - @enforce_keys [] |
306 |
| - defstruct field: nil |
307 |
| -
|
308 |
| - @type t() :: %__MODULE__{ |
309 |
| - field: term() | nil |
310 |
| - } |
311 |
| - end |
312 |
| - end |
313 |
| - """ |
| 2 | + @external_resource "README.md" |
| 3 | + @moduledoc "README.md" |
| 4 | + |> File.read!() |
| 5 | + |> String.split("<!-- MDOC !-->") |
| 6 | + |> Enum.fetch!(1) |
314 | 7 |
|
315 | 8 | @doc false
|
316 | 9 | defmacro __using__(_) do
|
|
0 commit comments