defmodule Decant.Field do
@moduledoc """
A normalized searchable field.
Field specs accepted by `Decant.dynamic/2`:
* `{binding, column}` — match `column` on the named binding `binding`.
* `{binding, column, opts}` — with per-field `opts`:
* `:cast` — `:string` wraps the column in `CAST(? AS TEXT)`.
* `:match` — `:contains | :prefix | :suffix | :exact`, overriding the
call-wide `:match` for this field only.
`binding` is the atom passed to `as:` in the host query. `column` is the
schema field atom.
"""
@enforce_keys [:binding, :column]
defstruct binding: nil, column: nil, cast: nil, match: nil
@type t :: %__MODULE__{
binding: atom(),
column: atom(),
cast: :string | nil,
match: :contains | :prefix | :suffix | :exact | nil
}
@doc "Normalize a user field spec into a `%Decant.Field{}`."
@spec normalize(tuple() | t()) :: t()
def normalize(%__MODULE__{} = field), do: validate!(field)
def normalize({binding, column}) when is_atom(binding) and is_atom(column),
do: validate!(%__MODULE__{binding: binding, column: column})
def normalize({binding, column, opts})
when is_atom(binding) and is_atom(column) and is_list(opts) do
validate!(%__MODULE__{
binding: binding,
column: column,
cast: Keyword.get(opts, :cast),
match: Keyword.get(opts, :match)
})
end
def normalize(other) do
raise ArgumentError,
"invalid field spec: #{inspect(other)} — expected {binding, column} or {binding, column, opts}"
end
defp validate!(%__MODULE__{cast: cast} = f) when cast not in [nil, :string],
do:
raise(
ArgumentError,
"field #{inspect(f.column)}: :cast must be :string or nil, got: #{inspect(cast)}"
)
defp validate!(%__MODULE__{match: match} = f)
when match not in [nil, :contains, :prefix, :suffix, :exact],
do:
raise(
ArgumentError,
"field #{inspect(f.column)}: :match must be one of :contains, :prefix, :suffix, :exact, got: #{inspect(match)}"
)
defp validate!(%__MODULE__{} = f), do: f
end