|
2 | 2 |
|
3 | 3 | [](https://hex.pm/packages/typed_struct) |
4 | 4 |
|
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. |
6 | 7 |
|
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 | +``` |
9 | 264 |
|
10 | 265 | ## [Contributing](CONTRIBUTING.md) |
11 | 266 |
|
12 | 267 | Before contributing to this project, please read the |
13 | 268 | [CONTRIBUTING.md](CONTRIBUTING.md). |
14 | 269 |
|
| 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 | + |
15 | 280 | ## License |
16 | 281 |
|
17 | 282 | Copyright © 2018 Jean-Philippe Cugnet |
|
0 commit comments