improvement: support do/else blocks in if

improvement: support `cond`
This commit is contained in:
Zach Daniel 2021-11-13 13:48:25 -05:00
parent f124e9bf7c
commit 7cb4401d8e
8 changed files with 220 additions and 9 deletions

View file

@ -793,7 +793,17 @@ defmodule Ash.Filter do
%{op | left: do_map(left, func), right: do_map(right, 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 ->
func.(other)
@ -806,6 +816,9 @@ defmodule Ash.Filter do
def update_aggregates(expression, mapper) do
case expression do
{key, value} when is_atom(key) ->
{key, update_aggregates(value, mapper)}
%Not{expression: expression} = not_expr ->
%{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)]
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
Enum.map(arguments, &do_relationship_paths(&1, kind))
end
@ -1327,6 +1344,8 @@ defmodule Ash.Filter do
Enum.flat_map(list, &list_refs/1)
end
def list_refs({key, value}) when is_atom(key), do: list_refs(value)
def list_refs(%__MODULE__{expression: expression}) do
list_refs(expression)
end
@ -1419,7 +1438,7 @@ defmodule Ash.Filter do
path
) do
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))
|> case do
true ->
@ -1434,6 +1453,8 @@ defmodule Ash.Filter do
other
end
defp scope_ref({key, %Ref{} = ref}, path), do: {key, scope_ref(ref, path)}
defp scope_ref(%Ref{} = ref, path) do
if List.starts_with?(ref.relationship_path, path) do
%{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), 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(
%Ref{attribute: attribute} = ref,
%{aggregates: aggregates, calculations: calculations} = context
@ -2234,6 +2265,9 @@ defmodule Ash.Filter do
defp add_to_predicate_path(expression, context) do
case expression do
{key, value} when is_atom(key) ->
{key, add_to_predicate_path(value, context)}
%Not{expression: expression} = not_expr ->
%{not_expr | expression: add_to_predicate_path(expression, context)}

View file

@ -214,6 +214,16 @@ defmodule Ash.Filter.Runtime do
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
{:ok, resolve_ref(ref, record)}
end

View file

@ -6,7 +6,54 @@ defmodule Ash.Query.Call do
defimpl Inspect do
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
do_inspect(call, inspect_opts)
end
defp do_inspect(call, inspect_opts) do
if call.operator? do
concat([
to_doc(Enum.at(call.args, 0), inspect_opts),

View file

@ -27,23 +27,34 @@ defmodule Ash.Query.Function do
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)
if configured_arg_count == given_arg_count do
if given_arg_count in allowed_arg_counts do
mod_args
|> Enum.filter(fn args ->
Enum.count(args) == given_arg_count
end)
|> Enum.find_value(&try_cast_arguments(&1, args))
|> case do
nil ->
{:error,
"Could not cast function arguments for #{mod.name()}/#{configured_arg_count}"}
{:error, "Could not cast function arguments for #{mod.name()}/#{given_arg_count}"}
casted ->
mod.new(casted)
end
else
did_you_mean =
Enum.map_join(allowed_arg_counts, "\n", fn arg_count ->
" . * #{mod.name()}/#{arg_count}"
end)
{: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

View file

@ -4,7 +4,23 @@ defmodule Ash.Query.Function.If do
"""
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
if condition do

View file

@ -548,8 +548,29 @@ defmodule Ash.Query do
soft_escape(Not.new(expression), escape?)
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
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?)
end
@ -558,6 +579,22 @@ defmodule Ash.Query do
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
{:%{}, [], Map.to_list(val)}
end

View file

@ -149,6 +149,10 @@ defmodule Ash.SatSolver do
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(
%Ash.Query.Ref{attribute: attribute, relationship_path: path} = ref,
resource

View file

@ -60,6 +60,28 @@ defmodule Ash.Test.CalculationTest do
calculate :conditional_full_name,
:string,
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
@ -185,4 +207,34 @@ defmodule Ash.Test.CalculationTest do
assert full_names == ["(none)", "brian cranston", "zach daniel"]
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