Current section
12 Versions
Jump to
Current section
12 Versions
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…