Packages

Declarative Ecto embedded schemas for data validation, coercion, and manipulation.

Current section

12 Versions

Jump to

Compare versions

18 files changed
+1486 additions
-669 deletions
  @@ -14,6 +14,12 @@
14 14 embeds_one!: 4,
15 15 embeds_many!: 2,
16 16 embeds_many!: 3,
17 - embeds_many!: 4
18 - ]
17 + embeds_many!: 4,
18 + # Extensions (Spark)
19 + attribute: 1,
20 + attribute: 2,
21 + option: 1,
22 + option: 2
23 + ],
24 + plugins: [Spark.Formatter]
19 25 ]
  @@ -14,13 +14,14 @@ Declarative [`Ecto`](https://github.com/elixir-ecto/ecto) `embedded_schema`s for
14 14 * Colocated input transformations let you either transform input fields before validation or derive field values from other fields ([Derived Fields / Input Transformations](#derived-fields--input-transformations))
15 15 * Colocated validations, so you can define common validations alongside field declarations ([Validations](#field-validations))
16 16 * Colocated output transformations let you transform fields after validation ([Mappings / Output Transformations](#mappings--output-transformations))
17 - * Adds `Access` implementation to all schemas
18 - * Adds `Jason.Encoder` implementation to all schemas
17 + * Extensible using the `Flint.Extension` module. Default extensions include:
18 + * `Accessible` - Adds `Access` implementation to the target schemas
19 + * `JSON` - Adds a custom JSON encoding (`Jason` and `Poison` supported) implementation to the target schemas
20 + * `Embedded` - Sets good default module attribute values used by `Ecto` specifically tailored for in-memory embedded schemas
21 + * And more!
19 22 * New [`Ecto.Schema` Reflection Functions](https://hexdocs.pm/ecto/Ecto.Schema.html#module-reflection)
20 23 * `__schema__(:required)` - Returns list of fields marked as required (from `!` macros)
21 - * `__schema__(:pre_transforms` - `Keyword` mapping of fields to pre-transformations (currently only `:derive` option)
22 - * `__schema__(:validations)` - `Keyword` mapping of fields to validations
23 - * `__schema__(:post_transforms` - `Keyword` mapping of fields to post-transformations (currently only `:map` option)
24 + * And more!
24 25 * Convenient generated function (`changeset`,`new`,`new!`,...) ([Generated Functions](#generated-functions))
25 26 * Configurable `Application`-wide defaults for `Ecto.Schema` API ([Config](#config))
26 27 * Conveniently create new `Ecto` types using the `Flint.Type` module and its `deftype/2` macro ([`Flint.Type`](#flinttype))
  @@ -30,7 +31,7 @@ Declarative [`Ecto`](https://github.com/elixir-ecto/ecto) `embedded_schema`s for
30 31 ```elixir
31 32 def deps do
32 33 [
33 - {:flint, "~> 0.1"}
34 + {:flint, "~> 0.4"}
34 35 ]
35 36 end
36 37 ```
  @@ -51,22 +52,12 @@ This is useful if you want to make changes in the server-side code without needi
51 52
52 53 ## Basic Usage
53 54
54 - If you want to declare a schema with `Flint`, just `use Flint` within your module, and now you have access to `Flint`'s implementation of the
55 + If you want to declare a schema with `Flint`, just `use Flint.Schema` within your module, and now you have access to `Flint`'s implementation of the
55 56 `embedded_schema/1` macro. You can declare an `embedded_schema` within your module as you otherwise would with `Ecto`. Within the `embedded_schema/1` block, you also have access to `Flint`s implementations of `embeds_one`,`embeds_one!`,`embeds_many`, `embeds_many!`, `field`, and `field!`.
56 57
57 - You can also use the shorthand notation, where you pass in your schema definition as an argument to the `use/2` macro. `Flint.__using__/1` also
58 - accepts the following options which will be passed as module attributes to the `Ecto` `embedded_schema`. Refer to the [`Ecto.Schema`](https://hexdocs.pm/ecto/Ecto.Schema.html#module-schema-attributes) docs for more about these options.
59 -
60 - * `primary_key` (default `false`)
61 - * `schema_prefix` (default `nil`)
62 - * `schema_context` (default `nil`)
63 - * `timestamp_opts` (default `[type: :naive_datetime]`)
64 -
65 - So these two are equivalent:
66 -
67 58 ```elixir
68 59 defmodule User do
69 - use Flint
60 + use Flint.Schema
70 61
71 62 embedded_schema do
72 63 field! :username, :string
  @@ -76,24 +67,6 @@ defmodule User do
76 67 end
77 68 ```
78 69
79 - is equivalent to:
80 -
81 - ```elixir
82 - defmodule User do
83 - use Flint, schema: [
84 - field!(:username, :string)
85 - field!(:password, :string, redacted: true)
86 - field(:nickname, :string)
87 - ]
88 - end
89 - ```
90 -
91 - If you're starting with `Flint` and you know you will stick with it, the shorthand might make more sense. But if you want to be able to quickly
92 - change between `use Ecto.Schema` and `use Flint`, or you're converting some existing `Ecto` `embedded_schema`s to `Flint`, the latter might be
93 - preferable.
94 -
95 - Since a call to `Flint`'s `embedded_schema` or `use Flint, schema: []` just creates an `Ecto` `embedded_schema` you can use them just as you would any other `Ecto` schemas. You can compose them, apply changesets to them, etc.
96 -
97 70 ## `Flint` Types
98 71
99 72 `Flint` also comes with some types that are automatically aliased when you `use Flint`.
  @@ -104,42 +77,41 @@ Union type for Ecto. Allows the field to be any of the specified types.
104 77
105 78 ## `Flint.Type`
106 79
107 - `Flint.Type` is meant to make writing new `Ecto` types require much less boilerplate, because you can base your
108 - type off of an existing type and only modify the callbacks that have different behavior.
80 + `Flint.Type` is meant to make writing new `Ecto` types require much less boilerplate, because you can base your type off of an existing type and only modify the callbacks that have different behavior.
109 81
110 - Simply `use Flint.Type` and pass the `:extends` option which says which type module to inherit callbacks
111 - from. This will delegate all required callbacks and any implemented optional callbacks and make them
112 - overridable.
82 + Simply `use Flint.Type` and pass the `:extends` option which says which type module to inherit callbacks
83 + from. This will delegate all required callbacks and any implemented optional callbacks and make them
84 + overridable.
113 85
114 - It also lets you make a type from an `Ecto.ParameterizedType` with default parameter values.
115 - You may supply any number of default parameters. This essentially provides a new
116 - `init/1` implementation for the type, supplying the default values, while not affecting any of the
117 - other `Ecto.ParameterizedType` callbacks. You may still override the newly set defaults at the local level.
86 + It also lets you make a type from an `Ecto.ParameterizedType` with default parameter values.
87 + You may supply any number of default parameters. This essentially provides a new
88 + `init/1` implementation for the type, supplying the default values, while not affecting any of the
89 + other `Ecto.ParameterizedType` callbacks. You may still override the newly set defaults at the local level.
118 90
119 - Just supply all options that you wish to be defaults as extra options when using `Flint.Type`.
91 + Just supply all options that you wish to be defaults as extra options when using `Flint.Type`.
120 92
121 - You may override any of the inherited callbacks inherity from the extended module
122 - in the case that you wish to customize the module further.
93 + You may override any of the inherited callbacks inherity from the extended module
94 + in the case that you wish to customize the module further.
123 95
124 96 ### Examples
125 97
126 - ``` elixir
127 - defmodule Category do
128 - use Flint.Type, extends: Ecto.Enum, values: [:folder, :file]
129 - end
130 - ```
98 + ``` elixir
99 + defmodule Category do
100 + use Flint.Type, extends: Ecto.Enum, values: [:folder, :file]
101 + end
102 + ```
131 103
132 - This will apply default `values` to `Ecto.Enum` when you supply a `Category` type
133 - to an Ecto schema. You may still override the values if you supply the `:values`
134 - option for the field.
104 + This will apply default `values` to `Ecto.Enum` when you supply a `Category` type
105 + to an Ecto schema. You may still override the values if you supply the `:values`
106 + option for the field.
135 107
136 - ```elixir
137 - import Flint.Type
138 - deftype NewUID, extends: Ecto.UUID, dump: &String.length/1
139 - ```
108 + ```elixir
109 + import Flint.Type
110 + deftype NewUID, extends: Ecto.UUID, dump: &String.length/1
111 + ```
140 112
141 - This will create a new `NewUID` type that behaves exactly like an `Ecto.UUID` except it dumps
142 - its string length.
113 + This will create a new `NewUID` type that behaves exactly like an `Ecto.UUID` except it dumps
114 + its string length.
143 115
144 116 ## Generated Functions
145 117
  @@ -149,36 +121,28 @@ Union type for Ecto. Allows the field to be any of the specified types.
149 121 * `new` - Creates a new changeset from the empty module struct and applies the changes (regardless of whether the changeset was valid).
150 122 * `new!` - Same as new, except raises if the changeset is not valid.
151 123
152 - ### Pipeline
124 + ## Flint Core
153 125
154 - At their core, the new `field` and `field!` macros' only additional functionality over the default `Ecto` macros
155 - is to store the allowed `Flint` options into module attributes which are exposed as new reflection functions.
126 + The core of Flint is the additional schema macros, which includes the bang (`!`) variants to mark
127 + a field as required, and the added support of validations through `do` blocks to fields, as well
128 + as the `Flint.Extension` API that allows extensions to define additional acceptable `field` options
129 + and module attributes that can be reflected upon.
156 130
157 - The bulk of the work done in Flint with validations and transformations of data occurs in the generated `changeset`
158 - function, which leaves it up to the end user whether to use the default implementation, roll their own from scratch
159 - using the information exposed through the reflection functions, or do something in between (such as using the `Flint.Pipeline` APIs).
131 + All other functionality comes in the form of `Flint` extensions.
160 132
161 - When you `use Flint`, you declare an overridable `changeset` function for your schema module that by default just
162 - delegates to the `Flint.Pipeline.changeset/3` function.
133 + At their core, the new `field` and `field!` macros' only additional functionality over the default `Ecto` macros is to store the allowed `Flint` options into module attributes which are exposed as new reflection functions.
163 134
164 - The `Flint.Pipeline.changeset/3` function operates as the following pipeline:
135 + The bulk of the work done in Flint with validations and transformations of data occurs in the generated `changeset` function, which leaves it up to the end user whether to use the default implementation, roll their own from scratch using the information exposed through the reflection functions, or do something in between (such as tuning which extensions you use).
136 +
137 + When you `use Flint.Schema`, you declare an overridable `changeset` function for your schema module that by default just
138 + delegates to the `Flint.Changeset.changeset/3` function.
139 +
140 + The `Flint.Changeset.changeset/3` function operates as the following pipeline:
165 141
166 142 1. Cast all fields (including embeds)
167 143 2. Validate required fields ([Required Fields](#required-fields))
168 - 3. Apply pre-transformations ([Derived Fields / Input Transformations](#derived-fields--input-transformations))
169 - 4. Apply field validations ([Validations](#field-validations))
170 - 5. Apply post-transformations ([Mappings / Output Transformations](#mappings--output-transformations))
171 144
172 - If you wish to compose your own `changeset` function, each of these steps has its own API, either from `Ecto` itself
173 - or exposed through `Flint`:
174 -
175 - 1. [`Ecto.Changeset.cast/4`](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast/4) / [`Ecto.Changeset.cast_embed/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_embed/3)
176 - 2. [`Ecto.Changeset.validate_required/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_required/3)
177 - 3. `Flint.Pipeline.apply_pre_transforms/2`
178 - 4. `Flint.Pipeline.apply_validations/2`
179 - 5. `Flint.Pipeline.apply_post_transforms/2`
180 -
181 - ## Required Fields
145 + ### Required Fields
182 146
183 147 `Flint` adds the convenience bang (`!`) macros (`embed_one!`,`embed_many!`, `field!`) for field declarations within your struct to declare a field as required within its `changeset` function.
184 148
  @@ -186,173 +150,11 @@ or exposed through `Flint`:
186 150
187 151 * `__schema__(:required)` -- Returns a list of all fields that were marked as required.
188 152
189 - ## Derived Fields / Input Transformations
153 + ### Field Validations
190 154
191 - `Flint` provides a convenient `:derive` option to express how the field is computed.
155 + #### `field` `do` Blocks
192 156
193 - **This occurs after casting and before validations.**
194 -
195 - Much like the [previous section](#validate-with-respect-to-other-fields), `derived` fields let you define
196 - expressions with support for custom bindings to include any `field` declarations that occur before the current field.
197 -
198 - `:derive` will automatically put the result of the input expression into the field value. This occurs before
199 - any other validation, so you can still have access to `field` bindings and even the current computed field value
200 - within a `:when` validation.
201 -
202 - You can define a `derived` field with respect to the field itself, in which case it acts as transformation. Typically in
203 - `Ecto`, incoming transformations of this support would happen at the `cast` step, which means the behavior is determined
204 - by the type in which you are casting into. `:derive` lets you apply a transformation after casting to change that behavior
205 - without changing the underlying allowed type.
206 -
207 - You can also define a `derived` field with an expression that does not depend on the field, in which case it is
208 - suggested that you use the `field` macro instead of `field!` since any input in that case would be thrashed by
209 - the derived value. This means that a field can be completely determined as a product of other fields!
210 -
211 - ```elixir
212 - defmodule Test do
213 - use Flint
214 -
215 - embedded_schema do
216 - field! :category, Union, oneof: [Ecto.Enum, :decimal, :integer], values: [a: 1, b: 2, c: 3]
217 - field! :rating, :integer, when: category == target_category
218 - field :score, derive: rating + category, :integer, gt: 1, lt: 100, when: score > rating
219 - end
220 - end
221 - ```
222 -
223 - ```elixir
224 - Test.new!(%{category: 1, rating: 80}, target_category: 1)
225 -
226 - # %Test{category: 1, rating: 80, score: 81}
227 - ```
228 -
229 - ## Field Validations
230 -
231 - ### Basic Validations
232 -
233 - `Flint` allows you to colocate schema definitions and validations.
234 -
235 - ```elixir
236 - defmodule Person do
237 - use Flint
238 -
239 - embedded_schema do
240 - field! :first_name, :string, max: 10, min: 5
241 - field! :last_name, :string, min: 5, max: 10
242 - field :favorite_colors, {:array, :string}, subset_of: ["red", "blue", "green"]
243 - field! :age, :integer, greater_than: 0, less_than: 100
244 - end
245 - end
246 - ```
247 -
248 - ### Parameterized Validations
249 -
250 - You can even parameterize the options passed to the validations:
251 -
252 - ```elixir
253 - defmodule Person do
254 - use Flint
255 -
256 - embedded_schema do
257 - field! :first_name, :string, max: 10, min: 5
258 - field! :last_name, :string, min: 5, max: 10
259 - field :favorite_colors, {:array, :string}, subset_of: ["red", "blue", "green"]
260 - field! :age, :integer, greater_than: 0, less_than: max_age
261 - end
262 - end
263 - ```
264 -
265 - If you do this, make sure to pass the options as a keyword list into the call to `changeset`:
266 -
267 - ```elixir
268 - Person.changeset(
269 - %Person{},
270 - %{first_name: "Bob", last_name: "Smith", favorite_colors: ["red", "blue", "pink"], age: 101},
271 - [max_age: 100]
272 - )
273 - ```
274 -
275 - ```elixir
276 - #Ecto.Changeset<
277 - action: nil,
278 - changes: %{
279 - age: 101,
280 - first_name: "Bob",
281 - last_name: "Smith",
282 - favorite_colors: ["red", "blue", "pink"]
283 - },
284 - errors: [
285 - first_name: {"should be at least %{count} character(s)",
286 - [count: 5, validation: :length, kind: :min, type: :string]},
287 - favorite_colors: {"has an invalid entry", [validation: :subset, enum: ["red", "blue", "green"]]},
288 - age: {"must be less than %{number}", [validation: :number, kind: :less_than, number: 100]}
289 - ],
290 - data: #Person<>,
291 - valid?: false,
292 - ...
293 - >
294 - ```
295 -
296 - ### Validate With Respect to Other Fields
297 -
298 - You might find yourself wishing to validate a field conditionally based on the values of other fields. In `Flint`, you
299 - can do this with any validation! Since all validations already accept parameterized conditions, they also let you refer
300 - to previously defined fields declared with `field` or `field!` macros. Just use a variable of the same name as the field(s) you want to refer to, and they will be bound to their respective variables.
301 -
302 - Additionally, `:when` lets you define an arbitrary boolean expression that will be evaluated and pass the validation if it
303 - evaluates to a truthy value. You may pass bindings to this condition just as explained [above](#parameterized-validations), and
304 - refer to previously defined fields as just discussed, but uniquely, `:when` also lets you refer to the current `field` in which
305 - the `:when` condition is defined. Theoretically, you could write many of the other validations using `:when`, but you will
306 - receive worse error messages with `:when` than with the dedicated validations.
307 -
308 - ```elixir
309 - defmodule Test do
310 - use Flint
311 -
312 - embedded_schema do
313 - field! :category, Union, oneof: [Ecto.Enum, :decimal, :integer], values: [a: 1, b: 2, c: 3]
314 - field! :rating, :integer, when: category == target_category
315 - field! :score, :integer, gt: 1, lt: 100, when: score > rating
316 - end
317 - end
318 - ```
319 -
320 - ```elixir
321 - > Test.new!(%{category: :a, rating: 80, score: 10}, target_category: :a)
322 -
323 - ** (ArgumentError) %Test{category: :a, rating: 80, score: ["Failed `:when` validation"]}
324 - (flint 0.0.1) lib/schema.ex:406: Flint.Schema.new!/3
325 - (elixir 1.15.7) src/elixir.erl:396: :elixir.eval_external_handler/3
326 - (stdlib 5.1.1) erl_eval.erl:750: :erl_eval.do_apply/7
327 - (elixir 1.15.7) src/elixir.erl:375: :elixir.eval_forms/4
328 - (elixir 1.15.7) lib/module/parallel_checker.ex:112: Module.ParallelChecker.verify/1
329 - lib/livebook/runtime/evaluator.ex:622: anonymous fn/3 in Livebook.Runtime.Evaluator.eval/4
330 - (elixir 1.15.7) lib/code.ex:574: Code.with_diagnostics/2
331 - ```
332 -
333 - ### Basic Validation Options
334 -
335 - `Flint` provides some shorthand options for common validation functions (mostly taken from `Ecto.Changeset`)
336 -
337 - * `:greater_than` ([`Ecto.Changeset.validate_number/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_number/3-options))
338 - * `:less_than` ([`Ecto.Changeset.validate_number/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_number/3-options))
339 - * `:less_than_or_equal_to` ([`Ecto.Changeset.validate_number/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_number/3-options))
340 - * `:greater_than_or_equal_to` ([`Ecto.Changeset.validate_number/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_number/3-options))
341 - * `:equal_to` ([`Ecto.Changeset.validate_number/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_number/3-options))
342 - * `:not_equal_to` ([`Ecto.Changeset.validate_number/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_number/3-options))
343 - * `:format` ([`Ecto.Changeset.validate_format/4`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_format/4))
344 - * `:subset_of` ([`Ecto.Changeset.validate_subset/4`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_subset/4))
345 - * `:in` ([`Ecto.Changeset.validate_inlusion/4`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_inclusion/4))
346 - * `:not_in` ([`Ecto.Changeset.validate_exclusion/4`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_exclusion/4))
347 - * `:is` ([`Ecto.Changeset.validate_length/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_length/3-options))
348 - * `:min` ([`Ecto.Changeset.validate_length/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_length/3-options))
349 - * `:max` ([`Ecto.Changeset.validate_length/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_length/3-options))
350 - * `:count` ([`Ecto.Changeset.validate_length/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_length/3-options))
351 - * `:when` - Let's you define an arbitrary boolean condition on the field which can refer to any `field` defined above it or itself. **NOTE** The `:when` option will output a generic error on failure, so if verbosity is desired, an [advanced validation](#advanced-validations) is more appropriate.
352 -
353 - ### Advanced Validations
354 -
355 - In `Flint`, the `field` and `field!` macros also now accept an optional `do` block to define condition/error pairs.
157 + In `Flint`, the `field` and `field!` macros now accept an optional `do` block to define condition/error pairs.
356 158
357 159 ```elixir
358 160 embedded_schema do
  @@ -395,66 +197,128 @@ The `:do` block accepts a list of validation clauses, where is clause is of the
395 197 `failure condition -> Error Message`
396 198
397 199 In the `:do` block expressions, the same rules apply as mentioned across this documentation. You can pass
398 - bindings to apply to the expression, and field name bindings will automatically be passed to the expression
399 - so you can just use the field names as variables.
200 + bindings to apply to the expression, and field name bindings will automatically be passed to the expression so you can just use the field names as variables.
400 201
401 - Additionally, you will have access to all local functions and imported functions that the parent module would
402 - have, so you can write expressions as you would in the parent module.
202 + Additionally, you will have access to all local functions and imported functions that the parent module would have, so you can write expressions as you would in the parent module.
403 203
404 - ### `__schema__(:validations)`
405 -
406 - Since validations are enforced through the generated `changeset` functions, if you override this function you will not get the benefits
407 - of the validations.
408 -
409 - If you want to implement your own, you can use `__schema__(:validations)` which is an added reflection function that stores validations.
410 -
411 - **NOTE** These are stored as their quoted representation to support passing bindings, so make sure to account for this if implementing yourself.
412 -
413 - If you want to override `changeset` but want to keep the default validation behavior, there is also the `Flint.Schema.validate_fields` function,
414 - which accepts an `%Ecto.Changetset{}` and optionally bindings, and performs validations using the information stored in `__schema__(:validations)`.
415 -
416 - ## Mappings / Output Transformations
417 -
418 - `Flint` also lets you declare a mapping to apply to the field after all validations. The same caveats apply to the
419 - `:map` expression as all other expressions, with the exception that the `:map` function **only** accepts arity-1 anonymous functions
420 - or non-anonymous function expressions (eg. using variable replacement).
421 -
422 - In the following example, `computed` is used to normalize incoming strings to downcase to prepare for the validation, then the output
423 - is mapped to the uppercase string using the `:map` option.
204 + The AST representations of these vaildations are stored in a module attribute and can be retrieved using
424 205
425 206 ```elixir
426 - defmodule Character do
427 - use Flint
428 -
207 + __schema__(:blocks)
208 + ```
209 +
210 + These validations are checked in the default `Flint.Changeset.changeset`.
211 +
212 + #### Parameterized Validations
213 +
214 + You can even parameterize the options passed to the validations:
215 +
216 + ```elixir
217 + defmodule Person do
218 + use Flint.Schema
219 +
429 220 embedded_schema do
430 - field! :type, :string, derive: &String.downcase/1, map: String.upcase(type) do
431 - type not in ~w[elf human] -> "Expected elf or human, got: #{type}"
432 - end
433 -
434 - field! :age, :integer do
435 - age < 0 ->
436 - "Nobody can have a negative age"
437 -
438 - type == "elf" and age > max_elf_age ->
439 - "Attention! The elf has become a bug! Should be dead already!"
440 -
441 - type == "human" and age > max_human_age ->
442 - "Expected human to have up to #{max_human_age}, got: #{age}"
443 - end
221 + field! :first_name, :string, max: 10, min: 5
222 + field! :last_name, :string, min: 5, max: 10
223 + field :favorite_colors, {:array, :string}, subset_of: ["red", "blue", "green"]
224 + field! :age, :integer, greater_than: 0, less_than: max_age
444 225 end
445 226 end
446 227 ```
447 228
229 + If you do this, make sure to pass the options as a keyword list into the call to `changeset`:
230 +
448 231 ```elixir
449 - max_elf_age = 400
450 - max_human_age = 120
451 - Character.new!(%{type: "Elf", age: 10}, binding())
232 + Person.changeset(
233 + %Person{},
234 + %{first_name: "Bob", last_name: "Smith", favorite_colors: ["red", "blue", "pink"], age: 101},
235 + [max_age: 100]
236 + )
452 237 ```
453 238
454 239 ```elixir
455 - %Character{type: "ELF", age: 10}
240 + #Ecto.Changeset<
241 + action: nil,
242 + changes: %{
243 + age: 101,
244 + first_name: "Bob",
245 + last_name: "Smith",
246 + favorite_colors: ["red", "blue", "pink"]
247 + },
248 + errors: [
249 + first_name: {"should be at least %{count} character(s)",
250 + [count: 5, validation: :length, kind: :min, type: :string]},
251 + favorite_colors: {"has an invalid entry", [validation: :subset, enum: ["red", "blue", "green"]]},
252 + age: {"must be less than %{number}", [validation: :number, kind: :less_than, number: 100]}
253 + ],
254 + data: #Person<>,
255 + valid?: false,
256 + ...
257 + >
456 258 ```
457 259
260 + #### Validate With Respect to Other Fields
261 +
262 + You might find yourself wishing to validate a field conditionally based on the values of other fields. In `Flint`, you
263 + can do this with any validation! Since all validations already accept parameterized conditions, they also let you refer
264 + to previously defined fields declared with `field` or `field!` macros. Just use a variable of the same name as the field(s) you want to refer to, and they will be bound to their respective variables.
265 +
266 + ## Extensions
267 +
268 + Flint provides an extensible architecture using the `Flint.Extension` module.
269 +
270 + In fact, most of the core features that `Flint` offers are written as `Flint` extensions.
271 +
272 + The default extensions can be retrieved using `Flint.default_extensions()`
273 +
274 + ```elixir
275 + Flint.default_extensions()
276 +
277 + [
278 + Flint.Extensions.PreTransforms,
279 + Flint.Extensions.When,
280 + Flint.Extensions.EctoValidations,
281 + Flint.Extensions.PostTransforms,
282 + Flint.Extensions.Accessible,
283 + Flint.Extensions.Embedded,
284 + Flint.Extensions.JSON
285 + ]
286 + ```
287 +
288 + To use extensions, you can specify them when using `Flint.Schema` in your module. If the `:extensions` option
289 + is not provided, the default extensions will be used.
290 +
291 + ```elixir
292 + defmodule MySchema do
293 + use Flint.Schema,
294 + extensions: [Accessible, JSON]
295 +
296 + embedded_schema do
297 + # Schema fields...
298 + end
299 + end
300 + ```
301 +
302 + **Note that you don't have to write the fully-qualified name for modules in the `Flint.Extensions` module.**
303 +
304 + You can use `Flint.default_extensions()` to refer to the default extensions, which you will have to
305 + explicitly add if you pass custom values to the `:extensions` option when using `Flint.Schema`, eg.
306 +
307 + ```elixir
308 + defmodule MySchema do
309 + use Flint.Schema,
310 + extensions: Flint.default_extensions() ++ [MyExtension]
311 +
312 + embedded_schema do
313 + # Schema fields...
314 + end
315 + end
316 + ```
317 +
318 + You can also create custom extensions by `use`ing `Flint.Extension`. This allows you to add additional functionality or modify the behavior of Flint schemas according to your specific needs.
319 +
320 + For more details on creating and using extensions, refer to the `Flint.Extension` module documentation.
321 +
458 322 ## Aliases
459 323
460 324 If you don't like the name of an option, you can provide a compile-time list of aliases to map new option names to existing options
  @@ -499,7 +363,7 @@ be the embedded representation and will have the values be the dumped representa
499 363
500 364 ```elixir
501 365 defmodule Book do
502 - use Flint, schema: [
366 + use Flint.Schema, schema: [
503 367 field(:genre, Ecto.Enum, values: [biography: 0, science_fiction: 1, fantasy: 2, mystery: 3])
504 368 ]
505 369 end
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/acalejos/flint">>}]}.
2 2 {<<"name">>,<<"flint">>}.
3 - {<<"version">>,<<"0.3.1">>}.
3 + {<<"version">>,<<"0.4.0">>}.
4 4 {<<"description">>,
5 5 <<"Declarative Ecto embedded schemas for data validation, coercion, and manipulation.">>}.
6 6 {<<"elixir">>,<<"~> 1.14">>}.
  @@ -12,14 +12,31 @@
12 12 {<<"optional">>,false},
13 13 {<<"requirement">>,<<"~> 3.12">>},
14 14 {<<"repository">>,<<"hexpm">>}],
15 + [{<<"name">>,<<"spark">>},
16 + {<<"app">>,<<"spark">>},
17 + {<<"optional">>,false},
18 + {<<"requirement">>,<<"~> 2.2">>},
19 + {<<"repository">>,<<"hexpm">>}],
15 20 [{<<"name">>,<<"jason">>},
16 21 {<<"app">>,<<"jason">>},
17 22 {<<"optional">>,true},
18 23 {<<"requirement">>,<<"~> 1.4">>},
24 + {<<"repository">>,<<"hexpm">>}],
25 + [{<<"name">>,<<"poison">>},
26 + {<<"app">>,<<"poison">>},
27 + {<<"optional">>,true},
28 + {<<"requirement">>,<<"~> 6.0">>},
19 29 {<<"repository">>,<<"hexpm">>}]]}.
20 30 {<<"files">>,
21 - [<<"lib">>,<<"lib/type.ex">>,<<"lib/schema.ex">>,<<"lib/flint.ex">>,
22 - <<"lib/flint">>,<<"lib/flint/types">>,<<"lib/flint/types/union.ex">>,
23 - <<"lib/pipeline.ex">>,<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,
24 - <<"LICENSE.md">>]}.
31 + [<<"lib">>,<<"lib/type.ex">>,<<"lib/changeset.ex">>,<<"lib/schema.ex">>,
32 + <<"lib/flint.ex">>,<<"lib/flint">>,<<"lib/flint/types">>,
33 + <<"lib/flint/types/union.ex">>,<<"lib/flint/extension">>,
34 + <<"lib/flint/extension/field.ex">>,<<"lib/flint/extension/dsl.ex">>,
35 + <<"lib/flint/extensions">>,<<"lib/flint/extensions/pre_transforms.ex">>,
36 + <<"lib/flint/extensions/post_transforms.ex">>,
37 + <<"lib/flint/extensions/ecto_validations.ex">>,
38 + <<"lib/flint/extensions/when.ex">>,<<"lib/flint/extensions/json.ex">>,
39 + <<"lib/flint/extensions/embedded.ex">>,
40 + <<"lib/flint/extensions/accessible.ex">>,<<"lib/extension.ex">>,
41 + <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>]}.
25 42 {<<"build_tools">>,[<<"mix">>]}.
  @@ -0,0 +1,115 @@
1 + defmodule Flint.Changeset do
2 + @moduledoc """
3 + The base `changeset` function defined by `Flint`. `Flint.Changeset` uses the module attributes
4 + that are collected when using the `Flint.Schema` macros to perform transformations and validations.
5 + """
6 + import Ecto.Changeset
7 +
8 + @doc """
9 + Uses the quoted expressions from the `Flint.Schema.field` and `Flint.Schema.field!`
10 + `do` blocks to validate the changeset.
11 +
12 + You can optionally pass bindings to be added to the evaluation context.
13 + """
14 + def validate_do_blocks(changeset, bindings \\ []) do
15 + module = changeset.data.__struct__
16 + env = Module.concat(module, Env) |> apply(:env, [])
17 +
18 + all_validations =
19 + module.__schema__(:blocks)
20 +
21 + for {field, validations} <- all_validations, reduce: changeset do
22 + changeset ->
23 + bindings = bindings ++ Enum.into(changeset.changes, [])
24 + block = Keyword.get(validations, :block, [])
25 +
26 + block
27 + |> Enum.with_index()
28 + |> Enum.reduce(changeset, fn
29 + {{quoted_condition, quoted_err}, index}, chngset ->
30 + try do
31 + {invalid?, _bindings} = Code.eval_quoted(quoted_condition, bindings, env)
32 +
33 + invalid? =
34 + if is_function(invalid?) do
35 + case Function.info(invalid?, :arity) do
36 + {:arity, 0} ->
37 + apply(invalid?, [])
38 +
39 + {:arity, 1} when not is_nil(field) ->
40 + apply(invalid?, [fetch_change!(changeset, field)])
41 +
42 + _ ->
43 + raise ArgumentError,
44 + "Anonymous functions in validation clause must be either 0-arity or an input value for the field must be provided."
45 + end
46 + else
47 + invalid?
48 + end
49 +
50 + {err_msg, _bindings} = Code.eval_quoted(quoted_err, bindings, env)
51 +
52 + if invalid? do
53 + add_error(chngset, field, err_msg,
54 + validation: :block,
55 + clause: index + 1
56 + )
57 + else
58 + chngset
59 + end
60 + rescue
61 + _ ->
62 + add_error(
63 + chngset,
64 + field,
65 + "Error evaluating expression in Clause ##{index + 1} of `do:` block"
66 + )
67 + end
68 + end)
69 + end
70 + end
71 +
72 + @doc """
73 + Given a `Flint` (or `Ecto`) schema and params (can be a map, struct of the given schema, or an existing changeset),
74 + applies all steps of the `Flint.Changeset` to generate a new changeset.
75 +
76 + This function casts all fields (recursively casting all embeds using this same function),
77 + validates required fields (specified using the bang (`!`) macros exposed by `Flint`),
78 + outputting the resulting `Ecto.Changeset`.
79 + """
80 + def changeset(schema, params \\ %{}, bindings \\ []) do
81 + module = schema.__struct__
82 + fields = module.__schema__(:fields) |> MapSet.new()
83 + embedded_fields = module.__schema__(:embeds) |> MapSet.new()
84 +
85 + params =
86 + case params do
87 + %Ecto.Changeset{params: params} -> params
88 + s when is_struct(s) -> Map.from_struct(params)
89 + _ -> params
90 + end
91 +
92 + required = module.__schema__(:required)
93 + fields = fields |> MapSet.difference(embedded_fields)
94 + required_embeds = Enum.filter(required, &(&1 in embedded_fields))
95 + required_fields = Enum.filter(required, &(&1 in fields))
96 +
97 + changeset =
98 + schema
99 + |> cast(params, fields |> MapSet.to_list())
100 +
101 + changeset =
102 + for field <- embedded_fields, reduce: changeset do
103 + changeset ->
104 + changeset
105 + |> cast_embed(field,
106 + required: field in required_embeds,
107 + with: &changeset(&1, &2, bindings)
108 + )
109 + end
110 +
111 + changeset
112 + |> validate_required(required_fields)
113 + |> validate_do_blocks(bindings)
114 + end
115 + end
  @@ -0,0 +1,147 @@
1 + defmodule Flint.Extension do
2 + @moduledoc """
3 + `Flint` extensions allow developers to easily hook into `Flint` metaprogramming lifecycle to add extra data into the embedded
4 + schema reflection functions.
5 +
6 + Flint currently offers three ways to extend behavior:
7 +
8 + 1. Schema-level attributes
9 + 2. Field-level additional options
10 + 3. Injected Code
11 +
12 + Extension authors define what fields / options / attributes `Flint` should look for in the module / schema definition and
13 + strip out and store in a schema reflection function, but it is still the resposibility of either the extension author or
14 + the end user to make use of the stored information.
15 +
16 + ## Schema-Level Attributes
17 +
18 + These are simply module attributes that are pre-registered with `Flint`, and can be given a default value
19 + as well as a validation function. When you use an extension that registers an attribute, then a new `__schema__`
20 + reflection function is added for each attribute name, with the attribute name as the argument.
21 +
22 + **Note that the validation occurs at compile time**
23 +
24 + ### Example
25 +
26 + Given the following extension:
27 +
28 + ```elixir
29 + defmodule Returnable do
30 + use Flint.Extension
31 +
32 + attribute :returns, validator: fn returns -> is_binary(returns) end
33 + end
34 + ```
35 +
36 + And the schema
37 +
38 + ```elixir
39 + defmodule Schema do
40 + use Flint.Schema, extensions: [Returnable]
41 + @returns "something"
42 + embedded_schema do
43 + ...
44 + end
45 + end
46 + ```
47 +
48 + Then you can reflect on this new attribute with:
49 +
50 + ```elixir
51 + Schema.__schema__(:returns)
52 + ```
53 +
54 + ## Field-Level Options
55 +
56 + You can also register additional field-level keyword options to be consumed in a downstream function.
57 +
58 + These function similarly to the built-in extra options that `Flint` provides, where the options are
59 + stripped and stored in a module attribute (and subsequently in a `__schema__` reflection function)
60 + before passing the valid `Ecto.Schema` options to `Ecto` itself.
61 +
62 + **Note that the validation occurs at compile time**
63 +
64 + ### Example
65 +
66 + Given the following extension that enables Go-like JSON marshalling options:
67 +
68 + ```elixir
69 + defmodule JSON do
70 + use Flint.Extension
71 +
72 + option :name, required: false, validator: &is_binary/1
73 + option :omitempty, required: false, default: false, validator: &is_boolean/1
74 + option :ignore, required: false, default: false, validator: &is_boolean/1
75 + end
76 + ```
77 +
78 + And the following schema:
79 +
80 + ```elixir
81 + defmodule Schema do
82 + use Flint.Schema, extensions: [JSON]
83 + embedded_schema do
84 + field :myfield, :string, name: "my_field", omitempty: true
85 + end
86 + end
87 + ```
88 +
89 + Then you can access these specific fields with:
90 +
91 + ```elixir
92 + Schema.__schema__(:extra_options)
93 + ```
94 +
95 + ```elixir
96 + [
97 + myfield: [ignore: false, omitempty: true, name: "my_field"],
98 + ]
99 + ```
100 +
101 + ## Injected Code
102 +
103 + Lastly, extensions allow you to define custom `__using__/1` macros which will be passed through
104 + to the target schema module. This is one of the core functionalities of extensions, and works the
105 + same as you would normally `use` a module, and helps compartmentalize similar functionality.
106 +
107 + ## Default Extensions
108 +
109 + By default, `Flint` will enable the following extensions:
110 +
111 + * `Flint.Extensions.PreTransforms`,
112 + * `Flint.Extensions.When`,
113 + * `Flint.Extensions.EctoValidations`,
114 + * `Flint.Extensions.PostTransforms`,
115 + * `Flint.Extensions.Accessible`,
116 + * `Flint.Extensions.Embedded`,
117 + * `Flint.Extensions.JSON`
118 +
119 + If you want to pass your own list of extensions for a module, you will need to explicitly pass the defaults
120 + as well if you would like to keep them. You can use the convenience `Flint.default_extensions/0` constant
121 + if you want to include all of the defaults.
122 + """
123 + use Spark.Dsl,
124 + many_extension_kinds: [:extensions],
125 + default_extensions: [extensions: Flint.Extension.Dsl]
126 +
127 + defmacro __using__(opts) do
128 + quote do
129 + unquote_splicing(super(opts))
130 +
131 + defmacro __using__(opts) do
132 + quote do
133 + end
134 + end
135 +
136 + defoverridable __using__: 1
137 +
138 + @doc false
139 + def option_names(),
140 + do: Spark.Dsl.Extension.get_entities(__MODULE__, :options) |> Enum.map(& &1.name)
141 +
142 + @doc false
143 + def attribute_names(),
144 + do: Spark.Dsl.Extension.get_entities(__MODULE__, :attributes) |> Enum.map(& &1.name)
145 + end
146 + end
147 + end
Loading more files…