Current section

Files

Jump to
pointers lib changesets.ex
Raw

lib/changesets.ex

defmodule Pointers.Changesets do
alias Pointers.Util
alias Ecto.Changeset
alias Ecto.Association.{Has, NotLoaded}
@doc "Returns the schema object's current state."
def state(thing), do: thing.__meta__.state
@doc "True if the schema object's current state is `:built`"
def is_built?(%_{__meta__: %{state: :built}}), do: true
def is_built?(_), do: false
@doc "True if the schema object's current state is `:loaded`"
def is_loaded?(%_{__meta__: %{state: :loaded}}), do: true
def is_loaded?(_), do: false
@doc "True if the schema object's current state is `:deleted`"
def is_deleted?(%_{__meta__: %{state: :deleted}}), do: true
def is_deleted?(_), do: false
def verb(%_{}=thing), do: if(is_built?(thing), do: :create, else: :update)
defmacro auto(thing, attrs, options, defaults) do
module = __CALLER__.module
quote bind_quoted: [
module: module,
thing: thing,
attrs: attrs,
options: options,
defaults: defaults,
] do
Pointers.Changesets.auto(
thing, attrs, options, defaults,
Pointers.Changesets.config_for(module, Pointers.Changesets.verb(thing))
)
end
end
def auto(thing, attrs, options, defaults, config) do
opts = options ++ config ++ defaults
thing
|> auto_cast(attrs, opts)
|> auto_required(opts[:required])
|> auto_fields(options, defaults, config) # individual for merge
|> rewrite_errors(options, config)
end
defp auto_cast(thing, attrs, opts) do
attrs = Util.rename(attrs, Util.add_binaries(Keyword.get(opts, :rename_attrs, [])))
cast(thing, attrs, opts[:cast])
end
defp cast(thing, _, nil), do: Changeset.cast(thing, %{}, [])
defp cast(thing, attrs, cast) when is_list(cast),
do: Changeset.cast(thing, Map.delete(attrs, :cast), cast)
defp auto_required(changeset, nil), do: changeset
defp auto_required(changeset, []), do: changeset
defp auto_required(changeset, req) when is_list(req),
do: Changeset.validate_required(changeset, req)
defp auto_fields(%Changeset{data: %what{}}=cs, options, defaults, config) do
fields = what.__schema__(:fields) -- [:id]
Enum.reduce(fields, cs, fn field, cs ->
opts =
Keyword.get(options, field, []) ++ # last moment overrides
Keyword.get(config, field, []) ++ # configuration overrides
Keyword.get(defaults, field, []) # vendor values
cs
|> auto_acceptance(field, opts[:acceptance])
|> auto_exclusion(field, opts[:exclusion])
|> auto_format(field, opts[:format])
|> auto_inclusion(field, opts[:inclusion])
|> auto_length(field, opts[:length])
|> auto_number(field, opts[:number])
|> auto_subset(field, opts[:subset])
end)
end
defp auto_acceptance(changeset, field, true),
do: Changeset.validate_acceptance(changeset, field)
defp auto_acceptance(changeset, _field, _), do: changeset
defp auto_exclusion(changeset, _field, nil), do: changeset
defp auto_exclusion(changeset, field, excl),
do: Changeset.validate_exclusion(changeset, field, excl)
defp auto_format(changeset, _key, nil), do: changeset
defp auto_format(changeset, _key, []), do: changeset
defp auto_format(changeset, key, %Regex{}=format),
do: Changeset.validate_format(changeset, key, format)
defp auto_format(_changeset, key, invalid),
do: throw {:invalid_format_regexp, regexp: invalid, key: key}
defp auto_inclusion(changeset, _field, nil), do: changeset
defp auto_inclusion(changeset, field, incl) when is_list(incl),
do: Changeset.validate_inclusion(changeset, field, incl)
defp auto_inclusion(_changeset, key, invalid),
do: throw {:invalid_inclusion_list, value: invalid, key: key}
defp auto_length(changeset, _key, nil), do: changeset
defp auto_length(changeset, _key, []), do: changeset
defp auto_length(changeset, key, opts),
do: Changeset.validate_length(changeset, key, opts)
defp auto_number(changeset, _key, nil), do: changeset
defp auto_number(changeset, _key, []), do: changeset
defp auto_number(changeset, key, opts),
do: Changeset.validate_number(changeset, key, opts)
defp auto_subset(changeset, _key, nil), do: changeset
defp auto_subset(changeset, _key, []), do: changeset
defp auto_subset(changeset, key, opts),
do: Changeset.validate_subset(changeset, key, opts)
def replicate_map_change(changeset, source_key, target_key, xform) do
case Changeset.fetch_change(changeset, source_key) do
{:ok, change} ->
Changeset.put_change(changeset, target_key, xform.(change))
_ -> changeset
end
end
def replicate_map_valid_change(%Changeset{valid?: true}=changeset, source_key, target_key, xform) do
case Changeset.fetch_change(changeset, source_key) do
{:ok, change} ->
Changeset.put_change(changeset, target_key, xform.(change))
_ -> changeset
end
end
def replicate_map_valid_change(changeset, _, _, _), do: changeset
def config_for(module) do
otp_app = module.__pointers__(:otp_app)
conf = Application.get_env(otp_app, module, [])
conf
end
def config_for(module, key, default \\ []) do
conf = config_for(module)
Keyword.get(conf, key, default) ++ conf
end
@doc "true if the provided changeset or list of changesets is valid."
def valid?(%Changeset{valid?: v}), do: v
def valid?(cs) when is_list(cs), do: Enum.all?(cs, &valid?/1)
@doc """
"""
def assoc_changeset(changeset, key, attrs, opts \\ [])
def assoc_changeset(%Changeset{data: %schema{}=data}, key, attrs, opts) do
case schema.__schema__(:association, key) do
%Has{
cardinality: :one,
owner_key: :id,
related: related,
related_key: :id,
relationship: :child,
} ->
ac(verb(data), data, key, related, attrs, opts)
_ ->
raise ArgumentError, message: "assoc_changeset only supports `has_one` at present!"
end
end
defp ac(:create, %_{}, _, related, attrs, opts),
do: ac_call(related, [struct(related), attrs], opts)
defp ac(:update, %_{}=data, key, related, attrs, opts) do
case Map.get(data, key) do
%NotLoaded{} ->
raise ArgumentError,
message: "You must preload an assoc before casting to it (or set it to nil)."
%_{}=other -> ac_call(related, [other, attrs], opts)
nil -> ac_call(related, [struct(related), attrs], opts)
end
end
defp ac_call(schema, args, []), do: call(schema, :changeset, args)
defp ac_call(schema, args, opts), do: call_extra(schema, :changeset, args, [opts])
def put_assoc(changeset, key, mixin_changeset)
def put_assoc(%Changeset{}=cs, key, %Changeset{valid?: true}=mixin),
do: Changeset.put_assoc(cs, key, mixin)
def put_assoc(%Changeset{}=cs, _, %Changeset{valid?: false}=mixin),
do: %{ cs | valid?: false, errors: cs.errors ++ mixin.errors }
def cast_assoc(%Changeset{}=cs, key, attrs, opts \\ []),
do: put_assoc(cs, key, assoc_changeset(cs, key, attrs, opts))
defp call_extra(module, func, args, extra) when is_list(extra) do
Code.ensure_loaded(module)
size = Enum.count(args)
function_exported?(module, func, size + Enum.count(extra))
|> ce(module, func, args, extra, size)
end
defp ce(true, module, func, args, extra, _), do: apply(module, func, args ++ extra)
defp ce(false, module, func, args, _, size),
do: c2(function_exported?(module, func, size), module, func, args)
defp call(module, func, args) when is_list(args) do
Code.ensure_loaded(module)
size = Enum.count(args)
c2(function_exported?(module, func, size), module, func, args)
end
defp c2(true, module, func, args), do: apply(module, func, args)
defp c2(false, module, func, args),
do: raise ArgumentError, message: "Function not found: #{module}.#{func}, args: #{inspect(args)}"
def rewrite_errors(%Changeset{errors: errors}=cs, options, config) do
errs = Keyword.get(options ++ config, :rename_attrs, [])
%{ cs | errors: Util.rename(errors, Util.flip(errs)) }
end
def rewrite_errors(%Changeset{errors: errors}=cs, options, config) do
errs = Keyword.get(options ++ config, :rename_attrs, [])
%{ cs | errors: Util.rename(errors, Util.flip(errs)) }
end
def rewrite_child_errors(%Changeset{data: %what{}}=cs) do
rewrite_errors(cs, [], config_for(what))
end
end