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)} %{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)}

View file

@ -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

View file

@ -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),

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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