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