lib/ash/filter/template_helpers.ex

defmodule Ash.Filter.TemplateHelpers do
  @moduledoc "Helpers for building filter templates"

  @deprecated "Use `expr?/1` instead, which is not a guard"

  defguard is_expr(value)
           when is_struct(value, Ash.Query.Not) or is_struct(value, Ash.Query.BooleanExpression) or
                  is_struct(value, Ash.Query.Call) or is_struct(value, Ash.Query.Ref) or
                  is_struct(value, Ash.Query.Exists) or
                  is_struct(value, Ash.Query.Parent) or
                  (is_struct(value) and is_map_key(value, :__predicate__?))

  def expr?({:_actor, _}), do: true
  def expr?({:_arg, _}), do: true
  def expr?({:_ref, _, _}), do: true
  def expr?({:_parent, _, _}), do: true
  def expr?({:_parent, _}), do: true
  def expr?({:_atomic_ref, _}), do: true
  def expr?({:_context, _}), do: true

  def expr?(value)
      when is_struct(value, Ash.Query.Not) or is_struct(value, Ash.Query.BooleanExpression) or
             is_struct(value, Ash.Query.Call) or is_struct(value, Ash.Query.Ref) or
             is_struct(value, Ash.Query.Exists) or
             is_struct(value, Ash.Query.Parent) or
             (is_struct(value) and is_map_key(value, :__predicate__?)) do
    true
  end

  def expr?(value) when is_list(value) do
    Enum.any?(value, &expr?/1)
  end

  def expr?(value) when is_map(value) and not is_struct(value) do
    Enum.any?(value, fn {key, value} ->
      expr?(key) || expr?(value)
    end)
  end

  def expr?({left, right}) do
    expr?(left) || expr?(right)
  end

  def expr?(tuple) when is_tuple(tuple) do
    tuple |> Tuple.to_list() |> expr?()
  end

  def expr?(_), do: false

  @doc "A helper for using actor values in filter templates"
  def actor(value), do: {:_actor, value}

  @doc "A helper for using action arguments in filter templates"
  def arg(name), do: {:_arg, name}

  @doc "A helper for creating a reference"
  def ref(name), do: {:_ref, [], name}

  @doc "A helper for creating a parent reference"
  def parent(expr), do: {:_parent, [], expr}

  @doc "A helper for referring to the most recent atomic expression applied to an update field"
  def atomic_ref(expr), do: {:_atomic_ref, expr}

  @doc "A helper for creating a reference to a related path"
  def ref(path, name), do: {:_ref, path, name}

  @doc """
  A helper for using query context in filter templates

  An atom will just get the key, and a list will be accessed via `get_in`.
  """
  def context(name), do: {:_context, name}

  @doc "A helper for building an expression style filter"
  defmacro expr(expr) do
    quote do
      require Ash.Expr

      Ash.Expr.expr(unquote(expr))
    end
  end
end