Packages

ash

3.0.0-rc.42
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])
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) 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), do: {:_ref, [], name}
@doc "A template helper for creating a reference to a related path"
def ref(path, 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?]
)
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
defp get_path(map, [key]) when is_struct(map) do
Map.get(map, key)
end
defp get_path(map, [key]) when is_map(map) do
Map.get(map, key)
end
defp get_path(map, [key | rest]) when is_map(map) do
get_path(get_path(map, [key]), rest)
end
defp 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
@doc "Whether or not a given template contains an actor reference"
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))
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: true}, escape?)
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({: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
args = Enum.map([{left, [], nil}, right], &do_expr(&1, false))
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: true}, 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(
{:&, _, _} = 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
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