|
| 1 | +defmodule OnePiece.Ecto.Schema do |
| 2 | + @moduledoc """ |
| 3 | + Extends a `Ecto.Schema` module with functionality. |
| 4 | + """ |
| 5 | + |
| 6 | + alias Ecto.Changeset |
| 7 | + |
| 8 | + @doc """ |
| 9 | + Extends a `Ecto.Schema` module with some functionality. |
| 10 | +
|
| 11 | + defmodule MyValueObject do |
| 12 | + use Ecto.Schema |
| 13 | + use OnePiece.Ecto.Schema |
| 14 | +
|
| 15 | + embedded_schema do |
| 16 | + field :title, :string |
| 17 | + # ... |
| 18 | + end |
| 19 | + end |
| 20 | +
|
| 21 | + The following functions are available in the module now: |
| 22 | +
|
| 23 | + `new/1`: **overridable** struct factory function. It takes an attribute map |
| 24 | + and runs the `changeset/2`. |
| 25 | +
|
| 26 | + `new!/1`: **overridable** like `new/1` raising an error when the validation |
| 27 | + fails. |
| 28 | +
|
| 29 | + `changeset/2`: **overridable** function. It takes a struct and the attributes |
| 30 | + and returns a `Ecto.Changeset`. |
| 31 | + The default implementation apply a deeply-nested casting over all the fields |
| 32 | + using `Ecto.Changeset.cast/4` and `Ecto.Changeset.cast_embed/4`. |
| 33 | + When `@enforce_keys` is defined, it will apply `Ecto.Changeset.validate_required/3` |
| 34 | + to the list of fields. |
| 35 | + When overriding the function, allows you have full control over the validation |
| 36 | + layer, deactivating all the nested-casting. |
| 37 | + """ |
| 38 | + @spec __using__(opts :: []) :: any() |
| 39 | + defmacro __using__(_opts \\ []) do |
| 40 | + quote do |
| 41 | + alias OnePiece.Ecto.Schema |
| 42 | + |
| 43 | + @before_compile OnePiece.Ecto.Schema |
| 44 | + |
| 45 | + @doc """ |
| 46 | + Creates a `t:t/0`. |
| 47 | + """ |
| 48 | + @spec new(attrs :: map()) :: {:ok, %__MODULE__{}} |
| 49 | + def new(attrs) do |
| 50 | + ValueObject.__new__(__MODULE__, attrs) |
| 51 | + end |
| 52 | + |
| 53 | + @doc """ |
| 54 | + Creates a `t:t/0`. |
| 55 | + """ |
| 56 | + @spec new!(attrs :: map()) :: %__MODULE__{} |
| 57 | + def new!(attrs) do |
| 58 | + ValueObject.__new__!(__MODULE__, attrs) |
| 59 | + end |
| 60 | + |
| 61 | + @doc """ |
| 62 | + Returns an `t:Ecto.Changeset.t/0` for a given `t:t/0` model. |
| 63 | + """ |
| 64 | + @spec changeset(model :: %__MODULE__{}, attrs :: map()) :: Ecto.Changeset.t() |
| 65 | + def changeset(model, attrs) do |
| 66 | + ValueObject.__changeset__(model, attrs) |
| 67 | + end |
| 68 | + |
| 69 | + defoverridable new: 1, new!: 1, changeset: 2 |
| 70 | + end |
| 71 | + end |
| 72 | + |
| 73 | + defmacro __before_compile__(env) do |
| 74 | + enforced_keys = get_enforced_keys(env) |
| 75 | + |
| 76 | + quote unquote: false, bind_quoted: [enforced_keys: enforced_keys] do |
| 77 | + def __enforced_keys__ do |
| 78 | + unquote(enforced_keys) |
| 79 | + end |
| 80 | + |
| 81 | + for the_key <- enforced_keys do |
| 82 | + def __enforced_keys__?(unquote(the_key)) do |
| 83 | + true |
| 84 | + end |
| 85 | + end |
| 86 | + |
| 87 | + def __enforced_keys__?(_) do |
| 88 | + false |
| 89 | + end |
| 90 | + end |
| 91 | + end |
| 92 | + |
| 93 | + defp get_enforced_keys(env) do |
| 94 | + enforce_keys = Module.get_attribute(env.module, :enforce_keys) || [] |
| 95 | + enforce_keys ++ get_primary_key_name(env) |
| 96 | + end |
| 97 | + |
| 98 | + defp get_primary_key_name(env) do |
| 99 | + case Module.get_attribute(env.module, :primary_key) do |
| 100 | + {field_name, _, _} -> [field_name] |
| 101 | + _ -> [] |
| 102 | + end |
| 103 | + end |
| 104 | + |
| 105 | + def __new__(struct_module, attrs) do |
| 106 | + struct_module |
| 107 | + |> apply_changeset(attrs) |
| 108 | + |> Changeset.apply_action(:new) |
| 109 | + end |
| 110 | + |
| 111 | + def __new__!(struct_module, attrs) do |
| 112 | + struct_module |
| 113 | + |> apply_changeset(attrs) |
| 114 | + |> Changeset.apply_action!(:new!) |
| 115 | + end |
| 116 | + |
| 117 | + def __changeset__(%struct_module{} = model, attrs) do |
| 118 | + embeds = struct_module.__schema__(:embeds) |
| 119 | + allowed = struct_module.__schema__(:fields) -- embeds |
| 120 | + |
| 121 | + changeset = |
| 122 | + model |
| 123 | + |> Changeset.cast(attrs, allowed) |
| 124 | + |> Changeset.validate_required(struct_module.__enforced_keys__() -- embeds) |
| 125 | + |
| 126 | + Enum.reduce( |
| 127 | + embeds, |
| 128 | + changeset, |
| 129 | + &Changeset.cast_embed(&2, &1, required: struct_module.__enforced_keys__?(&1)) |
| 130 | + ) |
| 131 | + end |
| 132 | + |
| 133 | + defp apply_changeset(struct_module, attrs) do |
| 134 | + struct_module |
| 135 | + |> struct() |
| 136 | + |> struct_module.changeset(attrs) |
| 137 | + end |
| 138 | +end |
0 commit comments