mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
566 lines
14 KiB
Elixir
566 lines
14 KiB
Elixir
defmodule Ash.Helpers do
|
|
@moduledoc false
|
|
|
|
@dialyzer {:nowarn_function, {:unwrap_or_raise!, 2}}
|
|
|
|
@spec try_compile(term) :: :ok
|
|
def try_compile(module) when is_atom(module) do
|
|
try do
|
|
# This is to get the compiler to ensure that the resource is compiled
|
|
# For some very strange reason, `Code.ensure_compiled/1` isn't enough
|
|
module.ash_dsl_config()
|
|
rescue
|
|
_ ->
|
|
:ok
|
|
end
|
|
|
|
Code.ensure_compiled!(module)
|
|
:ok
|
|
end
|
|
|
|
def try_compile(_), do: :ok
|
|
|
|
def flatten_preserving_keywords(list) do
|
|
if Keyword.keyword?(list) do
|
|
[list]
|
|
else
|
|
list
|
|
|> List.wrap()
|
|
|> Enum.flat_map(fn item ->
|
|
cond do
|
|
Keyword.keyword?(item) ->
|
|
[item]
|
|
|
|
is_list(item) ->
|
|
flatten_preserving_keywords(item)
|
|
|
|
true ->
|
|
[item]
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
defmacro expect_resource!(resource) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote generated: true, bind_quoted: [resource: resource, formatted: formatted] do
|
|
if !Ash.Resource.Info.resource?(resource) do
|
|
raise ArgumentError,
|
|
"Expected an `Ash.Resource` in #{formatted}, got: #{inspect(resource)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
defmacro expect_resource_or_query!(resource_or_query) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote generated: true,
|
|
bind_quoted: [resource_or_query: resource_or_query, formatted: formatted] do
|
|
case resource_or_query do
|
|
%Ash.Query{} ->
|
|
:ok
|
|
|
|
other ->
|
|
if !Ash.Resource.Info.resource?(other) do
|
|
raise ArgumentError,
|
|
"Expected an `%Ash.Query{}` or an `Ash.Resource` in #{formatted}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defmacro expect_query!(query) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote generated: true, bind_quoted: [query: query, formatted: formatted] do
|
|
case query do
|
|
%Ash.Query{} ->
|
|
:ok
|
|
|
|
other ->
|
|
raise ArgumentError,
|
|
"Expected an `%Ash.Query{}` in #{formatted}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
defmacro expect_changeset!(changeset) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote generated: true, bind_quoted: [changeset: changeset, formatted: formatted] do
|
|
case changeset do
|
|
%Ash.Changeset{} ->
|
|
:ok
|
|
|
|
other ->
|
|
raise ArgumentError,
|
|
"Expected an `%Ash.Changeset{}` in #{formatted}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
defmacro expect_resource_or_record!(resource) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote generated: true, bind_quoted: [resource: resource, formatted: formatted] do
|
|
case resource do
|
|
%resource{} = record ->
|
|
if !Ash.Resource.Info.resource?(resource) do
|
|
raise ArgumentError,
|
|
"Expected an `Ash.Resource` or a record in #{formatted}, got: #{inspect(record)}"
|
|
end
|
|
|
|
other ->
|
|
if !Ash.Resource.Info.resource?(resource) do
|
|
raise ArgumentError,
|
|
"Expected an `Ash.Resource` or a record in #{formatted}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defmacro expect_changeset_or_record!(changeset_or_record) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote generated: true,
|
|
bind_quoted: [changeset_or_record: changeset_or_record, formatted: formatted] do
|
|
case changeset_or_record do
|
|
%Ash.Changeset{} ->
|
|
:ok
|
|
|
|
%resource{} = record ->
|
|
if !Ash.Resource.Info.resource?(resource) do
|
|
raise ArgumentError,
|
|
"Expected an `Ash.Resource` or a record in #{formatted}, got: #{inspect(record)}"
|
|
end
|
|
|
|
other ->
|
|
raise ArgumentError,
|
|
"Expected an `Ash.Resource` or a record in #{formatted}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
defmacro expect_record!(record) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote bind_quoted: [record: record, formatted: formatted] do
|
|
case record do
|
|
%resource{} = record ->
|
|
if !Ash.Resource.Info.resource?(resource) do
|
|
raise ArgumentError,
|
|
"Expected a record in #{formatted}, got: #{inspect(record)}"
|
|
end
|
|
|
|
other ->
|
|
raise ArgumentError,
|
|
"Expected a record in #{formatted}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
defmacro expect_options!(options) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote generated: true, bind_quoted: [options: options, formatted: formatted] do
|
|
case options do
|
|
[] ->
|
|
:ok
|
|
|
|
[{atom, _} | _] when is_atom(atom) ->
|
|
:ok
|
|
|
|
other ->
|
|
raise ArgumentError,
|
|
"Expected a keyword list in #{formatted}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def resource_from_query_or_stream(domain, query_or_stream, opts) do
|
|
resource =
|
|
opts[:resource] ||
|
|
case query_or_stream do
|
|
[%resource{} | _] ->
|
|
resource
|
|
|
|
%Ash.Query{resource: resource} ->
|
|
resource
|
|
|
|
resource when is_atom(resource) ->
|
|
if Ash.Resource.Info.resource?(resource) do
|
|
resource
|
|
end
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
|
|
if !resource do
|
|
raise ArgumentError,
|
|
"Could not determine resource for bulk action. Please provide the `resource` option if providing a stream of inputs."
|
|
end
|
|
|
|
Ash.Domain.Info.resource(domain, resource)
|
|
end
|
|
|
|
def get_action(resource, params, type, preset \\ nil) do
|
|
case Keyword.fetch(params, :action) do
|
|
{:ok, %_{} = action} ->
|
|
{:ok, action}
|
|
|
|
{:ok, nil} ->
|
|
if preset do
|
|
get_action(resource, Keyword.put(params, :action, preset), type)
|
|
else
|
|
get_action(resource, Keyword.delete(params, :action), type)
|
|
end
|
|
|
|
{:ok, action} ->
|
|
case Ash.Resource.Info.action(resource, action, type) do
|
|
nil ->
|
|
{:error,
|
|
Ash.Error.Invalid.NoSuchAction.exception(
|
|
resource: resource,
|
|
action: action,
|
|
type: type
|
|
)}
|
|
|
|
action ->
|
|
{:ok, action}
|
|
end
|
|
|
|
:error ->
|
|
if preset do
|
|
get_action(resource, Keyword.put(params, :action, preset), type)
|
|
else
|
|
case Ash.Resource.Info.primary_action(resource, type) do
|
|
nil ->
|
|
if Ash.Resource.Info.resource?(resource) do
|
|
{:error,
|
|
Ash.Error.Invalid.NoPrimaryAction.exception(resource: resource, type: type)}
|
|
else
|
|
{:error, Ash.Error.Invalid.NoSuchResource.exception(resource: resource)}
|
|
end
|
|
|
|
action ->
|
|
{:ok, action}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def pagination_check(action, resource, opts) do
|
|
if Keyword.get(opts, :page) && Keyword.get(opts, :page) != [] && !Map.get(action, :pagination) do
|
|
{:error,
|
|
Ash.Error.to_error_class(
|
|
Ash.Error.Invalid.PaginationNotSupported.exception(resource: resource, action: action)
|
|
)}
|
|
else
|
|
{:ok, action}
|
|
end
|
|
end
|
|
|
|
def unwrap_one({:error, error}) do
|
|
{:error, error}
|
|
end
|
|
|
|
def unwrap_one({:ok, result, query}) do
|
|
case unwrap_one({:ok, result}) do
|
|
{:ok, result} ->
|
|
{:ok, result, query}
|
|
|
|
{:error, error} ->
|
|
{:error, Ash.Error.to_ash_error(error, query: query)}
|
|
end
|
|
end
|
|
|
|
def unwrap_one({:ok, result}) do
|
|
case unwrap_one(result) do
|
|
{:ok, result} ->
|
|
{:ok, result}
|
|
|
|
{:error, error} ->
|
|
{:error, error}
|
|
end
|
|
end
|
|
|
|
def unwrap_one(%{results: results}) do
|
|
unwrap_one(results)
|
|
end
|
|
|
|
def unwrap_one([]), do: {:ok, nil}
|
|
def unwrap_one([result]), do: {:ok, result}
|
|
|
|
def unwrap_one([_ | _] = results) do
|
|
error =
|
|
Ash.Error.Invalid.MultipleResults.exception(
|
|
count: Enum.count(results),
|
|
at_least?: true
|
|
)
|
|
|
|
{:error, error}
|
|
end
|
|
|
|
def resource_from_data!(data, query, opts) do
|
|
if opts[:resource] do
|
|
opts[:resource]
|
|
else
|
|
case query do
|
|
%Ash.Query{resource: resource} -> resource
|
|
_ -> do_resource_from_data!(data)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp do_resource_from_data!(%struct{rerun: {%Ash.Query{resource: resource}, _}})
|
|
when struct in [Ash.Page.Offset, Ash.Page.Keyset] do
|
|
resource
|
|
end
|
|
|
|
defp do_resource_from_data!(%struct{results: [%resource{} | _]} = data)
|
|
when struct in [Ash.Page.Offset, Ash.Page.Keyset] do
|
|
if Ash.Resource.Info.resource?(resource) do
|
|
resource
|
|
else
|
|
raise_no_resource_error!(data)
|
|
end
|
|
end
|
|
|
|
defp do_resource_from_data!(%resource{} = data) do
|
|
if Ash.Resource.Info.resource?(resource) do
|
|
resource
|
|
else
|
|
raise_no_resource_error!(data)
|
|
end
|
|
end
|
|
|
|
defp do_resource_from_data!([%resource{} | _] = data) do
|
|
if Ash.Resource.Info.resource?(resource) do
|
|
resource
|
|
else
|
|
raise_no_resource_error!(data)
|
|
end
|
|
end
|
|
|
|
defp do_resource_from_data!(data) do
|
|
raise_no_resource_error!(data)
|
|
end
|
|
|
|
defp raise_no_resource_error!(data) do
|
|
raise ArgumentError,
|
|
message: """
|
|
Could not determine a resource from the provided input:
|
|
|
|
#{inspect(data)}
|
|
"""
|
|
end
|
|
|
|
defmacro domain!(
|
|
subject,
|
|
opts,
|
|
instructions \\ "Please specify the `:domain` option, or adjust the input."
|
|
) do
|
|
formatted = format_caller(__CALLER__)
|
|
|
|
quote generated: true,
|
|
bind_quoted: [
|
|
subject: subject,
|
|
opts: opts,
|
|
formatted: formatted,
|
|
instructions: instructions
|
|
] do
|
|
domain = Ash.Helpers.get_domain(subject, opts)
|
|
|
|
expanded =
|
|
if not is_nil(subject) do
|
|
"\n\n#{inspect(subject)}"
|
|
end
|
|
|
|
if !domain do
|
|
raise(
|
|
ArgumentError,
|
|
"""
|
|
Could not determine domain for input in #{formatted}. #{instructions}#{expanded}
|
|
"""
|
|
)
|
|
end
|
|
|
|
domain
|
|
end
|
|
end
|
|
|
|
defp format_caller(caller) do
|
|
mod = caller.module
|
|
{func, arity} = caller.function
|
|
"`#{inspect(mod)}.#{func}/#{arity}`"
|
|
end
|
|
|
|
def get_domain(nil, nil) do
|
|
nil
|
|
end
|
|
|
|
def get_domain(%input_struct{domain: domain}, _opts)
|
|
when input_struct in [Ash.Query, Ash.Changeset, Ash.ActionInput] and not is_nil(domain) do
|
|
domain
|
|
end
|
|
|
|
def get_domain([record | _], opts) do
|
|
get_domain(record, opts)
|
|
end
|
|
|
|
def get_domain({%resource{}, _}, opts) do
|
|
get_domain(resource, opts)
|
|
end
|
|
|
|
def get_domain({resource, _}, opts) when is_atom(resource) do
|
|
get_domain(resource, opts)
|
|
end
|
|
|
|
def get_domain(%page_struct{rerun: {query, _}}, opts)
|
|
when page_struct in [Ash.Page.Offset, Ash.Page.Keyset] do
|
|
get_domain(query, opts)
|
|
end
|
|
|
|
def get_domain(%page_struct{results: results}, opts)
|
|
when page_struct in [Ash.Page.Offset, Ash.Page.Keyset] do
|
|
get_domain(results, opts)
|
|
end
|
|
|
|
def get_domain(%{resource: resource}, opts) when not is_nil(resource) do
|
|
get_domain(resource, opts)
|
|
end
|
|
|
|
def get_domain(nil, opts) do
|
|
cond do
|
|
domain = opts[:domain] ->
|
|
domain
|
|
|
|
resource = opts[:resource] ->
|
|
get_domain(resource, Keyword.delete(opts, :resource))
|
|
|
|
true ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
def get_domain(%resource{}, opts) do
|
|
if Ash.Resource.Info.resource?(resource) do
|
|
get_domain(resource, opts)
|
|
else
|
|
get_domain(nil, opts)
|
|
end
|
|
end
|
|
|
|
def get_domain(resource, opts) do
|
|
opts[:domain] || domain_from_resource(resource)
|
|
end
|
|
|
|
defp domain_from_resource(resource) do
|
|
if Ash.Resource.Info.resource?(resource) || (is_map(resource) and not is_struct(resource)) do
|
|
Ash.Resource.Info.domain(resource)
|
|
end
|
|
end
|
|
|
|
def unwrap_or_raise!(first, destroy? \\ false)
|
|
def unwrap_or_raise!(%Ash.BulkResult{} = bulk_result, _), do: bulk_result
|
|
def unwrap_or_raise!(:ok, _), do: :ok
|
|
def unwrap_or_raise!({:ok, result}, false), do: result
|
|
def unwrap_or_raise!({:ok, _result}, true), do: :ok
|
|
def unwrap_or_raise!({:ok, result, other}, _), do: {result, other}
|
|
|
|
def unwrap_or_raise!({:error, error}, destroy?) when is_list(error) do
|
|
unwrap_or_raise!({:error, Ash.Error.to_error_class(error)}, destroy?)
|
|
end
|
|
|
|
def unwrap_or_raise!({:error, error}, _) do
|
|
exception = Ash.Error.to_error_class(error)
|
|
|
|
case exception do
|
|
%{stacktrace: %{stacktrace: stacktrace}} = exception ->
|
|
reraise exception, stacktrace
|
|
|
|
_ ->
|
|
raise exception
|
|
end
|
|
end
|
|
|
|
def implements_behaviour?(module, behaviour) do
|
|
:attributes
|
|
|> module.module_info()
|
|
|> Enum.flat_map(fn
|
|
{:behaviour, value} -> List.wrap(value)
|
|
_ -> []
|
|
end)
|
|
|> Enum.any?(&(&1 == behaviour))
|
|
rescue
|
|
_ ->
|
|
false
|
|
end
|
|
|
|
# sobelow_skip ["Misc.BinToTerm"]
|
|
def non_executable_binary_to_term(binary, opts \\ []) when is_binary(binary) do
|
|
term = :erlang.binary_to_term(binary, opts)
|
|
non_executable_terms(term)
|
|
term
|
|
end
|
|
|
|
defp non_executable_terms(list) when is_list(list) do
|
|
non_executable_list(list)
|
|
end
|
|
|
|
defp non_executable_terms(tuple) when is_tuple(tuple) do
|
|
non_executable_tuple(tuple, tuple_size(tuple))
|
|
end
|
|
|
|
defp non_executable_terms(map) when is_map(map) do
|
|
folder = fn key, value, acc ->
|
|
non_executable_terms(key)
|
|
non_executable_terms(value)
|
|
acc
|
|
end
|
|
|
|
:maps.fold(folder, map, map)
|
|
end
|
|
|
|
defp non_executable_terms(other)
|
|
when is_atom(other) or is_number(other) or is_bitstring(other) or is_pid(other) or
|
|
is_reference(other) do
|
|
other
|
|
end
|
|
|
|
defp non_executable_terms(other) do
|
|
raise ArgumentError,
|
|
"cannot deserialize #{inspect(other)}, the term is not safe for deserialization"
|
|
end
|
|
|
|
defp non_executable_list([]), do: :ok
|
|
|
|
defp non_executable_list([h | t]) when is_list(t) do
|
|
non_executable_terms(h)
|
|
non_executable_list(t)
|
|
end
|
|
|
|
defp non_executable_list([h | t]) do
|
|
non_executable_terms(h)
|
|
non_executable_terms(t)
|
|
end
|
|
|
|
defp non_executable_tuple(_tuple, 0), do: :ok
|
|
|
|
defp non_executable_tuple(tuple, n) do
|
|
non_executable_terms(:erlang.element(n, tuple))
|
|
non_executable_tuple(tuple, n - 1)
|
|
end
|
|
|
|
@doc false
|
|
def deep_merge_maps(left, right)
|
|
when is_map(left) and is_map(right) and not is_struct(left) and not is_struct(right) do
|
|
Map.merge(left, right, fn _, left, right ->
|
|
deep_merge_maps(left, right)
|
|
end)
|
|
end
|
|
|
|
def deep_merge_maps(_left, right), do: right
|
|
end
|