Skip to content

Commit 8c1f6f5

Browse files
committed
Merge branch 'release/0.1.0'
2 parents 490338c + ace7f8f commit 8c1f6f5

8 files changed

Lines changed: 771 additions & 44 deletions

File tree

.formatter.exs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
locals_without_parens = [field: 2, field: 3]
2+
13
[
24
inputs: [
35
"{mix,.iex,.formatter,.credo}.exs",
46
"{config,lib,test}/**/*.{ex,exs}"
57
],
6-
line_length: 80
8+
line_length: 80,
9+
locals_without_parens: locals_without_parens,
10+
export: [locals_without_parens: locals_without_parens]
711
]

.gitignore

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
1-
# The directory Mix will write compiled artifacts to.
1+
# Application artifacts (Elixir)
22
/_build/
3-
4-
# Elixir LS artifacts.
5-
/.elixir_ls/
6-
7-
# If you run "mix test --cover", coverage assets end up here.
8-
/cover/
9-
10-
# The directory Mix downloads your dependencies sources to.
113
/deps/
4+
/.fetch
5+
*.beam
6+
*.ez
7+
typed_struct-*.tar
128

13-
# Where 3rd-party dependencies like ExDoc output generated docs.
9+
# Test coverage and documentation
10+
/cover/
1411
/doc/
1512

16-
# Ignore .fetch files in case you like to edit your project deps locally.
17-
/.fetch
13+
# Editor artifacts
14+
/.elixir_ls/
15+
/.history/
1816

19-
# If the VM crashes, it generates a dump, let's ignore it too.
17+
# Crash dumps
2018
erl_crash.dump
21-
22-
# Also ignore archive artifacts (built via "mix archive.build").
23-
*.ez
24-
25-
# Ignore package tarball (built via "mix hex.build").
26-
typed_struct-*.tar

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3-
## v0.1.0-dev
3+
## v0.1.0
44

55
* Initial version
6+
* Struct definition
7+
* Type definition
8+
* Default values
9+
* Enforced keys

README.md

Lines changed: 268 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,281 @@
22

33
[![hex.pm version](http://img.shields.io/hexpm/v/typed_struct.svg?style=flat)](https://hex.pm/packages/typed_struct)
44

5-
TypedStruct is an Elixir library to define structs with a type.
5+
TypedStruct is a library for defining structs with a type without writing
6+
boilerplate code.
67

7-
*This is a work in progress, please look at the `develop` branch for ongoing
8-
development.*
8+
## Rationale
9+
10+
To define a struct in Elixir, you probably want to define three things:
11+
12+
* the struct itself, with default values,
13+
* the list of enforced keys,
14+
* its associated type.
15+
16+
It ends up in something like this:
17+
18+
```elixir
19+
defmodule Person do
20+
@moduledoc """
21+
A struct representing a person.
22+
"""
23+
24+
@enforce_keys [:name]
25+
defstruct name: nil,
26+
age: nil,
27+
happy?: true,
28+
phone: nil
29+
30+
@typedoc "A person"
31+
@type t() :: %__MODULE__{
32+
name: String.t(),
33+
age: non_neg_integer() | nil,
34+
happy?: boolean() | nil,
35+
phone: String.t() | nil
36+
}
37+
end
38+
```
39+
40+
In the example above you can notice several points:
41+
42+
* the keys are present in both the `defstruct` and type definition,
43+
* enforced keys must also be written in `@enforce_keys`,
44+
* if a key is not enforced, the type should be nullable.
45+
46+
If you want to add a field in the struct, you must therefore:
47+
48+
* add the key with its default value in the `defstruct` list,
49+
* add the key with its type in the type definition.
50+
51+
If the field is not optional, you should even add it to `@enforce_keys`. This is
52+
way too much work for lazy people like me, and moreover it can be error-prone.
53+
54+
It would be way better if we could write something like this:
55+
56+
```elixir
57+
defmodule Person do
58+
@moduledoc """
59+
A struct representing a person.
60+
"""
61+
62+
use TypedStruct
63+
64+
@typedoc "A person"
65+
typedstruct do
66+
field :name, String.t(), enforce: true
67+
field :age, non_neg_integer()
68+
field :happy?, boolean(), default: true
69+
field :phone, String.t()
70+
end
71+
end
72+
```
73+
74+
Thanks to TypedStruct, this is now possible :)
75+
76+
## Usage
77+
78+
### Setup
79+
80+
To use TypedStruct in your project, add this to you Mix dependencies:
81+
82+
```elixir
83+
{:typed_struct, "~> 0.1.0", runtime: false}
84+
```
85+
86+
If you want to avoid `mix format` putting parentheses on field definitions,
87+
you can write in your `.formatter.exs`:
88+
89+
```elixir
90+
[
91+
import_deps: [:typed_struct]
92+
]
93+
```
94+
95+
### General usage
96+
97+
To define a typed struct, use `TypedStruct`, then define your struct within a
98+
`typedstruct` block:
99+
100+
```elixir
101+
defmodule MyStruct do
102+
# Use TypedStruct to import the typedstruct macro
103+
use TypedStruct
104+
105+
# Define your struct
106+
typedstruct do
107+
# Define each field with the field macro
108+
field :a_string, String.t()
109+
110+
# You can set a default value
111+
field :string_with_default, String.t(), default: "default"
112+
113+
# You can enforce a field
114+
field :enforced_field, integer(), enforce: true
115+
end
116+
end
117+
```
118+
119+
Each field is defined through the `field/2` macro.
120+
121+
### Documentation
122+
123+
To add a `@typedoc` to the struct type, just add the attribute above the
124+
`typedstruct` block:
125+
126+
```elixir
127+
@typedoc "A typed struct"
128+
typedstruct do
129+
field :a_string, String.t()
130+
field :an_int, integer()
131+
end
132+
```
133+
134+
### Reflexion
135+
136+
To enable the use of information defined by TypedStruct by other modules, each
137+
typed struct defines three functions:
138+
139+
* `__keys__/0` - returns the keys of the struct
140+
* `__defaults__/0` - returns the default value for each field
141+
* `__types__/0` - returns the quoted type for each field
142+
143+
For instance:
144+
145+
```elixir
146+
iex(1)> defmodule Demo do
147+
...(1)> use TypedStruct
148+
...(1)>
149+
...(1)> typedstruct do
150+
...(1)> field :a_field, String.t()
151+
...(1)> field :with_default, integer(), default: 7
152+
...(1)> end
153+
...(1)> end
154+
{:module, Demo,
155+
<<70, 79, 82, 49, 0, 0, 8, 60, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 241,
156+
0, 0, 0, 24, 11, 69, 108, 105, 120, 105, 114, 46, 68, 101, 109, 111, 8, 95,
157+
95, 105, 110, 102, 111, 95, 95, 9, 102, ...>>, {:__types__, 0}}
158+
iex(2)> Demo.__keys__()
159+
[:a_field, :with_default]
160+
iex(3)> Demo.__defaults__()
161+
[a_field: nil, with_default: 7]
162+
iex(4)> Demo.__types__()
163+
[
164+
a_field: {:|, [],
165+
[
166+
{{:., [line: 5],
167+
[{:__aliases__, [line: 5, counter: -576460752303422524], [:String]}, :t]},
168+
[line: 5], []},
169+
nil
170+
]},
171+
with_default: {:|, [], [{:integer, [line: 6], []}, nil]}
172+
]
173+
```
174+
175+
## What do I get?
176+
177+
When defining an empty `typedstruct` block:
178+
179+
```elixir
180+
defmodule Example do
181+
use TypedStruct
182+
183+
typedstruct do
184+
end
185+
end
186+
```
187+
188+
you get an empty struct with its module type `t()`:
189+
190+
```elixir
191+
defmodule Example do
192+
@enforce_keys []
193+
defstruct []
194+
195+
@type t() :: %__MODULE__{}
196+
end
197+
```
198+
199+
Each `field` call adds information to the struct, `@enforce_keys` and the type
200+
`t()`.
201+
202+
A field with no options adds the name to the `defstruct` list, with `nil` as
203+
default. The type itself is made nullable:
204+
205+
```elixir
206+
defmodule Example do
207+
use TypedStruct
208+
209+
typedstruct do
210+
field :name, String.t()
211+
end
212+
end
213+
```
214+
215+
becomes:
216+
217+
```elixir
218+
defmodule Example do
219+
@enforce_keys []
220+
defstruct name: nil
221+
222+
@type t() :: %__MODULE__{
223+
name: String.t() | nil
224+
}
225+
end
226+
```
227+
228+
The `default` option adds the default value to the `defstruct`:
229+
230+
```elixir
231+
field :name, String.t(), default: "John Smith"
232+
233+
# Becomes
234+
defstruct name: "John Smith"
235+
```
236+
237+
The type itself remains the same.
238+
239+
The `enforce` option, when set to `true`, enforces the key. Therefore, the
240+
type is not nullable anymore:
241+
242+
```elixir
243+
defmodule Example do
244+
use TypedStruct
245+
246+
typedstruct do
247+
field :name, String.t(), enforce: true
248+
end
249+
end
250+
```
251+
252+
becomes:
253+
254+
```elixir
255+
defmodule Example do
256+
@enforce_keys [:name] # :name is enforced
257+
defstruct name: nil
258+
259+
@type t() :: %__MODULE__{
260+
name: String.t() # Not nullable
261+
}
262+
end
263+
```
9264

10265
## [Contributing](CONTRIBUTING.md)
11266

12267
Before contributing to this project, please read the
13268
[CONTRIBUTING.md](CONTRIBUTING.md).
14269

270+
## Roadmap
271+
272+
* [x] Struct definition
273+
* [x] Type definition (with nullable types)
274+
* [x] Default values
275+
* [x] Enforced keys (non-nullable types)
276+
* [ ] Default value type-checking (is it possible?)
277+
* [ ] Guard generation
278+
* [ ] Ecto integration
279+
15280
## License
16281

17282
Copyright © 2018 Jean-Philippe Cugnet

0 commit comments

Comments
 (0)