Current section

43 Versions

Jump to

Compare versions

9 files changed
+240 additions
-15 deletions
  @@ -249,6 +249,33 @@ from the precondition function \\"&(rem(&1, 5) == 0)\\" defined \\
249 249 for User.divisible_5_integer() type."]}
250 250 ```
251 251
252 + Domo library plays nicely with [Echo.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html).
253 + It exports `validate_type/*` functions in `Domo.Changeset` module
254 + to automatically validate field changes to conform `t()` type of the schema.
255 +
256 + ```elixir
257 + defmodule User do
258 + use Ecto.Schema
259 + use Domo
260 + import Ecto.Changeset
261 + import Domo.Changeset
262 +
263 + schema "users" do
264 + field :name
265 + end
266 +
267 + @type t :: %__MODULE__{
268 + name :: String.t() | nil
269 + }
270 +
271 + def changeset(user, params \\ %{}) do
272 + user
273 + |> cast(params, [:name])
274 + |> validate_type()
275 + end
276 + end
277 + ```
278 +
252 279 ## How it works
253 280
254 281 For `MyModule` struct using Domo, the library generates a `MyModule.TypeEnsurer`
  @@ -508,6 +535,14 @@ identifier
508 535 |> tag(Id)
509 536 ```
510 537
538 + ## Usage with Ecto
539 +
540 + Ecto schema can have `t()` type defined and Domo library can validate
541 + field changes conform to the defined type with `validate_type/*` functions
542 + for the given changeset.
543 +
544 + See `Domo.Changeset` module documentation for example.
545 +
511 546 ## Options
512 547
513 548 The following options can be passed with `use Domo, [...]`
  @@ -659,6 +694,11 @@ with correct states at every update that is valid in many business contexts.
659 694
660 695 ## Changelog
661 696
697 + ### 1.2.8
698 + * Add `Domo.Changeset.validate_type/*` functions to validate Echo.Changeset field changes matching the t() type.
699 +
700 + * Fix the bug to return custom error from precondition function as underlying error for :| types.
701 +
662 702 ### 1.2.7
663 703 * Fix the bug to make recompilation occur when fixing alias for remote type.
664 704
  @@ -745,3 +785,4 @@ with correct states at every update that is valid in many business contexts.
745 785 Copyright © 2021 Ivan Rublev
746 786
747 787 This project is licensed under the [MIT license](LICENSE).
788 +
  @@ -6,8 +6,9 @@
6 6 {<<"files">>,
7 7 [<<".formatter.exs">>,<<"lib">>,<<"lib/domo">>,
8 8 <<"lib/domo/tagged_tuple.ex">>,<<"lib/domo/error_builder.ex">>,
9 - <<"lib/domo/mix_project_helper.ex">>,<<"lib/domo/raises.ex">>,
10 - <<"lib/domo/precondition_handler.ex">>,<<"lib/domo/type_ensurer_factory">>,
9 + <<"lib/domo/changeset.ex">>,<<"lib/domo/mix_project_helper.ex">>,
10 + <<"lib/domo/raises.ex">>,<<"lib/domo/precondition_handler.ex">>,
11 + <<"lib/domo/type_ensurer_factory">>,
11 12 <<"lib/domo/type_ensurer_factory/batch_ensurer.ex">>,
12 13 <<"lib/domo/type_ensurer_factory/atomizer.ex">>,
13 14 <<"lib/domo/type_ensurer_factory/error.ex">>,
  @@ -41,4 +42,4 @@
41 42 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/IvanRublev/Domo">>}]}.
42 43 {<<"name">>,<<"domo">>}.
43 44 {<<"requirements">>,[]}.
44 - {<<"version">>,<<"1.2.7">>}.
45 + {<<"version">>,<<"1.2.8">>}.
  @@ -229,6 +229,31 @@ defmodule Domo do
229 229 from the precondition function \\"&(rem(&1, 5) == 0)\\" defined \\
230 230 for User.divisible_5_integer() type."]}
231 231
232 + Domo library plays nicely with [Echo.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html).
233 + It exports `validate_type/*` functions in `Domo.Changeset` module
234 + to automatically validate field changes to conform `t()` type of the schema.
235 +
236 + defmodule User do
237 + use Ecto.Schema
238 + use Domo
239 + import Ecto.Changeset
240 + import Domo.Changeset
241 +
242 + schema "users" do
243 + field :name
244 + end
245 +
246 + @type t :: %__MODULE__{
247 + name :: String.t() | nil
248 + }
249 +
250 + def changeset(user, params \\ %{}) do
251 + user
252 + |> cast(params, [:name])
253 + |> validate_type()
254 + end
255 + end
256 +
232 257 ## How it works
233 258
234 259 For `MyModule` struct using Domo, the library generates a `MyModule.TypeEnsurer`
  @@ -468,6 +493,14 @@ defmodule Domo do
468 493 |> Enum.join()
469 494 |> tag(Id)
470 495
496 + ## Usage with Ecto
497 +
498 + Ecto schema can have `t()` type defined and Domo library can validate
499 + field changes conform to the defined type with `validate_type/*` functions
500 + for the given changeset.
501 +
502 + See `Domo.Changeset` module documentation for example.
503 +
471 504 ## Options
472 505
473 506 The following options can be passed with `use Domo, [...]`
  @@ -0,0 +1,127 @@
1 + defmodule Domo.Changeset do
2 + @moduledoc """
3 + Module with validation functions for [Echo.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html#module-validations-and-constraints).
4 +
5 + To make `validate_type/*` functions work `t()` type can be defined for the schema like the following:
6 +
7 + defmodule User do
8 + use Ecto.Schema
9 + use Domo
10 + import Ecto.Changeset
11 + import Domo.Changeset
12 +
13 + schema "users" do
14 + field :name
15 + field :email
16 + field :age, :integer
17 + end
18 +
19 + @type t :: %__MODULE__{
20 + name :: String.t() | nil,
21 + email :: String.t() | nil,
22 + age :: integer() | nil
23 + }
24 +
25 + def changeset(user, params \\ %{}) do
26 + user
27 + |> cast(params, [:name, :email, :age])
28 + |> validate_required([:name, :email])
29 + |> validate_type()
30 + end
31 + end
32 + """
33 +
34 + alias Domo.ErrorBuilder
35 + alias Domo.Raises
36 +
37 + @doc """
38 + Validates field change values conforms to appropriate types defined within the schema's t() type.
39 +
40 + It perform the validation only if a change value is not nil.
41 +
42 + In case there's at least one error, the list of errors will be appended to the `:errors` field of the changeset and the `:valid?` flag will be set to false.
43 + """
44 + def validate_type(%{data: %schema{}} = changeset) do
45 + validate_type(changeset, schema)
46 + end
47 +
48 + def validate_type(_changeset) do
49 + Raises.raise_no_schema_module()
50 + end
51 +
52 + @doc """
53 + Similar to validate_type/1, but can work with a map changeset. Takes struct module name as `struct`.
54 +
55 + ## Examples
56 +
57 + {%{}, %{name: :string, email: :string, age: :integer}}
58 + |> cast(%{name: "Hello world", email: "some@address", age: 21}, [:name, :email, :age])
59 + |> validate_type(User)
60 + """
61 + def validate_type(changeset, struct) when is_atom(struct) do
62 + type_ensurer = Module.concat(struct, TypeEnsurer)
63 +
64 + if Code.ensure_loaded?(type_ensurer) do
65 + fields =
66 + type_ensurer
67 + |> apply(:fields, [])
68 + |> Enum.reject(&(&1 |> Atom.to_string() |> String.starts_with?("__")))
69 +
70 + Enum.reduce(fields, changeset, &do_validate_change(type_ensurer, &2, &1))
71 + else
72 + Raises.raise_no_type_ensurer_for_schema_module(struct)
73 + end
74 + end
75 +
76 + @doc """
77 + Similar to validate_type/1, but validates only given `fields`.
78 + """
79 + def validate_type_fields(changeset, fields)
80 +
81 + def validate_type_fields(changeset, []) do
82 + changeset
83 + end
84 +
85 + def validate_type_fields(%{data: %schema{}} = changeset, [_ | _] = fields) do
86 + validate_type_fields(changeset, schema, fields)
87 + end
88 +
89 + def validate_type_fields(_changeset, [_ | _] = _fields) do
90 + Raises.raise_no_schema_module()
91 + end
92 +
93 + @doc """
94 + Similar to validate_type/2, but validates only given `fields`.
95 + """
96 + def validate_type_fields(changeset, struct, fields)
97 +
98 + def validate_type_fields(changeset, _struct, []) do
99 + changeset
100 + end
101 +
102 + def validate_type_fields(changeset, struct, fields) do
103 + type_ensurer = Module.concat(struct, TypeEnsurer)
104 +
105 + if Code.ensure_loaded?(type_ensurer) do
106 + Enum.reduce(fields, changeset, &do_validate_change(type_ensurer, &2, &1))
107 + else
108 + Raises.raise_no_type_ensurer_for_schema_module(struct)
109 + end
110 + end
111 +
112 + defp do_validate_change(type_ensurer, changeset, field) do
113 + apply(Ecto.Changeset, :validate_change, [
114 + changeset,
115 + field,
116 + fn field, value ->
117 + case apply(type_ensurer, :ensure_field_type, [{field, value}]) do
118 + :ok ->
119 + []
120 +
121 + {:error, _message} = error ->
122 + [{field, ErrorBuilder.pretty_error(error)}]
123 + end
124 + end
125 + ])
126 + end
127 + end
  @@ -22,7 +22,7 @@ defmodule Domo.ErrorBuilder do
22 22 {field || :t, pretty_error(error)}
23 23 end
24 24
25 - def pretty_error({:error, {:type_mismatch, _struct_module, _field, _value, _expected_types, {:bypass, message}}}) do
25 + def pretty_error({:error, {:type_mismatch, _struct_module, _field, _value, _expected_types, [{:bypass, message}]}}) do
26 26 message
27 27 end
28 28
  @@ -34,14 +34,19 @@ defmodule Domo.ErrorBuilder do
34 34 end
35 35
36 36 def pretty_error({:error, {:type_mismatch, struct_module, field, value, expected_types, error_templates}}) do
37 - top_level_error = generate_top_level_error(expected_types)
38 - underlying_errors = collect_deepest_underlying_errors(error_templates)
37 + if bypass_error = Enum.find(error_templates, &match?([{:bypass, _message}], &1)) do
38 + [{:bypass, message}] = bypass_error
39 + message
40 + else
41 + top_level_error = generate_top_level_error(expected_types)
42 + underlying_errors = collect_deepest_underlying_errors(error_templates)
39 43
40 - invalid_value = invalid_value_message(value, field, struct_module)
41 - general_error_string = general_error_message(top_level_error)
42 - underlying_errors_string = underlying_errors_message(underlying_errors)
44 + invalid_value = invalid_value_message(value, field, struct_module)
45 + general_error_string = general_error_message(top_level_error)
46 + underlying_errors_string = underlying_errors_message(underlying_errors)
43 47
44 - "#{invalid_value} #{general_error_string}#{underlying_errors_string}"
48 + "#{invalid_value} #{general_error_string}#{underlying_errors_string}"
49 + end
45 50 end
46 51
47 52 defp generate_top_level_error(expected_types) do
  @@ -113,9 +118,7 @@ defmodule Domo.ErrorBuilder do
113 118 end
114 119
115 120 defp underlying_errors_message([_ | _] = errors) do
116 - lines =
117 - errors
118 - |> interpolate_indent_unerlying()
121 + lines = interpolate_indent_unerlying(errors)
119 122
120 123 ["\nUnderlying errors:"]
121 124 |> Enum.concat(lines)
  @@ -132,6 +135,10 @@ defmodule Domo.ErrorBuilder do
132 135 end)
133 136 end
134 137
138 + defp interpolate_error_template(:bypass, message) do
139 + message
140 + end
141 +
135 142 defp interpolate_error_template(template, args) do
136 143 Enum.reduce(args, template, fn {key, value}, template ->
137 144 string = if is_nil(value), do: "nil", else: to_string(value)
Loading more files…