Packages

ash

3.4.53
3.29.3 3.29.2 3.29.1 3.29.0 3.28.0 3.27.8 3.27.7 3.27.6 3.27.5 3.27.4 3.27.3 3.27.2 3.27.1 3.27.0 3.26.0 3.25.2 3.25.1 3.25.0 retired 3.24.7 3.24.6 3.24.5 3.24.4 3.24.3 3.24.2 3.24.1 3.24.0 3.23.1 3.23.0 3.22.2 3.22.1 3.22.0 3.21.3 3.21.2 3.21.1 3.21.0 3.20.0 3.19.3 3.19.2 3.19.1 3.19.0 3.18.0 3.17.1 3.17.0 3.16.0 3.15.0 3.14.1 3.14.0 retired 3.13.2 3.13.1 3.13.0 3.12.0 3.11.3 3.11.2 3.11.1 3.11.0 3.10.1 3.10.0 3.9.0 3.8.0 3.7.6 3.7.5 3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 retired 3.6.3 retired 3.6.2 3.6.1 3.6.0 3.5.43 3.5.42 3.5.41 3.5.40 3.5.39 3.5.38 3.5.37 3.5.36 3.5.35 3.5.34 3.5.33 3.5.32 3.5.31 3.5.30 3.5.29 3.5.28 3.5.27 3.5.26 3.5.25 3.5.24 3.5.23 3.5.22 3.5.21 3.5.20 3.5.19 3.5.18 3.5.17 3.5.16 3.5.15 3.5.14 3.5.13 3.5.12 3.5.11 3.5.10 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.74 retired 3.4.73 3.4.72 3.4.71 3.4.70 3.4.69 3.4.68 3.4.67 3.4.66 3.4.65 3.4.64 3.4.63 3.4.62 3.4.61 3.4.60 3.4.59 3.4.58 3.4.57 3.4.56 3.4.55 3.4.54 3.4.53 3.4.52 3.4.51 3.4.50 3.4.49 3.4.48 3.4.47 3.4.46 3.4.45 3.4.44 3.4.43 3.4.42 3.4.41 3.4.40 3.4.39 3.4.38 3.4.37 3.4.36 3.4.35 3.4.34 3.4.33 3.4.32 3.4.31 3.4.30 3.4.29 3.4.28 3.4.27 3.4.26 3.4.25 3.4.24 3.4.23 3.4.22 3.4.21 3.4.20 3.4.19 3.4.18 3.4.17 3.4.16 3.4.15 3.4.14 3.4.13 3.4.12 3.4.11 3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.3 3.3.2 3.3.1 3.3.0 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 3.2.0 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.16 3.0.15 3.0.14 3.0.13 3.0.12 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 3.0.0-rc.46 3.0.0-rc.45 3.0.0-rc.44 3.0.0-rc.43 3.0.0-rc.42 3.0.0-rc.41 3.0.0-rc.40 3.0.0-rc.39 3.0.0-rc.38 3.0.0-rc.37 3.0.0-rc.36 3.0.0-rc.35 3.0.0-rc.34 3.0.0-rc.33 3.0.0-rc.32 3.0.0-rc.31 3.0.0-rc.29 3.0.0-rc.28 3.0.0-rc.27 3.0.0-rc.26 3.0.0-rc.25 3.0.0-rc.24 3.0.0-rc.23 3.0.0-rc.22 3.0.0-rc.21 3.0.0-rc.20 3.0.0-rc.19 3.0.0-rc.18 3.0.0-rc.17 3.0.0-rc.16 3.0.0-rc.15 3.0.0-rc.14 3.0.0-rc.13 3.0.0-rc.12 3.0.0-rc.11 3.0.0-rc.10 3.0.0-rc.9 3.0.0-rc.8 3.0.0-rc.7 3.0.0-rc.6 3.0.0-rc.5 3.0.0-rc.4 3.0.0-rc.3 3.0.0-rc.1 3.0.0-rc.0 2.21.15 2.21.14 2.21.13 2.21.12 2.21.11 2.21.10 2.21.9 2.21.8 2.21.7 2.21.6 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.3 2.20.2 2.20.1 2.20.0 2.19.14 2.19.13 2.19.12 2.19.11 2.19.10 2.19.9 2.19.8 2.19.7 2.19.6 2.19.5 2.19.4 2.19.3 retired 2.19.2 retired 2.19.1 retired 2.19.0 retired 2.18.2 2.18.1 2.18.0 2.17.24 2.17.23 2.17.22 2.17.21 2.17.20 2.17.19 2.17.18 2.17.17 2.17.16 2.17.15 2.17.14 2.17.13 2.17.12 2.17.11 2.17.10 2.17.9 2.17.8 2.17.7 2.17.6 2.17.5 2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.1 2.16.0 2.15.20 2.15.19 2.15.18 2.15.17 2.15.16 2.15.15 2.15.14 2.15.13 2.15.12 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.2 2.15.1 2.15.0 2.14.21 2.14.20 2.14.19 2.14.18 2.14.17 2.14.16 2.14.15 2.14.14 2.14.13 2.14.12 2.14.11 2.14.10 2.14.9 2.14.8 2.14.7 2.14.6 2.14.5 2.14.4 2.14.3 2.14.2 2.14.1 2.14.0 2.13.4 retired 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.11 2.11.10 2.11.9 2.11.8 2.11.7 2.11.6 2.11.5 2.11.4 2.11.3 2.11.2 2.11.1 2.11.0 2.11.0-rc.3 2.11.0-rc.2 2.11.0-rc.1 2.11.0-rc.0 2.10.2 2.10.1 2.10.0 2.9.29 2.9.28 2.9.27 2.9.26 2.9.25 2.9.24 2.9.23 2.9.22 2.9.21 2.9.20 2.9.19 2.9.18 2.9.17 2.9.16 2.9.15 2.9.14 2.9.13 2.9.12 2.9.11 2.9.10 2.9.9 2.9.8 2.9.7 2.9.6 2.9.5 2.9.4 2.9.3 2.9.2 2.9.1 2.9.0 2.8.1 2.8.0 2.7.1 2.7.0 2.6.31 2.6.30 2.6.29 2.6.28 2.6.27 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.16 2.5.15 2.5.14 2.5.13 2.5.12 2.5.11 2.5.10 2.5.9 2.5.8 2.5.7 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.5.0-rc.6 2.5.0-rc.5 2.5.0-rc.4 2.5.0-rc.3 2.5.0-rc.2 2.5.0-rc.1 2.5.0-rc.0 2.4.30 2.4.29 2.4.28 2.4.27 2.4.26 2.4.25 2.4.24 2.4.23 2.4.22 2.4.21 2.4.20 2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 2.0.0-rc.15 2.0.0-rc.14 2.0.0-rc.13 2.0.0-rc.12 2.0.0-rc.11 2.0.0-rc.10 2.0.0-rc.9 2.0.0-rc.8 2.0.0-rc.7 2.0.0-rc.6 2.0.0-rc.5 2.0.0-rc.4 2.0.0-rc.3 2.0.0-rc.2 2.0.0-rc.1 2.0.0-rc.0 2.0.0-pre.8 2.0.0-pre.7 2.0.0-pre.6 2.0.0-pre.5 2.0.0-pre.4 2.0.0-pre.3 2.0.0-pre.2 2.0.0-pre.1 2.0.0-pre.0 1.53.3 1.53.2 1.53.0 1.52.0-rc.22 1.52.0-rc.21 1.52.0-rc.20 1.52.0-rc.19 1.52.0-rc.18 1.52.0-rc.17 1.52.0-rc.16 1.52.0-rc.15 1.52.0-rc.14 1.52.0-rc.13 1.52.0-rc.12 1.52.0-rc.11 1.52.0-rc.10 1.52.0-rc.9 1.52.0-rc.8 1.52.0-rc.7 1.52.0-rc.6 1.52.0-rc.5 1.52.0-rc.4 1.52.0-rc.3 1.52.0-rc.2 1.52.0-rc.1 1.52.0-rc.0 1.51.2 1.51.1 retired 1.51.0 1.50.21 1.50.20 1.50.19 1.50.18 1.50.17 1.50.16 1.50.15 1.50.14 1.50.13 1.50.12 1.50.11 1.50.10 1.50.9 1.50.8 1.50.7 1.50.6 1.50.5 1.50.4 1.50.3 1.50.2 1.50.1 1.50.0 1.49.0 1.48.0-rc.30 1.48.0-rc.29 1.48.0-rc.28 1.48.0-rc.27 1.48.0-rc.26 1.48.0-rc.25 1.48.0-rc.24 1.48.0-rc.23 1.48.0-rc.22 1.48.0-rc.21 1.48.0-rc.20 1.48.0-rc.19 1.48.0-rc.18 1.48.0-rc.17 1.48.0-rc.16 1.48.0-rc.15 1.48.0-rc.14 1.48.0-rc.13 1.48.0-rc.12 1.48.0-rc.11 1.48.0-rc.10 1.48.0-rc.9 1.48.0-rc.8 1.48.0-rc.7 1.48.0-rc.6 1.48.0-rc.5 1.48.0-rc.4 1.48.0-rc.3 1.48.0-rc.2 1.48.0-rc.1 1.48.0-rc.0 1.47.12 1.47.11 1.47.10 1.47.9 1.47.8 1.47.7 1.47.6 1.47.5 1.47.4 1.47.3 1.47.2 1.47.1 1.47.0 1.46.13 1.46.12 1.46.11 1.46.10 1.46.9 1.46.8 1.46.7 1.46.6 1.46.5 1.46.4 1.46.3 1.46.2 1.46.1 1.46.0 1.45.0-rc9 1.45.0-rc8 1.45.0-rc7 1.45.0-rc6 1.45.0-rc5 1.45.0-rc4 1.45.0-rc3 1.45.0-rc20 1.45.0-rc2 1.45.0-rc19 1.45.0-rc18 1.45.0-rc17 1.45.0-rc16 1.45.0-rc15 1.45.0-rc14 1.45.0-rc13 1.45.0-rc12 1.45.0-rc11 1.45.0-rc10 1.45.0-rc1 1.45.0-rc0 1.44.13 1.44.12 1.44.11 1.44.10 1.44.9 1.44.8 1.44.7 1.44.6 1.44.5 1.44.4 1.44.3 1.44.2 1.44.1 1.44.0 1.43.12 1.43.11 1.43.10 1.43.9 1.43.8 1.43.7 1.43.6 1.43.5 1.43.4 1.43.3 1.43.2 1.43.1 1.43.0 1.42.0 1.41.12 1.41.11 1.41.10 1.41.9 1.41.8 1.41.7 1.41.6 1.41.5 1.41.4 1.41.3 1.41.2 1.41.1 1.41.0 1.40.0 1.39.7 1.39.6 1.39.5 1.39.4 1.39.3 1.39.2 1.39.1 1.39.0 1.38.0 1.37.2 1.37.1 1.37.0 1.36.22 1.36.21 1.36.19 1.36.18 1.36.17 1.36.16 1.36.15 1.36.14 1.36.13 1.36.12 1.36.11 1.36.10 1.36.9 1.36.8 1.36.7 1.36.6 1.36.5 1.36.4 1.36.3 1.36.2 1.36.0 1.35.1 1.35.0 1.34.9 1.34.8 1.34.7 1.34.6 1.34.5 1.34.4 1.34.3 1.34.2 1.34.1 1.34.0 1.33.0 1.32.2 1.32.1 1.32.0 1.31.1 1.31.0 1.30.2 1.30.1 1.29.0-rc1 1.29.0-rc0 1.28.1 1.28.0 1.27.1 1.27.0 1.26.13 1.26.12 1.26.11 1.26.10 1.26.9 1.26.8 1.26.7 1.26.6 1.26.5 1.26.4 1.26.2 1.26.1 1.26.0 1.25.8 1.25.7 1.25.6 1.25.5 1.25.4 1.25.3 1.25.2 1.25.1 1.25.0 1.24.2 1.24.1 1.24.0 1.23.3 1.23.2 1.23.1 1.23.0 1.22.1 1.22.0 1.20.1 1.20.0 1.19.1 1.19.0 1.18.1 1.18.0 1.17.1 1.17.0 1.16.2 1.15.1 1.15.0 1.14.0 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.12.0 1.11.1 1.11.0 1.10.0 1.9.0 1.8.0 1.7.0 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.1 1.5.0 1.4.1 1.4.0 1.3.1 1.3.0 1.2.1 1.2.0 1.1.3 1.1.2 1.1.0 1.0.3 1.0.2 1.0.1 1.0.0 0.13.1 0.13.0 0.12.0 0.10.0 0.9.1 0.9.0 0.8.0 0.7.0 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.2 0.5.1 0.5.0 0.4.0 0.3.0 0.2.0 0.1.9 0.1.8 0.1.3 0.1.1 0.1.0

A declarative, extensible framework for building Elixir applications.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash expr expr.ex
Raw

lib/ash/expr/expr.ex

defmodule Ash.Expr do
@moduledoc "Tools to build Ash expressions"
alias Ash.Query.{BooleanExpression, Not}
@type t :: any
@pass_through_funcs [:where, :or_where, :expr, :@]
@doc """
Evaluate an expression. See `eval/2` for more.
"""
def eval!(expression, opts \\ []) do
case eval(expression, opts) do
{:ok, result} ->
result
{:error, error} ->
raise Ash.Error.to_ash_error(error)
end
end
@doc """
Evaluate an expression. This function only works if you have no references, or if you provide the `record` option.
"""
def eval(expression, opts \\ []) do
context =
opts[:context]
|> Kernel.||(%{})
|> Map.put_new(:resource, opts[:resource])
|> Map.put(:eval?, true)
expression
|> Ash.Filter.hydrate_refs(context)
|> case do
{:ok, hydrated} ->
eval_hydrated(hydrated, opts)
{:error, error} ->
{:error, error}
end
end
@doc "Returns true if the value is or contains an expression"
@spec expr?(term) :: boolean()
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, Ash.Query.UpsertConflict) or
is_struct(value, Ash.CustomExpression) 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 template helper for using actor values in filter templates"
def actor(value), do: {:_actor, value}
@doc "A template helper for using action arguments in filter templates"
def arg(name), do: {:_arg, name}
@doc "A template helper for creating a reference"
def ref(name) when is_atom(name), do: {:_ref, [], name}
@doc "A template helper for creating a reference to a related path"
def ref(path, name) when is_list(path) and is_atom(name), do: {:_ref, path, name}
@doc "A template helper for creating a parent reference"
def parent(expr), do: {:_parent, [], expr}
@doc "A template helper for referring to the most recent atomic expression applied to an update field"
def atomic_ref(expr), do: {:_atomic_ref, expr}
@doc """
A template helper for using query context in filter templates
An atom will get the value for a key, and a list will be accessed via `get_in`.
"""
def context(name), do: {:_context, name}
@doc false
def eval_hydrated(expression, opts \\ []) do
Ash.Filter.Runtime.load_and_eval(
opts[:record],
expression,
opts[:parent],
opts[:resource],
opts[:domain],
opts[:unknown_on_unknown_refs?],
opts[:actor],
opts[:tenant]
)
end
@spec where(Macro.t(), Macro.t()) :: t
defmacro where(left, right) do
quote do
Ash.Query.BooleanExpression.optimized_new(
:and,
Ash.Expr.expr(unquote(left)),
Ash.Expr.expr(unquote(right))
)
end
end
@spec or_where(Macro.t(), Macro.t()) :: t
defmacro or_where(left, right) do
quote do
Ash.Query.BooleanExpression.optimized_new(
:or,
Ash.Expr.expr(unquote(left)),
Ash.Expr.expr(unquote(right))
)
end
end
@spec expr(Macro.t()) :: t()
defmacro expr(do: body) do
quote location: :keep do
Ash.Expr.expr(unquote(body))
end
end
defmacro expr(body) do
expr = do_expr(body)
quote location: :keep do
unquote(expr)
end
end
@doc false
def fill_template(
template,
actor \\ nil,
args \\ %{},
context \\ %{},
changeset \\ nil
) do
walk_template(template, fn
{:_actor, :_primary_key} ->
if actor do
Map.take(actor, Ash.Resource.Info.primary_key(actor.__struct__))
end
{:_actor, field} when is_atom(field) or is_binary(field) ->
Map.get(actor || %{}, field)
{:_actor, path} when is_list(path) ->
get_path(actor || %{}, path)
{:_arg, field} ->
case Map.fetch(args, field) do
:error ->
Map.get(args, to_string(field))
{:ok, value} ->
value
end
{:_atomic_ref, field} when is_atom(field) ->
if changeset do
Ash.Changeset.atomic_ref(changeset, field)
else
{:_atomic_ref, field}
end
{:_context, fields} when is_list(fields) ->
get_path(context, fields)
{:_context, field} ->
Map.get(context, field)
{:_ref, path, name} ->
%Ash.Query.Ref{
attribute: fill_template(name, actor, args, context),
relationship_path: fill_template(path, actor, args, context)
}
other ->
other
end)
end
@doc false
def get_path(map, [key]) when is_map(map) do
Map.get(map, key)
end
def get_path(map, [key | rest]) when is_map(map) do
get_path(get_path(map, [key]), rest)
end
def get_path(_, _), do: nil
@doc false
def template_references_actor?(template) do
template_references?(template, fn
{:_actor, _} -> true
_ -> false
end)
end
def template_references_argument?(template) do
template_references?(template, fn
{:_arg, _} -> true
_ -> false
end)
end
def template_references_context?(template) do
template_references?(template, fn
{:_context, _} -> true
_ -> false
end)
end
def can_return_nil?(nil), do: true
def can_return_nil?(%Ash.Query.BooleanExpression{left: left, right: right}) do
can_return_nil?(left) || can_return_nil?(right)
end
def can_return_nil?(%Ash.Query.Not{expression: expression}) do
can_return_nil?(expression)
end
def can_return_nil?(%Ash.Query.Parent{expr: expr}) do
can_return_nil?(expr)
end
def can_return_nil?(%Ash.Query.UpsertConflict{}), do: true
def can_return_nil?(%Ash.Query.Exists{}), do: false
def can_return_nil?(%mod{__predicate__?: _} = pred) do
mod.can_return_nil?(pred)
end
def can_return_nil?(%Ash.Query.Ref{attribute: %{allow_nil?: false}}), do: false
def can_return_nil?(_), do: true
@doc "Whether or not a given template contains an actor reference"
def template_references?(%{__struct__: Ash.Filter, expression: expression}, pred) do
template_references?(expression, pred)
end
def template_references?(%BooleanExpression{op: :and, left: left, right: right}, pred) do
template_references?(left, pred) || template_references?(right, pred)
end
def template_references?(%Not{expression: expression}, pred) do
template_references?(expression, pred)
end
def template_references?(%Ash.Query.Exists{expr: expr}, pred) do
template_references?(expr, pred)
end
def template_references?(%Ash.Query.Parent{expr: expr}, pred) do
template_references?(expr, pred)
end
def template_references?(
%Ash.CustomExpression{expression: expression, simple_expression: simple_expression},
pred
) do
template_references?(expression, pred) || template_references?(simple_expression, pred)
end
def template_references?(%{left: left, right: right}, pred) do
template_references?(left, pred) || template_references?(right, pred)
end
def template_references?(%{arguments: args}, pred) do
Enum.any?(args, &template_references?(&1, pred))
end
def template_references?(%Ash.Query.Call{args: args}, pred) do
Enum.any?(args, &template_references?(&1, pred))
end
def template_references?(list, pred) when is_list(list) do
Enum.any?(list, &template_references?(&1, pred))
end
def template_references?(map, pred) when is_map(map) and not is_struct(map) do
Enum.any?(map, &template_references?(&1, pred))
end
def template_references?(tuple, pred) when is_tuple(tuple) do
pred.(tuple) ||
tuple
|> Tuple.to_list()
|> Enum.any?(&template_references?(&1, pred))
end
def template_references?(thing, pred), do: pred.(thing)
@doc false
def walk_template(filter, mapper) when is_list(filter) do
case mapper.(filter) do
^filter ->
Enum.map(filter, &walk_template(&1, mapper))
other ->
walk_template(other, mapper)
end
end
def walk_template(%BooleanExpression{left: left, right: right} = expr, mapper) do
case mapper.(expr) do
^expr ->
%{
expr
| left: walk_template(left, mapper),
right: walk_template(right, mapper)
}
other ->
walk_template(other, mapper)
end
end
def walk_template(%Not{expression: expression} = not_expr, mapper) do
case mapper.(not_expr) do
^not_expr ->
%{not_expr | expression: walk_template(expression, mapper)}
other ->
walk_template(other, mapper)
end
end
def walk_template(%{__struct__: Ash.Filter, expression: expression} = filter, mapper) do
%{filter | expression: walk_template(expression, mapper)}
end
def walk_template(%Ash.Query.Parent{expr: expr} = this_expr, mapper) do
case mapper.(this_expr) do
^this_expr ->
%{this_expr | expr: walk_template(expr, mapper)}
other ->
walk_template(other, mapper)
end
end
def walk_template(%Ash.Query.Exists{expr: expr} = exists_expr, mapper) do
case mapper.(exists_expr) do
^exists_expr ->
%{exists_expr | expr: walk_template(expr, mapper)}
other ->
walk_template(other, mapper)
end
end
def walk_template(%{__predicate__?: _, left: left, right: right} = pred, mapper) do
case mapper.(pred) do
^pred ->
%{
pred
| left: walk_template(left, mapper),
right: walk_template(right, mapper)
}
other ->
walk_template(other, mapper)
end
end
def walk_template(%{__predicate__?: _, arguments: arguments} = func, mapper) do
case mapper.(func) do
^func ->
%{
func
| arguments: Enum.map(arguments, &walk_template(&1, mapper))
}
other ->
walk_template(other, mapper)
end
end
def walk_template(%Ash.Query.Call{args: args} = call, mapper) do
case mapper.(call) do
^call ->
%{
call
| args: Enum.map(args, &walk_template(&1, mapper))
}
other ->
walk_template(other, mapper)
end
end
def walk_template(filter, mapper) when is_map(filter) do
if Map.has_key?(filter, :__struct__) do
filter
else
case mapper.(filter) do
^filter ->
Enum.into(filter, %{}, &walk_template(&1, mapper))
other ->
walk_template(other, mapper)
end
end
end
def walk_template(tuple, mapper) when is_tuple(tuple) do
case mapper.(tuple) do
^tuple ->
tuple
|> Tuple.to_list()
|> Enum.map(&walk_template(&1, mapper))
|> List.to_tuple()
other ->
walk_template(other, mapper)
end
end
def walk_template(value, mapper), do: mapper.(value)
@operator_symbols Ash.Query.Operator.operator_symbols() -- [:is_nil]
@doc false
def do_expr(expr, escape? \\ true)
def do_expr({:|>, _, [first, {func, meta, args}]}, escape?) do
do_expr({func, meta, [first | args]}, escape?)
end
def do_expr({func, _, _} = expr, _escape?) when func in @pass_through_funcs do
expr
end
def do_expr({{:., _, [_, func]}, _, _} = expr, _escape?)
when func in @pass_through_funcs do
expr
end
def do_expr({op, _, nil}, escape?) when is_atom(op) do
soft_escape(%Ash.Query.Ref{relationship_path: [], attribute: op}, escape?)
end
def do_expr({op, _, Elixir}, escape?) when is_atom(op) do
soft_escape(%Ash.Query.Ref{relationship_path: [], attribute: op}, escape?)
end
def do_expr({:__aliases__, _, _} = expr, _escape?) do
expr
end
def do_expr({:^, _, [value]}, _escape?) do
value
end
def do_expr({{:., _, [Access, :get]}, _, [left, right]}, escape?) do
left = do_expr(left, false)
right = do_expr(right, false)
soft_escape(
quote do
[unquote(left), unquote(right)]
|> Ash.Query.Function.GetPath.new()
|> case do
{:ok, call} ->
call
{:error, error} ->
raise error
end
end,
escape?
)
end
def do_expr({{:., _, [_, _]} = left, _, []}, escape?) do
do_expr(left, escape?)
end
def do_expr(value, escape?) when is_list(value) do
Enum.map(value, &do_expr(&1, escape?))
end
def do_expr({:%{}, _, keys}, escape?) do
{:%{}, [],
Enum.map(keys, fn {key, value} -> {do_expr(key, escape?), do_expr(value, escape?)} end)}
end
def do_expr({{:., _, [at_path, :exists]}, _, [path, expr]}, escape?) do
expr_with_at_path(path, at_path, expr, Ash.Query.Exists, escape?)
end
def do_expr({{:., _, [_, _]} = left, _, args}, escape?) do
args = Enum.map(args, &do_expr(&1, false))
case do_expr(left, escape?) do
{:%{}, [], parts} = other when is_list(parts) ->
if Enum.any?(parts, &(&1 == {:__struct__, Ash.Query.Ref})) do
ref = Map.new(parts)
soft_escape(
%Ash.Query.Call{
name: ref.attribute,
relationship_path: ref.relationship_path,
args: args,
operator?: false
},
escape?
)
else
other
end
%Ash.Query.Ref{} = ref ->
soft_escape(
%Ash.Query.Call{
name: ref.attribute,
relationship_path: ref.relationship_path,
args: args,
operator?: false
},
escape?
)
other ->
other
end
end
def do_expr({:ref, _, [field, path]} = expr, _escape?) do
raise ArgumentError, """
Usage of `ref/1` and `ref/2` must now be pinned, got: #{Macro.to_string(expr)}.
For example: `^ref(#{Macro.to_string(remove_pin(field))}, #{Macro.to_string(remove_pin(path))})`
"""
end
def do_expr({:ref, _, [field]} = expr, _escape?) do
raise ArgumentError, """
Usage of `ref/1` and `ref/2` must now be pinned, got: #{Macro.to_string(expr)}.
For example: `^ref(#{Macro.to_string(remove_pin(field))}})`
"""
end
def do_expr(
{:<<>>, meta,
[
{:"::", _meta1,
[{{:., _meta2, [Kernel, :to_string]}, _meta3, [left]}, {:binary, _, _}]}
]},
escape?
) do
do_expr({:type, meta, [left, :string]}, escape?)
end
def do_expr(
{:<<>>, meta, [second_to_last, last]},
escape?
) do
do_expr({:<>, meta, [second_to_last, last]}, escape?)
end
def do_expr({:<<>>, _meta, [single]}, _escape?) do
single
end
def do_expr(
{:<<>>, meta, [next | rest]},
escape?
) do
do_expr({:<>, meta, [next, do_expr({:<<>>, meta, rest}, escape?)]}, escape?)
end
def do_expr(
{:"::", meta, [{{:., _meta1, [Kernel, :to_string]}, _meta2, [left]}, {:binary, _, _}]},
escape?
) do
do_expr({:type, meta, [left, :string]}, escape?)
end
def do_expr({:., _, [left, right]} = ref, escape?) when is_atom(right) do
case do_ref(left, right) do
%Ash.Query.Ref{} = ref ->
soft_escape(ref, escape?)
:error ->
raise "Invalid reference! #{Macro.to_string(ref)}"
end
end
def do_expr({op, _, args}, escape?) when op in [:and, :or] do
args = Enum.map(args, &do_expr(&1, false))
soft_escape(BooleanExpression.optimized_new(op, Enum.at(args, 0), Enum.at(args, 1)), escape?)
end
def do_expr({op, _, [_, _] = args}, escape?)
when is_atom(op) and op in @operator_symbols do
args = Enum.map(args, &do_expr(&1, false))
if op == :== do
soft_escape(
quote do
args = unquote(args)
call = %Ash.Query.Call{name: unquote(op), args: args, operator?: true}
if Enum.any?(args, &is_nil/1) do
IO.warn(
"Comparing values with `nil` will always return `false`. Use `is_nil/1` instead. In: `#{inspect(call)}`"
)
end
call
end,
escape?
)
else
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: true}, escape?)
end
end
def do_expr({parent, _, [expr]}, escape?) when parent in [:parent, :source, :parent_expr] do
expr = do_expr(expr, escape?)
soft_escape(
quote do
Ash.Query.Parent.new(unquote(expr))
end,
escape?
)
end
def do_expr({:upsert_conflict, _, [expr]}, escape?) do
expr = do_expr(expr, escape?)
soft_escape(
quote do
Ash.Query.UpsertConflict.new(unquote(expr))
end,
escape?
)
end
def do_expr({:exists, _, [path, original_expr]}, escape?) do
expr_with_at_path(path, [], original_expr, Ash.Query.Exists, escape?)
end
def do_expr({left, _, [{op, _, [right]}]}, escape?)
when is_atom(op) and op in @operator_symbols and is_atom(left) and left != :not do
do_expr({op, [], [left, right]}, escape?)
end
def do_expr({:not, _, [expression]}, escape?) do
expression = do_expr(expression, false)
soft_escape(Not.new(expression), escape?)
end
def do_expr({:cond, _, [[do: options]]}, escape?) do
options
|> Enum.map(fn {:->, _, [condition, result]} ->
{condition, result}
end)
|> cond_to_if_tree()
|> do_expr(escape?)
end
def do_expr({:lazy, _, args}, escape?) do
soft_escape(%Ash.Query.Call{name: :lazy, args: args, operator?: false}, escape?)
end
def do_expr({:sigil_i, _, [{:<<>>, _, [str]}, mods]}, escape?) do
soft_escape(Ash.CiString.sigil_i(str, mods), escape?)
end
def do_expr({:fragment, _, [first | _] = args}, escape?)
when is_binary(first) or is_function(first) do
last_arg = List.last(args)
args =
if Keyword.keyword?(last_arg) && Keyword.has_key?(last_arg, :do) do
Enum.map(:lists.droplast(args), &do_expr(&1, false)) ++
[
Enum.map(last_arg, fn {key, arg_value} ->
{key, do_expr(arg_value, false)}
end)
]
else
Enum.map(args, &do_expr(&1, false))
end
soft_escape(%Ash.Query.Call{name: :fragment, args: args, operator?: false}, escape?)
end
def do_expr(
{:&, _,
[
{:/, _,
[
{{:., _, [{:__aliases__, _, [_]}, _]}, _, []},
_
]}
]} = expr,
_
) do
expr
end
def do_expr(
{:&, _,
[
{:/, _,
[
{{:., _, [v, _]}, _, []},
_
]}
]} = expr,
_
)
when is_atom(v) do
expr
end
def do_expr(
{:&, _,
[
{:/, _,
[
{{:., _, [{mod, _, context}, _]}, _, []},
_
]}
]} = expr,
_
)
when is_atom(mod) and is_atom(context) do
expr
end
def do_expr(
{:&, _, _} = expr,
_
) do
raise """
The only kind of anonymous functions allowed in expressions are in the format `&Module.function/arity`.
Got: #{Macro.to_string(expr)}
"""
end
def do_expr(
{:fn, _, _} = expr,
_
) do
raise """
The only kind of anonymous functions allowed in expressions are in the format `&Module.function/arity`.
Got: #{Macro.to_string(expr)}
"""
end
def do_expr({:fragment, _, [{:&, _, _} | _] = args}, escape?) do
last_arg = List.last(args)
args =
if Keyword.keyword?(last_arg) && Keyword.has_key?(last_arg, :do) do
Enum.map(:lists.droplast(args), &do_expr(&1, false)) ++
[
Enum.map(last_arg, fn {key, arg_value} ->
{key, do_expr(arg_value, false)}
end)
]
else
Enum.map(args, &do_expr(&1, false))
end
soft_escape(%Ash.Query.Call{name: :fragment, args: args, operator?: false}, escape?)
end
def do_expr({:fragment, _, [{m, f, a} | _] = args}, escape?)
when is_atom(m) and is_atom(f) and is_list(a) do
last_arg = List.last(args)
args =
if Keyword.keyword?(last_arg) && Keyword.has_key?(last_arg, :do) do
Enum.map(:lists.droplast(args), &do_expr(&1, false)) ++
[
Enum.map(last_arg, fn {key, arg_value} ->
{key, do_expr(arg_value, false)}
end)
]
else
Enum.map(args, &do_expr(&1, false))
end
soft_escape(%Ash.Query.Call{name: :fragment, args: args, operator?: false}, escape?)
end
def do_expr({:fragment, _, [first | _]}, _escape?) do
raise """
To prevent SQL injection attacks, fragment(...) allows only two specific kinds of values
1. A string literal *not* interpolated. This is for use with data layers like `AshPostgres.
2. A one argument function or an MFA *not* interpolated. This is for use with data layers like `Ash.DataLayer.Simple` and `Ash.DataLayer.Ets`.
Got: #{Macro.to_string(first)}
"""
end
def do_expr({op, _, args}, escape?) when is_atom(op) and is_list(args) do
last_arg = List.last(args)
args =
if Keyword.keyword?(last_arg) && Keyword.has_key?(last_arg, :do) do
Enum.map(:lists.droplast(args), &do_expr(&1, false)) ++
[
Enum.map(last_arg, fn {key, arg_value} ->
{key, do_expr(arg_value, false)}
end)
]
else
Enum.map(args, &do_expr(&1, false))
end
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: false}, escape?)
end
def do_expr({left, _, _}, escape?) when is_tuple(left), do: do_expr(left, escape?)
def do_expr({left, right}, escape?) do
left = do_expr(left, escape?)
right = do_expr(right, escape?)
soft_escape({left, right}, escape?)
end
def do_expr(other, _), do: other
def determine_types(mod, args, returns \\ nil, nested? \\ false)
def determine_types(Ash.Query.Function.Type, [_, type], _returns, _nested?) do
{type, []}
end
def determine_types(Ash.Query.Function.Type, [_, type, constraints], _returns, _nested?) do
{type, constraints}
end
def determine_types(mod, values, known_result, _nested?) do
Code.ensure_compiled(mod)
name =
cond do
function_exported?(mod, :operator, 0) ->
mod.operator()
function_exported?(mod, :name, 0) ->
mod.name()
true ->
nil
end
cond do
:erlang.function_exported(mod, :types, 0) ->
{mod.types(), mod.returns()}
:erlang.function_exported(mod, :args, 0) ->
{mod.args(), mod.returns()}
true ->
{[:any], [:any]}
end
|> then(fn {types, returns} ->
if types == :var_args || returns == :no_return || returns == :unknown do
[]
else
overloads = Ash.Query.Operator.operator_overloads(name) || %{}
more_types = Map.keys(overloads)
more_returns = Map.values(overloads)
types = Enum.concat(types, List.wrap(more_types))
returns = Enum.concat(returns, List.wrap(more_returns))
returns =
Enum.map(returns, fn
{:array, any} when any in [:same, :any] -> {:array, any}
any when any in [:same, :any] -> any
{type, constraints} -> {type, constraints}
type -> {type, []}
end)
Enum.zip(types, returns)
end
end)
|> Enum.reject(fn {typeset, _} -> typeset == :any end)
|> Enum.filter(fn {typeset, _} ->
typeset == :same ||
length(typeset) == length(values)
end)
|> Enum.map(fn {typeset, returns} ->
basis =
cond do
!returns ->
nil
returns == :same ->
known_result
returns == {:array, :same} ->
case known_result do
{:array, type} ->
case type do
{type, constraints} ->
{type, constraints}
type ->
{type, []}
end
_ ->
nil
end
true ->
nil
end
types_and_values =
if typeset == :same do
Enum.map(values, &{:same, &1})
else
Enum.zip(typeset, values)
end
types_and_values
|> Enum.with_index()
|> Enum.reduce_while(
%{must_adopt_basis: [], basis: basis, types: [], fallback_basis: nil},
fn
{{vague_type, value}, index}, acc when vague_type in [:any, :same] ->
case determine_type(value) do
{:ok, {type, constraints}} ->
case acc[:basis] do
nil ->
if vague_type == :any do
acc = Map.update!(acc, :types, &[{type, constraints} | &1])
{:cont, Map.put(acc, :basis, {type, constraints})}
else
acc =
acc
|> Map.update!(:types, &[nil | &1])
|> Map.put(:fallback_basis, {type, constraints})
{:cont, Map.update!(acc, :must_adopt_basis, &[{index, fn x -> x end} | &1])}
end
{^type, matched_constraints} ->
{:cont, Map.update!(acc, :types, &[{type, matched_constraints} | &1])}
_ ->
{:halt, :error}
end
:error ->
acc =
case acc[:fallback_basis] do
{type, constraints} ->
if !Ash.Expr.expr?(value) && !matches_type?(type, value, constraints) do
# Map.put(acc, :fallback_basis, nil)
acc
else
acc
end
_ ->
acc
end
acc = Map.update!(acc, :types, &[nil | &1])
{:cont, Map.update!(acc, :must_adopt_basis, &[{index, fn x -> x end} | &1])}
end
{{{:array, vague_type}, value}, index}, acc when vague_type in [:any, :same] ->
case determine_type(value) do
{:ok, {{:array, type}, constraints}} ->
case acc[:basis] do
nil ->
if vague_type == :any do
acc = Map.update!(acc, :types, &[{:array, {type, constraints}} | &1])
{:cont, Map.put(acc, :basis, {type, constraints})}
else
acc =
acc
|> Map.update!(:types, &[nil | &1])
|> Map.put(:fallback_basis, {type, constraints})
{:cont,
Map.update!(
acc,
:must_adopt_basis,
&[
{index,
fn {type, constraints} -> {{:array, type}, items: constraints} end}
| &1
]
)}
end
{^type, matched_constraints} ->
{:cont,
Map.update!(acc, :types, &[{:array, {type, matched_constraints}} | &1])}
_ ->
{:halt, :error}
end
_ ->
acc = Map.update!(acc, :types, &[nil | &1])
{:cont,
Map.update!(
acc,
:must_adopt_basis,
&[
{index, fn {type, constraints} -> {{:array, type}, items: constraints} end}
| &1
]
)}
end
{{{type, constraints}, value}, _index}, acc ->
determined_type = determine_type(value)
cond do
!Ash.Expr.expr?(value) && !matches_type?(type, value, constraints) ->
case Ash.Type.cast_input(type, value, constraints) do
{:ok, _} ->
{:cont, Map.update!(acc, :types, &[{type, constraints} | &1])}
_ ->
{:halt, :error}
end
match?({:ok, {determined_type, _}} when determined_type != type, determined_type) ->
{:halt, :error}
Ash.Expr.expr?(value) ->
{:cont, Map.update!(acc, :types, &[{type, constraints} | &1])}
true ->
{:cont, Map.update!(acc, :types, &[{type, constraints} | &1])}
end
{{type, value}, _index}, acc ->
determined_type = determine_type(value)
cond do
!Ash.Expr.expr?(value) && !matches_type?(type, value, []) ->
case Ash.Type.cast_input(type, value, []) do
{:ok, _} ->
{:cont, Map.update!(acc, :types, &[{type, []} | &1])}
_ ->
{:halt, :error}
end
match?({:ok, {determined_type, _}} when determined_type != type, determined_type) ->
{:halt, :error}
match?({:ok, _}, determined_type) ->
{:cont, Map.update!(acc, :types, &[elem(determined_type, 1) | &1])}
Ash.Expr.expr?(value) ->
{:cont, Map.update!(acc, :types, &[{type, []} | &1])}
true ->
{:cont, Map.update!(acc, :types, &[{type, []} | &1])}
end
end
)
|> then(fn
%{basis: nil, fallback_basis: fallback_basis} = data when not is_nil(fallback_basis) ->
%{data | basis: fallback_basis}
data ->
data
end)
|> case do
:error ->
nil
%{basis: nil, must_adopt_basis: [], types: types} ->
if returns not in [:same, :any, {:array, :same}, {:array, :any}] do
{Enum.reverse(types), returns, Enum.count(types)}
end
%{basis: nil, must_adopt_basis: _} ->
nil
%{basis: basis, must_adopt_basis: basis_adopters, types: types} ->
returns =
case returns do
same when same in [:same, :any] ->
basis
same when same in [{:array, :same}, {:array, :any}] ->
{type, constraints} = basis
{{:array, type}, items: constraints}
other ->
other
end
{basis_adopters
|> Enum.reduce(
Enum.reverse(types),
fn {index, function_of_basis}, types ->
List.replace_at(types, index, function_of_basis.(basis))
end
), returns, Enum.count(basis_adopters)}
end
end)
|> Enum.filter(& &1)
|> case do
[{types, returns, _}] ->
{types, returns}
types ->
select_matches(types, length(values), values)
end
end
defp select_matches([], value_count, _values) do
{Enum.map(1..value_count, fn _ -> nil end), nil}
end
defp select_matches(results, value_count, values) do
case Enum.find(results, fn
{_type, _returns, 0} ->
true
_ ->
false
end) do
{type, returns, 0} ->
{type, returns}
_ ->
results
|> Enum.map(fn {types, {type, constraints}, _} ->
types =
Enum.map(types, fn {type, constraints} ->
{Ash.Type.get_type(type), constraints}
end)
{types, {Ash.Type.get_type(type), constraints}}
end)
|> Enum.reject(fn {types, _} ->
types
|> Enum.zip(values)
|> Enum.any?(fn {{type, constraints}, value} ->
!Ash.Expr.expr?(value) and
!(matches_type?(type, value, constraints) ||
match?({:ok, _}, Ash.Type.cast_input(type, value, constraints)))
end)
end)
|> case do
[] ->
{Enum.map(1..value_count, fn _ -> nil end), nil}
results ->
arg_types =
1..value_count
|> Enum.map(fn i ->
possible_types =
Enum.map(results, fn {types, _} ->
Enum.at(types, i - 1)
end)
case Enum.find(possible_types, fn {type, constraints} ->
matches_type?(type, Enum.at(values, i - 1), constraints)
end) do
type when not is_nil(type) ->
type
nil ->
case Enum.uniq_by(possible_types, &elem(&1, 0)) do
[single] ->
Enum.find(possible_types, single, fn {_, constraints} ->
constraints != []
end)
_ ->
nil
end
end
end)
all_returns = Enum.map(results, &elem(&1, 1))
case Enum.find_value(results, fn {types, returns} ->
if types == arg_types do
returns
end
end) do
nil ->
case Enum.uniq(all_returns) do
[single] ->
{arg_types, single}
_ ->
{arg_types, nil}
end
case Enum.uniq_by(all_returns, &elem(&1, 0)) do
[single] ->
{arg_types,
Enum.find(all_returns, single, fn {_, constraints} -> constraints != [] end)}
_ ->
{arg_types, nil}
end
returns ->
{arg_types, returns}
end
end
end
end
def determine_type(value) do
case value do
%{__struct__: Ash.Query.Function.Type, arguments: [_, type, constraints]} ->
if Ash.Type.ash_type?(type) do
{:ok, {type, constraints}}
else
:error
end
%{__struct__: Ash.Query.Function.Type, arguments: [_, type]} ->
if Ash.Type.ash_type?(type) do
{:ok, {type, []}}
else
:error
end
%{__struct__: Ash.Query.Ref, attribute: %{type: type, constraints: constraints}} ->
if Ash.Type.ash_type?(type) do
{:ok, {type, constraints}}
else
:error
end
%{__struct__: Ash.Query.Ref, attribute: %{type: type}} ->
if Ash.Type.ash_type?(type) do
{:ok, {type, []}}
else
:error
end
%{__predicate__?: true} ->
{:ok, {:boolean, []}}
%{__struct__: Ash.Query.BooleanExpression} ->
{:ok, {:boolean, []}}
%{__struct__: Ash.Query.Exists} ->
{:ok, {:boolean, []}}
%{__struct__: Ash.Query.Parent, expr: expr} ->
determine_type(expr)
%{__struct__: Ash.Query.UpsertConflict, expr: expr} ->
determine_type(expr)
%mod{__predicate__?: _, arguments: arguments} ->
case determine_types(mod, arguments, nil, true) do
{_, nil} -> :error
{_, type} -> {:ok, type}
end
%mod{__predicate__?: _, left: left, right: right} ->
case determine_types(mod, [left, right], nil, true) do
{_, nil} -> :error
{_, type} -> {:ok, type}
end
_ ->
:error
end
end
defp matches_type?(type, value, constraints) do
type = Ash.Type.get_type(type)
Ash.Type.ash_type?(type) && Ash.Type.matches_type?(type, value, constraints)
end
defp expr_with_at_path(path, at_path, expr, struct, escape?) do
expr = do_expr(expr, escape?)
path =
case path do
{:^, _, [value]} ->
value
{:., _, [left, right]} ->
ref = do_ref(left, right)
ref.relationship_path ++ [ref.attribute]
{{:., _, [left, right]}, _, _} ->
ref = do_ref(left, right)
ref.relationship_path ++ [ref.attribute]
{atom, _, _} when is_atom(atom) ->
[atom]
path when is_list(path) ->
path
other ->
raise "Invalid value used in the first argument in exists, i.e exists(#{Macro.to_string(other)}, #{Macro.to_string(expr)})"
end
at_path =
case at_path do
{:^, _, [value]} ->
value
{:., _, [left, right]} ->
ref = do_ref(left, right)
ref.relationship_path ++ [ref.attribute]
{{:., _, [left, right]}, _, _} ->
ref = do_ref(left, right)
ref.relationship_path ++ [ref.attribute]
{atom, _, _} when is_atom(atom) ->
[atom]
path when is_list(path) ->
path
other ->
raise "Invalid value used in the first argument in exists, i.e exists(#{Macro.to_string(other)}, #{Macro.to_string(at_path)})"
end
soft_escape(
quote do
unquote(struct).new(
unquote(path),
unquote(expr),
unquote(at_path)
)
end,
escape?
)
end
defp cond_to_if_tree([{condition, result}]) do
{:if, [], [cond_condition(condition), [do: result]]}
end
defp cond_to_if_tree([{condition, result} | rest]) do
{:if, [], [cond_condition(condition), [do: result, else: cond_to_if_tree(rest)]]}
end
defp cond_condition([condition]) do
condition
end
defp cond_condition([condition | rest]) do
{:and, [], [condition, cond_condition(rest)]}
end
defp soft_escape(%_{} = val, _) do
{:%{}, [], Map.to_list(val)}
end
defp soft_escape(other, _), do: other
defp do_ref({left, _, nil}, _right) when left in @operator_symbols do
raise ArgumentError, "invalid use of `.` in expression. Use `[]` to access nested fields"
end
defp do_ref({left, _, nil}, right) do
%Ash.Query.Ref{relationship_path: [left], attribute: right}
end
defp do_ref({{:., _, [_, _]} = left, _, _}, right) do
do_ref(left, right)
end
defp do_ref({:., _, [_left, _right]}, far_right) when far_right in @operator_symbols do
raise ArgumentError, "invalid use of `.` in expression. Use `[]` to access nested fields"
end
defp do_ref({:., _, [left, right]}, far_right) do
case do_ref(left, right) do
%Ash.Query.Ref{relationship_path: path, attribute: attribute} = ref ->
%{ref | relationship_path: path ++ [attribute], attribute: far_right}
:error ->
:error
end
end
defp do_ref({left, _, _}, right) when left in @operator_symbols and is_atom(right) do
raise ArgumentError, "invalid use of `.` in expression. Use `[]` to access nested fields"
end
defp do_ref({left, _, _}, right) when is_atom(left) and right in @operator_symbols do
raise ArgumentError, "invalid use of `.` in expression. Use `[]` to access nested fields"
end
defp do_ref({left, _, _}, right) when is_atom(left) and is_atom(right) do
%Ash.Query.Ref{relationship_path: [left], attribute: right}
end
defp do_ref(_left, _right) do
:error
end
defp remove_pin({:^, _, [value]}), do: value
defp remove_pin(value), do: value
end