Packages

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

Current section

12 Versions

Jump to

Compare versions

16 files changed
+533 additions
-254 deletions
  @@ -31,7 +31,9 @@ Declarative [`Ecto`](https://github.com/elixir-ecto/ecto) `embedded_schema`s for
31 31 ```elixir
32 32 def deps do
33 33 [
34 - {:flint, "~> 0.5"}
34 + {:flint, "~> 0.6"},
35 + # If you want access to the `Typed` extension to add generated typespecs
36 + {:typed_ecto_schema, "~> 0.4", runtime: false}
35 37 ]
36 38 end
37 39 ```
  @@ -349,7 +351,6 @@ You can configure the default options set by `Flint`.
349 351 * `embeds_one!`: The default arguments when using `embeds_one!`. Defaults to `[on_replace: :delete]`
350 352 * `embeds_many`: The default arguments when using `embeds_many` or `embeds_many!`. Defaults to `[on_replace: :delete]`
351 353 * `embeds_many!`: The default arguments when using `embeds_many!`. Defaults to `[on_replace: :delete]`
352 - * `:enum`: The default arguments for an `Ecto.Enum` field. Defaults to `[embed_as: :dumped]`.
353 354 * `:aliases`: [Aliases](#aliases)
354 355
355 356 You can also configure any aliases you want to use for schema validations.
  @@ -358,13 +359,13 @@ You can also configure any aliases you want to use for schema validations.
358 359
359 360 `Flint` takes advantage of the distinction `Ecto` makes between an `embedded_schema`'s embedded and dumped representations.
360 361
361 - For example, by default in `Flint`, `Ecto.Enum`s that are `Keyword` (rather than just lists of atoms) will have their keys
362 + For example, `Flint` provides the `Flint.Types.Enum` type, which are `Ecto.Enum`s where, when given values that are `Keyword` (rather than just lists of atoms) will have their keys
362 363 be the embedded representation and will have the values be the dumped representation.
363 364
364 365 ```elixir
365 366 defmodule Book do
366 367 use Flint.Schema, schema: [
367 - field(:genre, Ecto.Enum, values: [biography: 0, science_fiction: 1, fantasy: 2, mystery: 3])
368 + field(:genre, Flint.Types.Enum, values: [biography: 0, science_fiction: 1, fantasy: 2, mystery: 3])
368 369 ]
369 370 end
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/acalejos/flint">>}]}.
2 2 {<<"name">>,<<"flint">>}.
3 - {<<"version">>,<<"0.5.1">>}.
3 + {<<"version">>,<<"0.6.0">>}.
4 4 {<<"description">>,
5 5 <<"Declarative Ecto embedded schemas for data validation, coercion, and manipulation.">>}.
6 6 {<<"elixir">>,<<"~> 1.14">>}.
  @@ -17,6 +17,11 @@
17 17 {<<"optional">>,false},
18 18 {<<"requirement">>,<<"~> 2.2">>},
19 19 {<<"repository">>,<<"hexpm">>}],
20 + [{<<"name">>,<<"typed_ecto_schema">>},
21 + {<<"app">>,<<"typed_ecto_schema">>},
22 + {<<"optional">>,true},
23 + {<<"requirement">>,<<"~> 0.4">>},
24 + {<<"repository">>,<<"hexpm">>}],
20 25 [{<<"name">>,<<"jason">>},
21 26 {<<"app">>,<<"jason">>},
22 27 {<<"optional">>,true},
  @@ -30,13 +35,15 @@
30 35 {<<"files">>,
31 36 [<<"lib">>,<<"lib/type.ex">>,<<"lib/changeset.ex">>,<<"lib/schema.ex">>,
32 37 <<"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">>,
38 + <<"lib/flint/types/enum.ex">>,<<"lib/flint/types/union.ex">>,
39 + <<"lib/flint/extension">>,<<"lib/flint/extension/field.ex">>,
40 + <<"lib/flint/extension/dsl.ex">>,<<"lib/flint/extensions">>,
41 + <<"lib/flint/extensions/pre_transforms.ex">>,
42 + <<"lib/flint/extensions/typed.ex">>,
36 43 <<"lib/flint/extensions/post_transforms.ex">>,
37 44 <<"lib/flint/extensions/ecto_validations.ex">>,
38 - <<"lib/flint/extensions/when.ex">>,<<"lib/flint/extensions/json.ex">>,
39 - <<"lib/flint/extensions/embedded.ex">>,
45 + <<"lib/flint/extensions/block.ex">>,<<"lib/flint/extensions/when.ex">>,
46 + <<"lib/flint/extensions/json.ex">>,<<"lib/flint/extensions/embedded.ex">>,
40 47 <<"lib/flint/extensions/accessible.ex">>,<<"lib/extension.ex">>,
41 48 <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>]}.
42 49 {<<"build_tools">>,[<<"mix">>]}.
  @@ -4,69 +4,6 @@ defmodule Flint.Changeset do
4 4 that are collected when using the `Flint.Schema` macros to perform transformations and validations.
5 5 """
6 6
7 - @doc """
8 - Uses the quoted expressions from the `Flint.Schema.field` and `Flint.Schema.field!`
9 - `do` blocks to validate the changeset.
10 -
11 - You can optionally pass bindings to be added to the evaluation context.
12 - """
13 - def validate_do_blocks(changeset, bindings \\ []) do
14 - module = changeset.data.__struct__
15 - env = Module.concat(module, Env) |> apply(:env, [])
16 -
17 - all_validations =
18 - module.__schema__(:blocks)
19 -
20 - for {field, block} <- all_validations, reduce: changeset do
21 - changeset ->
22 - bindings = bindings ++ Enum.into(changeset.changes, [])
23 -
24 - block
25 - |> Enum.with_index()
26 - |> Enum.reduce(changeset, fn
27 - {{quoted_condition, quoted_err}, index}, chngset ->
28 - try do
29 - {invalid?, _bindings} = Code.eval_quoted(quoted_condition, bindings, env)
30 -
31 - invalid? =
32 - if is_function(invalid?) do
33 - case Function.info(invalid?, :arity) do
34 - {:arity, 0} ->
35 - apply(invalid?, [])
36 -
37 - {:arity, 1} when not is_nil(field) ->
38 - apply(invalid?, [Ecto.Changeset.fetch_change!(changeset, field)])
39 -
40 - _ ->
41 - raise ArgumentError,
42 - "Anonymous functions in validation clause must be either 0-arity or an input value for the field must be provided."
43 - end
44 - else
45 - invalid?
46 - end
47 -
48 - {err_msg, _bindings} = Code.eval_quoted(quoted_err, bindings, env)
49 -
50 - if invalid? do
51 - Ecto.Changeset.add_error(chngset, field, err_msg,
52 - validation: :block,
53 - clause: index + 1
54 - )
55 - else
56 - chngset
57 - end
58 - rescue
59 - _ ->
60 - Ecto.Changeset.add_error(
61 - chngset,
62 - field,
63 - "Error evaluating expression in Clause ##{index + 1} of `do:` block"
64 - )
65 - end
66 - end)
67 - end
68 - end
69 -
70 7 @doc """
71 8 Given a `Flint` (or `Ecto`) schema and params (can be a map, struct of the given schema, or an existing changeset),
72 9 applies all steps of the `Flint.Changeset` to generate a new changeset.
  @@ -106,8 +43,19 @@ defmodule Flint.Changeset do
106 43 )
107 44 end
108 45
46 + extension_names =
47 + module.__schema__(:extensions)
48 + |> Enum.map(fn
49 + {ext, _opts} when is_atom(ext) -> ext
50 + ext when is_atom(ext) -> ext
51 + end)
52 +
109 53 changeset
110 54 |> Ecto.Changeset.validate_required(required_fields)
111 - |> validate_do_blocks(bindings)
55 + |> then(
56 + &Enum.reduce(extension_names, &1, fn extension, chst ->
57 + extension.changeset(chst, bindings)
58 + end)
59 + )
112 60 end
113 61 end
  @@ -161,6 +161,8 @@ defmodule Flint.Extension do
161 161
162 162 By default, `Flint` will enable the following extensions:
163 163
164 + * `Flint.Extensions.Block`
165 + * `Flint.Extensions.Typed`
164 166 * `Flint.Extensions.PreTransforms`,
165 167 * `Flint.Extensions.When`,
166 168 * `Flint.Extensions.EctoValidations`,
  @@ -177,6 +179,9 @@ defmodule Flint.Extension do
177 179 many_extension_kinds: [:extensions],
178 180 default_extensions: [extensions: Flint.Extension.Dsl]
179 181
182 + @callback changeset(Ecto.Changeset.t()) :: Ecto.Changeset.t()
183 + @callback changeset(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
184 +
180 185 @doc false
181 186 defmacro embedded_schema(do: {:__block__, _, contents}) do
182 187 Module.put_attribute(__CALLER__.module, :embedded_schema, contents)
  @@ -187,20 +192,50 @@ defmodule Flint.Extension do
187 192 Module.put_attribute(__CALLER__.module, :embedded_schema, [block])
188 193 end
189 194
195 + @doc false
196 + def __embedded_schema__(env, extension) do
197 + mods =
198 + env.macros
199 + |> Enum.find_value(
200 + fn {_mod, exports} -> Keyword.has_key?(exports, :embedded_schema) end,
201 + fn {mod, exports} ->
202 + {mod, Keyword.take(exports, [:embedded_schema])}
203 + end
204 + )
205 +
206 + Module.put_attribute(env.module, :__embedded_schema_super__, {extension, mods})
207 +
208 + mods
209 + end
210 +
211 + @doc false
212 + def __context__(env, extension) do
213 + Module.get_attribute(env.module, :__embedded_schema_super__)
214 + |> Keyword.fetch!(extension)
215 + end
216 +
190 217 defmacro __using__(opts) do
191 218 Module.put_attribute(__CALLER__.module, :embedded_schema, [])
192 219
193 220 quote do
194 221 import Flint.Extension
222 + @behaviour Flint.Extension
195 223 unquote_splicing(super(opts))
196 224
225 + @impl true
226 + def changeset(changeset, bindings \\ []) do
227 + changeset
228 + end
229 +
197 230 defmacro __using__(opts) do
198 231 quote do
199 232 end
200 233 end
201 234
202 235 defoverridable __using__: 1
236 + defoverridable Flint.Extension
203 237
238 + @doc false
204 239 def template_schema() do
205 240 @embedded_schema
206 241 end
  @@ -9,15 +9,19 @@ defmodule Flint do
9 9 if opts[:except] && opts[:only],
10 10 do: raise(ArgumentError, "Cannot specify both `:only` and `:except` options.")
11 11
12 - defaults = [
13 - Flint.Extensions.PreTransforms,
14 - Flint.Extensions.When,
15 - Flint.Extensions.EctoValidations,
16 - Flint.Extensions.PostTransforms,
17 - Flint.Extensions.Accessible,
18 - Flint.Extensions.Embedded,
19 - Flint.Extensions.JSON
20 - ]
12 + defaults =
13 + [
14 + Flint.Extensions.Block,
15 + if(Code.ensure_loaded?(TypedEctoSchema), do: Flint.Extensions.Typed),
16 + Flint.Extensions.PreTransforms,
17 + Flint.Extensions.When,
18 + Flint.Extensions.EctoValidations,
19 + Flint.Extensions.PostTransforms,
20 + Flint.Extensions.Accessible,
21 + Flint.Extensions.Embedded,
22 + Flint.Extensions.JSON
23 + ]
24 + |> Enum.filter(& &1)
21 25
22 26 cond do
23 27 opts[:only] ->
Loading more files…