Packages
ash
3.4.40
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
Current section
Files
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) def determine_types(Ash.Query.Function.Type, [_, type], _returns) do {type, []} end def determine_types(Ash.Query.Function.Type, [_, type, constraints], _returns) do {type, constraints} end def determine_types(mod, values, known_result) 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 = 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 -> cond do !Ash.Expr.expr?(value) && !matches_type?(type, value, constraints) -> {: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 -> cond do !Ash.Expr.expr?(value) && !matches_type?(type, value, []) -> {:halt, :error} 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, 0} 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) |> select_matches(length(values)) end defp select_matches([], value_count) do {Enum.map(1..value_count, fn _ -> nil end), nil} end defp select_matches(results, value_count) do case Enum.find(results, fn {_type, _returns, 0} -> true _ -> false end) do {type, returns, 0} -> {type, returns} _ -> results = Enum.map(results, 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) arg_types = 1..value_count |> Enum.map(fn i -> possible_types = Enum.map(results, fn {types, _} -> Enum.at(types, i - 1) end) if Enum.uniq(possible_types) == possible_types do Enum.at(possible_types, 0) end end) all_returns = Enum.map(results, &elem(&1, 1)) if Enum.uniq(all_returns) == all_returns do {arg_types, Enum.at(all_returns, 0)} else {arg_types, nil} 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) do {_, nil} -> :error {_, type} -> {:ok, type} end %mod{__predicate__?: _, left: left, right: right} -> case determine_types(mod, [left, right]) do {_, nil} -> :error {_, type} -> {:ok, type} end _ -> :error end end defp matches_type?({:array, type}, %MapSet{} = value, constraints) do Enum.all?(value, &matches_type?(&1, type, constraints[:items])) end defp matches_type?(type, value, constraints) do 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: valueend