defmodule Ash.Type.Module do
@constraints [
behaviour: [
type: :atom,
doc: "Allows constraining the module a one which implements a behaviour"
],
protocol: [
type: :atom,
doc: "Allows constraining the module a one which implements a protocol"
]
]
@moduledoc """
Stores a module as a string in the database.
A builtin type that can be referenced via `:module`.
### Constraints
#{Spark.OptionsHelpers.docs(@constraints)}
"""
use Ash.Type
@impl true
def storage_type, do: :string
@impl true
def constraints, do: @constraints
def apply_constraints(nil, _), do: :ok
def apply_constraints(value, constraints) do
[]
|> apply_behaviour_constraint(value, constraints[:behaviour])
|> apply_protocol_constraint(value, constraints[:protocol])
|> case do
[] -> {:ok, value}
errors -> {:error, errors}
end
end
defp apply_behaviour_constraint(errors, _module, nil), do: errors
defp apply_behaviour_constraint(errors, module, behaviour) do
if Spark.implements_behaviour?(module, behaviour) do
errors
else
Enum.concat(errors, [
[
message: "module %{module} does not implement the %{behaviour} behaviour",
module: module,
behaviour: behaviour
]
])
end
end
defp apply_protocol_constraint(errors, _module, nil), do: errors
defp apply_protocol_constraint(errors, module, protocol) do
Protocol.assert_protocol!(protocol)
Protocol.assert_impl!(protocol, module)
errors
rescue
ArgumentError ->
Enum.concat(errors, [
[
message: "module %{module} does not implement the %{protocol} protocol",
module: module,
protocol: protocol
]
])
end
@impl true
def cast_input(value, _) when is_atom(value) do
if Code.ensure_loaded?(value) do
{:ok, value}
else
:error
end
end
def cast_input("", _), do: {:ok, nil}
def cast_input("Elixir." <> _ = value, _) do
module = Module.concat([value])
if Code.ensure_loaded?(module) do
{:ok, module}
else
:error
end
end
def cast_input(value, _) when is_binary(value) do
atom = String.to_existing_atom(value)
if Code.ensure_loaded?(atom) do
{:ok, atom}
else
:error
end
rescue
ArgumentError ->
:error
end
def cast_input(_, _), do: :error
@impl true
def cast_stored(nil, _), do: {:ok, nil}
def cast_stored(value, _) when is_atom(value) do
if Code.ensure_loaded?(value) do
{:ok, value}
else
:error
end
end
def cast_stored("Elixir." <> _ = value, _) do
module = Module.concat([value])
if Code.ensure_loaded?(module) do
{:ok, module}
else
:error
end
end
def cast_stored(value, _) when is_binary(value) do
atom = String.to_existing_atom(value)
if Code.ensure_loaded?(atom) do
{:ok, atom}
else
:error
end
rescue
ArgumentError -> :error
end
def cast_stored(_, _), do: :error
@impl true
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(value, _) when is_atom(value) do
{:ok, to_string(value)}
end
def dump_to_native(_, _), do: :error
end