defmodule Ash.Query.Operator do
@moduledoc """
An operator is a predicate with a `left` and a `right`
For more information on being a predicate, see `Ash.Filter.Predicate`. Most of the complexities
are there. An operator must meet both behaviours.
"""
alias Ash.Query.{Call, Expression, Not, Ref}
import Ash.Filter.TemplateHelpers, only: [expr?: 1]
@doc """
Create a new predicate. There are various return types possible:
* `{:ok, left, right}` - Return the left/right values of the operator
* `{:ok, operator}` - Return the operator itself, this or the one above are acceptable
* `{:known, boolean}` - If the value is already known, e.g `1 == 1`
* `{:error, error}` - If there was an error creating the operator
"""
@callback new(term, term) ::
{:ok, term, term} | {:ok, term} | {:known, boolean} | {:error, term}
@doc """
The implementation of the inspect protocol.
If not defined, it will be inferred
"""
@callback to_string(struct, Inspect.Opts.t()) :: term
@doc "Create a new operator. Pass the module and the left and right values"
def new(mod, %Ref{} = left, right) do
try_cast_with_ref(mod, left, right)
end
def new(mod, left, %Ref{} = right) do
try_cast_with_ref(mod, left, right)
end
def new(mod, %{__struct__: struct} = left, right)
when struct in [Call, Expression, Not] do
mod.new(left, right)
end
def new(mod, left, %{__struct__: struct} = right)
when struct in [Call, Expression, Not] do
mod.new(left, right)
end
def new(mod, %{__predicate__?: _} = left, right) do
mod.new(left, right)
end
def new(mod, left, %{__predicate__?: _} = right) do
mod.new(left, right)
end
def new(mod, left, right) do
if expr?(left) or expr?(right) do
mod.new(left, right)
else
case mod.new(left, right) do
{:ok, val} ->
case val do
%mod{__predicate__?: _} ->
case mod.evaluate(val) do
{:known, value} -> {:ok, value}
{:error, error} -> {:error, error}
:unknown -> {:ok, val}
end
_ ->
{:ok, val}
end
{:error, error} ->
{:error, error}
end
end
end
@doc false
def try_cast_with_ref(mod, left, right) do
Enum.find_value(mod.types(), fn type ->
try_cast(left, right, type)
end)
|> case do
nil ->
{:error, "Could not cast expression: #{inspect(mod)} #{inspect(left)} #{inspect(right)}"}
{:error, error} ->
{:error, error}
{:ok, left, right} ->
mod.new(left, right)
end
end
defp try_cast(%{__predicate__?: _} = left, right, _) do
{:ok, left, right}
end
defp try_cast(left, %{__predicate__?: _} = right, _) do
{:ok, left, right}
end
defp try_cast(%Ref{attribute: %{type: type}} = left, right, :same) do
case Ash.Query.Type.try_cast(right, type) do
{:ok, new_right} ->
{:ok, left, new_right}
_ ->
nil
end
end
defp try_cast(left, %Ref{attribute: %{type: type}} = right, :same) do
case Ash.Query.Type.try_cast(left, type) do
{:ok, new_left} ->
{:ok, new_left, right}
_ ->
nil
end
end
defp try_cast(%Ref{attribute: %{type: type}} = left, right, [:any, {:array, :same}]) do
case right do
# TODO: app level type compatibility?
%Ref{attribute: %{type: {:array, _type}}} ->
{:ok, left, right}
%Ref{} ->
nil
right ->
case Ash.Query.Type.try_cast(right, {:array, type}) do
{:ok, new_right} ->
{:ok, left, new_right}
_ ->
nil
end
end
end
defp try_cast(left, %Ref{attribute: %{type: {:array, type}}} = right, [:any, {:array, :same}]) do
case Ash.Query.Type.try_cast(left, type) do
{:ok, new_left} ->
{:ok, new_left, right}
_ ->
nil
end
end
# We don't have a way to infer types from values right now
defp try_cast(left, right, [:same, :same]) do
{:ok, left, right}
end
defp try_cast(left, right, [:same, {:array, :same}]) do
{:ok, left, right}
end
defp try_cast(left, right, [{:array, :same}, :same]) do
{:ok, left, right}
end
defp try_cast(left, right, [{:array, :same}, {:array, :same}]) do
{:ok, left, right}
end
defp try_cast(left, right, [:same, type]) do
try_cast(left, right, [type, type])
end
defp try_cast(left, right, [type, :same]) do
try_cast(left, right, [type, type])
end
defp try_cast(left, right, [{:array, :same}, type]) do
try_cast(left, right, [{:array, type}, type])
end
defp try_cast(left, right, [type, :same]) do
try_cast(left, right, [type, type])
end
defp try_cast(left, right, [type, {:array, :same}]) do
try_cast(left, right, [type, {:array, type}])
end
defp try_cast(left, right, [left_type, right_type]) do
with {:ok, left} <- cast_one(left, left_type),
{:ok, right} <- cast_one(right, right_type) do
{:ok, left, right}
else
{:error, error} ->
{:error, error}
end
end
defp try_cast(left, right, :any) do
{:ok, left, right}
end
defp try_cast(left, right, {:array, :any}) do
{:ok, left, right}
end
defp try_cast(_, _, _), do: nil
defp cast_one(value, {:array, :any}) do
{:ok, value}
end
defp cast_one(value, :any) do
{:ok, value}
end
defp cast_one(value, type) do
if Ash.Filter.TemplateHelpers.expr?(value) do
{:ok, value}
else
case Ash.Query.Type.try_cast(value, type) do
{:ok, casted} ->
{:ok, casted}
_ ->
{:error, "Could not cast #{inspect(value)} as #{inspect(type)}"}
end
end
end
def operators do
[
Ash.Query.Operator.Eq,
Ash.Query.Operator.GreaterThanOrEqual,
Ash.Query.Operator.GreaterThan,
Ash.Query.Operator.In,
Ash.Query.Operator.IsNil,
Ash.Query.Operator.LessThanOrEqual,
Ash.Query.Operator.LessThan,
Ash.Query.Operator.NotEq
] ++ Ash.Query.Operator.Basic.operator_modules()
end
def operator_symbols do
Enum.map(operators(), & &1.operator)
end
defmacro __using__(opts) do
unless opts[:operator] do
raise "Operator is required!"
end
quote do
defstruct [
:left,
:right,
operator: unquote(opts[:operator]),
embedded?: false,
__operator__?: true,
__predicate__?: unquote(opts[:predicate?] || false)
]
if unquote(opts[:predicate?]) do
@behaviour Ash.Filter.Predicate
end
alias Ash.Query.Ref
def operator, do: unquote(opts[:operator])
def name, do: unquote(opts[:name] || opts[:operator])
def predicate? do
unquote(opts[:predicate?])
end
def types do
unquote(opts[:types] || [:same, :any])
end
def new(left, right), do: {:ok, struct(__MODULE__, left: left, right: right)}
import Inspect.Algebra
def to_string(%{left: left, right: right, operator: operator}, opts) do
concat([
to_doc(left, opts),
" ",
to_string(operator),
" ",
to_doc(right, opts)
])
end
defoverridable to_string: 2, new: 2
defimpl Inspect do
def inspect(%mod{} = op, opts) do
mod.to_string(op, opts)
end
end
end
end
end