mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
improvement: support do/else blocks in if
improvement: support `cond`
This commit is contained in:
parent
f124e9bf7c
commit
7cb4401d8e
8 changed files with 220 additions and 9 deletions
|
@ -793,7 +793,17 @@ defmodule Ash.Filter do
|
||||||
%{op | left: do_map(left, func), right: do_map(right, func)}
|
%{op | left: do_map(left, func), right: do_map(right, func)}
|
||||||
|
|
||||||
%{__function__?: true, arguments: arguments} = func ->
|
%{__function__?: true, arguments: arguments} = func ->
|
||||||
%{func | arguments: Enum.map(arguments, &do_map(&1, func))}
|
%{
|
||||||
|
func
|
||||||
|
| arguments:
|
||||||
|
Enum.map(arguments, fn
|
||||||
|
{key, arg} when is_atom(key) ->
|
||||||
|
{key, do_map(arg, func)}
|
||||||
|
|
||||||
|
arg ->
|
||||||
|
do_map(arg, func)
|
||||||
|
end)
|
||||||
|
}
|
||||||
|
|
||||||
other ->
|
other ->
|
||||||
func.(other)
|
func.(other)
|
||||||
|
@ -806,6 +816,9 @@ defmodule Ash.Filter do
|
||||||
|
|
||||||
def update_aggregates(expression, mapper) do
|
def update_aggregates(expression, mapper) do
|
||||||
case expression do
|
case expression do
|
||||||
|
{key, value} when is_atom(key) ->
|
||||||
|
{key, update_aggregates(value, mapper)}
|
||||||
|
|
||||||
%Not{expression: expression} = not_expr ->
|
%Not{expression: expression} = not_expr ->
|
||||||
%{not_expr | expression: update_aggregates(expression, mapper)}
|
%{not_expr | expression: update_aggregates(expression, mapper)}
|
||||||
|
|
||||||
|
@ -1234,6 +1247,10 @@ defmodule Ash.Filter do
|
||||||
[do_relationship_paths(left, kind), do_relationship_paths(right, kind)]
|
[do_relationship_paths(left, kind), do_relationship_paths(right, kind)]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp do_relationship_paths({key, value}, kind) when is_atom(key) do
|
||||||
|
do_relationship_paths(value, kind)
|
||||||
|
end
|
||||||
|
|
||||||
defp do_relationship_paths(%{__function__?: true, arguments: arguments}, kind) do
|
defp do_relationship_paths(%{__function__?: true, arguments: arguments}, kind) do
|
||||||
Enum.map(arguments, &do_relationship_paths(&1, kind))
|
Enum.map(arguments, &do_relationship_paths(&1, kind))
|
||||||
end
|
end
|
||||||
|
@ -1327,6 +1344,8 @@ defmodule Ash.Filter do
|
||||||
Enum.flat_map(list, &list_refs/1)
|
Enum.flat_map(list, &list_refs/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_refs({key, value}) when is_atom(key), do: list_refs(value)
|
||||||
|
|
||||||
def list_refs(%__MODULE__{expression: expression}) do
|
def list_refs(%__MODULE__{expression: expression}) do
|
||||||
list_refs(expression)
|
list_refs(expression)
|
||||||
end
|
end
|
||||||
|
@ -1419,7 +1438,7 @@ defmodule Ash.Filter do
|
||||||
path
|
path
|
||||||
) do
|
) do
|
||||||
arguments
|
arguments
|
||||||
|> Enum.filter(&match?(%Ref{}, &1))
|
|> Enum.filter(fn arg -> match?(%Ref{}, arg) || match?({_, %Ref{}}, arg) end)
|
||||||
|> Enum.any?(&List.starts_with?(&1.relationship_path, path))
|
|> Enum.any?(&List.starts_with?(&1.relationship_path, path))
|
||||||
|> case do
|
|> case do
|
||||||
true ->
|
true ->
|
||||||
|
@ -1434,6 +1453,8 @@ defmodule Ash.Filter do
|
||||||
other
|
other
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp scope_ref({key, %Ref{} = ref}, path), do: {key, scope_ref(ref, path)}
|
||||||
|
|
||||||
defp scope_ref(%Ref{} = ref, path) do
|
defp scope_ref(%Ref{} = ref, path) do
|
||||||
if List.starts_with?(ref.relationship_path, path) do
|
if List.starts_with?(ref.relationship_path, path) do
|
||||||
%{ref | relationship_path: Enum.drop(ref.relationship_path, Enum.count(path))}
|
%{ref | relationship_path: Enum.drop(ref.relationship_path, Enum.count(path))}
|
||||||
|
@ -2006,6 +2027,16 @@ defmodule Ash.Filter do
|
||||||
defp module_and_opts({module, opts}), do: {module, opts}
|
defp module_and_opts({module, opts}), do: {module, opts}
|
||||||
defp module_and_opts(module), do: {module, []}
|
defp module_and_opts(module), do: {module, []}
|
||||||
|
|
||||||
|
def hydrate_refs({key, value}, context) when is_atom(key) do
|
||||||
|
case hydrate_refs(value, context) do
|
||||||
|
{:ok, hydrated} ->
|
||||||
|
{:ok, {key, hydrated}}
|
||||||
|
|
||||||
|
other ->
|
||||||
|
other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def hydrate_refs(
|
def hydrate_refs(
|
||||||
%Ref{attribute: attribute} = ref,
|
%Ref{attribute: attribute} = ref,
|
||||||
%{aggregates: aggregates, calculations: calculations} = context
|
%{aggregates: aggregates, calculations: calculations} = context
|
||||||
|
@ -2234,6 +2265,9 @@ defmodule Ash.Filter do
|
||||||
|
|
||||||
defp add_to_predicate_path(expression, context) do
|
defp add_to_predicate_path(expression, context) do
|
||||||
case expression do
|
case expression do
|
||||||
|
{key, value} when is_atom(key) ->
|
||||||
|
{key, add_to_predicate_path(value, context)}
|
||||||
|
|
||||||
%Not{expression: expression} = not_expr ->
|
%Not{expression: expression} = not_expr ->
|
||||||
%{not_expr | expression: add_to_predicate_path(expression, context)}
|
%{not_expr | expression: add_to_predicate_path(expression, context)}
|
||||||
|
|
||||||
|
|
|
@ -214,6 +214,16 @@ defmodule Ash.Filter.Runtime do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp resolve_expr({key, value}, record) when is_atom(key) do
|
||||||
|
case resolve_expr(value, record) do
|
||||||
|
{:ok, resolved} ->
|
||||||
|
{:ok, {key, resolved}}
|
||||||
|
|
||||||
|
other ->
|
||||||
|
other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp resolve_expr(%Ref{} = ref, record) do
|
defp resolve_expr(%Ref{} = ref, record) do
|
||||||
{:ok, resolve_ref(ref, record)}
|
{:ok, resolve_ref(ref, record)}
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,54 @@ defmodule Ash.Query.Call do
|
||||||
defimpl Inspect do
|
defimpl Inspect do
|
||||||
import Inspect.Algebra
|
import Inspect.Algebra
|
||||||
|
|
||||||
|
def inspect(%{name: :if, operator?: false, args: [condition, blocks]} = call, opts) do
|
||||||
|
if Keyword.keyword?(blocks) do
|
||||||
|
if Keyword.has_key?(blocks, :else) do
|
||||||
|
concat([
|
||||||
|
nest(
|
||||||
|
concat([
|
||||||
|
group(concat(["if ", to_doc(condition, opts), " do"])),
|
||||||
|
line(),
|
||||||
|
to_doc(blocks[:do], opts)
|
||||||
|
]),
|
||||||
|
2
|
||||||
|
),
|
||||||
|
line(),
|
||||||
|
"else",
|
||||||
|
nest(
|
||||||
|
concat([
|
||||||
|
line(),
|
||||||
|
to_doc(blocks[:else], opts)
|
||||||
|
]),
|
||||||
|
2
|
||||||
|
),
|
||||||
|
line(),
|
||||||
|
"end"
|
||||||
|
])
|
||||||
|
else
|
||||||
|
concat([
|
||||||
|
nest(
|
||||||
|
concat([
|
||||||
|
group(concat(["if ", to_doc(condition, opts), " do"])),
|
||||||
|
line(),
|
||||||
|
to_doc(blocks[:do], opts)
|
||||||
|
]),
|
||||||
|
2
|
||||||
|
),
|
||||||
|
line(),
|
||||||
|
"end"
|
||||||
|
])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
do_inspect(call, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def inspect(call, inspect_opts) do
|
def inspect(call, inspect_opts) do
|
||||||
|
do_inspect(call, inspect_opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_inspect(call, inspect_opts) do
|
||||||
if call.operator? do
|
if call.operator? do
|
||||||
concat([
|
concat([
|
||||||
to_doc(Enum.at(call.args, 0), inspect_opts),
|
to_doc(Enum.at(call.args, 0), inspect_opts),
|
||||||
|
|
|
@ -27,23 +27,34 @@ defmodule Ash.Query.Function do
|
||||||
|
|
||||||
mod_args ->
|
mod_args ->
|
||||||
configured_args = List.wrap(mod_args)
|
configured_args = List.wrap(mod_args)
|
||||||
configured_arg_count = Enum.count(Enum.at(configured_args, 0))
|
allowed_arg_counts = Enum.map(configured_args, &Enum.count/1)
|
||||||
given_arg_count = Enum.count(args)
|
given_arg_count = Enum.count(args)
|
||||||
|
|
||||||
if configured_arg_count == given_arg_count do
|
if given_arg_count in allowed_arg_counts do
|
||||||
mod_args
|
mod_args
|
||||||
|
|> Enum.filter(fn args ->
|
||||||
|
Enum.count(args) == given_arg_count
|
||||||
|
end)
|
||||||
|> Enum.find_value(&try_cast_arguments(&1, args))
|
|> Enum.find_value(&try_cast_arguments(&1, args))
|
||||||
|> case do
|
|> case do
|
||||||
nil ->
|
nil ->
|
||||||
{:error,
|
{:error, "Could not cast function arguments for #{mod.name()}/#{given_arg_count}"}
|
||||||
"Could not cast function arguments for #{mod.name()}/#{configured_arg_count}"}
|
|
||||||
|
|
||||||
casted ->
|
casted ->
|
||||||
mod.new(casted)
|
mod.new(casted)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
did_you_mean =
|
||||||
|
Enum.map_join(allowed_arg_counts, "\n", fn arg_count ->
|
||||||
|
" . * #{mod.name()}/#{arg_count}"
|
||||||
|
end)
|
||||||
|
|
||||||
{:error,
|
{:error,
|
||||||
"function #{mod.name()}/#{configured_arg_count} takes #{configured_arg_count} arguments, provided #{given_arg_count}"}
|
"""
|
||||||
|
No such function #{mod.name()}/#{given_arg_count}. Did you mean one of:
|
||||||
|
|
||||||
|
#{did_you_mean}
|
||||||
|
"""}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,23 @@ defmodule Ash.Query.Function.If do
|
||||||
"""
|
"""
|
||||||
use Ash.Query.Function, name: :if
|
use Ash.Query.Function, name: :if
|
||||||
|
|
||||||
def args, do: [[:boolean, :any, :any]]
|
def args, do: [[:boolean, :any], [:boolean, :any, :any]]
|
||||||
|
|
||||||
|
def new([condition, block]) do
|
||||||
|
if Keyword.keyword?(block) && Keyword.has_key?(block, :do) do
|
||||||
|
if Keyword.has_key?(block, :else) do
|
||||||
|
super([condition, block[:do], block[:else]])
|
||||||
|
else
|
||||||
|
super([condition, block[:do], nil])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
super([condition, block, nil])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(other) do
|
||||||
|
super(other)
|
||||||
|
end
|
||||||
|
|
||||||
def evaluate(%{arguments: [condition, when_true, when_false]}) do
|
def evaluate(%{arguments: [condition, when_true, when_false]}) do
|
||||||
if condition do
|
if condition do
|
||||||
|
|
|
@ -548,8 +548,29 @@ defmodule Ash.Query do
|
||||||
soft_escape(Not.new(expression), escape?)
|
soft_escape(Not.new(expression), escape?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp do_expr({:cond, _, [[do: options]]}, escape?) do
|
||||||
|
options
|
||||||
|
|> Enum.map(fn {:->, _, [condition, result]} ->
|
||||||
|
{condition, result}
|
||||||
|
end)
|
||||||
|
|> cond_to_if_tree()
|
||||||
|
|> do_expr(escape?)
|
||||||
|
end
|
||||||
|
|
||||||
defp do_expr({op, _, args}, escape?) when is_atom(op) and is_list(args) do
|
defp do_expr({op, _, args}, escape?) when is_atom(op) and is_list(args) do
|
||||||
args = Enum.map(args, &do_expr(&1, false))
|
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?)
|
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: false}, escape?)
|
||||||
end
|
end
|
||||||
|
@ -558,6 +579,22 @@ defmodule Ash.Query do
|
||||||
|
|
||||||
defp do_expr(other, _), do: other
|
defp do_expr(other, _), do: other
|
||||||
|
|
||||||
|
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
|
defp soft_escape(%_{} = val, _) do
|
||||||
{:%{}, [], Map.to_list(val)}
|
{:%{}, [], Map.to_list(val)}
|
||||||
end
|
end
|
||||||
|
|
|
@ -149,6 +149,10 @@ defmodule Ash.SatSolver do
|
||||||
|
|
||||||
defp upgrade_related_filters_to_join_keys(expr, _), do: expr
|
defp upgrade_related_filters_to_join_keys(expr, _), do: expr
|
||||||
|
|
||||||
|
defp upgrade_ref({key, ref}, resource) when is_atom(key) do
|
||||||
|
{key, upgrade_ref(ref, resource)}
|
||||||
|
end
|
||||||
|
|
||||||
defp upgrade_ref(
|
defp upgrade_ref(
|
||||||
%Ash.Query.Ref{attribute: attribute, relationship_path: path} = ref,
|
%Ash.Query.Ref{attribute: attribute, relationship_path: path} = ref,
|
||||||
resource
|
resource
|
||||||
|
|
|
@ -60,6 +60,28 @@ defmodule Ash.Test.CalculationTest do
|
||||||
calculate :conditional_full_name,
|
calculate :conditional_full_name,
|
||||||
:string,
|
:string,
|
||||||
expr(if(first_name and last_name, first_name <> " " <> last_name, "(none)"))
|
expr(if(first_name and last_name, first_name <> " " <> last_name, "(none)"))
|
||||||
|
|
||||||
|
calculate :conditional_full_name_block,
|
||||||
|
:string,
|
||||||
|
expr(
|
||||||
|
if first_name and last_name do
|
||||||
|
first_name <> " " <> last_name
|
||||||
|
else
|
||||||
|
"(none)"
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
calculate :conditional_full_name_cond,
|
||||||
|
:string,
|
||||||
|
expr(
|
||||||
|
cond do
|
||||||
|
first_name and last_name ->
|
||||||
|
first_name <> " " <> last_name
|
||||||
|
|
||||||
|
true ->
|
||||||
|
"(none)"
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -185,4 +207,34 @@ defmodule Ash.Test.CalculationTest do
|
||||||
|
|
||||||
assert full_names == ["(none)", "brian cranston", "zach daniel"]
|
assert full_names == ["(none)", "brian cranston", "zach daniel"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "the `if` calculation can use the `do` style syntax" do
|
||||||
|
User
|
||||||
|
|> Ash.Changeset.new(%{first_name: "bob"})
|
||||||
|
|> Api.create!()
|
||||||
|
|
||||||
|
full_names =
|
||||||
|
User
|
||||||
|
|> Ash.Query.load(:conditional_full_name_block)
|
||||||
|
|> Api.read!()
|
||||||
|
|> Enum.map(& &1.conditional_full_name_block)
|
||||||
|
|> Enum.sort()
|
||||||
|
|
||||||
|
assert full_names == ["(none)", "brian cranston", "zach daniel"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "the `if` calculation can use the `cond` style syntax" do
|
||||||
|
User
|
||||||
|
|> Ash.Changeset.new(%{first_name: "bob"})
|
||||||
|
|> Api.create!()
|
||||||
|
|
||||||
|
full_names =
|
||||||
|
User
|
||||||
|
|> Ash.Query.load(:conditional_full_name_cond)
|
||||||
|
|> Api.read!()
|
||||||
|
|> Enum.map(& &1.conditional_full_name_cond)
|
||||||
|
|> Enum.sort()
|
||||||
|
|
||||||
|
assert full_names == ["(none)", "brian cranston", "zach daniel"]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue