mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
improvement(Ash.Type.Module): Add :module
type. (#578)
This commit is contained in:
parent
ccacfd78fc
commit
7326ca330e
3 changed files with 308 additions and 1 deletions
155
lib/ash/type/module.ex
Normal file
155
lib/ash/type/module.ex
Normal file
|
@ -0,0 +1,155 @@
|
|||
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
|
|
@ -45,7 +45,8 @@ defmodule Ash.Type do
|
|||
utc_datetime: "Ash.Type.UtcDatetime",
|
||||
utc_datetime_usec: "Ash.Type.UtcDatetimeUsec",
|
||||
url_encoded_binary: "Ash.Type.UrlEncodedBinary",
|
||||
union: "Ash.Type.Union"
|
||||
union: "Ash.Type.Union",
|
||||
module: "Ash.Type.Module"
|
||||
]
|
||||
|> Enum.map(fn {key, value} ->
|
||||
{key, Module.concat([value])}
|
||||
|
|
151
test/type/module_test.exs
Normal file
151
test/type/module_test.exs
Normal file
|
@ -0,0 +1,151 @@
|
|||
defmodule Ash.Test.Type.ModuleTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Ash.Changeset
|
||||
require Ash.Query
|
||||
|
||||
defmodule StinkyBehaviour do
|
||||
@moduledoc false
|
||||
@callback stinky? :: boolean
|
||||
end
|
||||
|
||||
defmodule StinkyModule do
|
||||
@moduledoc false
|
||||
@behaviour StinkyBehaviour
|
||||
def stinky?, do: true
|
||||
end
|
||||
|
||||
defprotocol StinkyProtocol do
|
||||
@moduledoc false
|
||||
def stinky?(_)
|
||||
end
|
||||
|
||||
defmodule StinkyStruct do
|
||||
@moduledoc false
|
||||
defstruct stinky?: true
|
||||
|
||||
defimpl StinkyProtocol do
|
||||
def stinky?(stinky), do: stinky.stinky?
|
||||
end
|
||||
end
|
||||
|
||||
defmodule GenericModule do
|
||||
@moduledoc false
|
||||
end
|
||||
|
||||
defmodule ModuleAttr do
|
||||
@moduledoc false
|
||||
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
||||
|
||||
ets do
|
||||
private? true
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :module, :module
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ModuleAttrWithBehaviourConstraint do
|
||||
@moduledoc false
|
||||
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
||||
|
||||
ets do
|
||||
private? true
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :module, :module do
|
||||
constraints behaviour: StinkyBehaviour
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ModuleAttrWithProtocolConstraint do
|
||||
@moduledoc false
|
||||
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
||||
|
||||
ets do
|
||||
private? true
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :module, :module do
|
||||
constraints protocol: StinkyProtocol
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Registry do
|
||||
@moduledoc false
|
||||
use Ash.Registry
|
||||
|
||||
entries do
|
||||
entry ModuleAttr
|
||||
entry ModuleAttrWithBehaviourConstraint
|
||||
entry ModuleAttrWithProtocolConstraint
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Api do
|
||||
@moduledoc false
|
||||
use Ash.Api
|
||||
|
||||
resources do
|
||||
registry Registry
|
||||
end
|
||||
end
|
||||
|
||||
test "module attribute with no constraints" do
|
||||
ModuleAttr
|
||||
|> new(%{module: GenericModule})
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
test "module attribute with behaviour constraint when the module complies" do
|
||||
ModuleAttrWithBehaviourConstraint
|
||||
|> new(%{module: StinkyModule})
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
test "module attribute with behaviour constraint when the module is not compliant" do
|
||||
assert_raise Ash.Error.Invalid, fn ->
|
||||
ModuleAttrWithBehaviourConstraint
|
||||
|> new(%{module: GenericModule})
|
||||
|> Api.create!()
|
||||
end
|
||||
end
|
||||
|
||||
test "module attribute with protocol constraint when the module complies" do
|
||||
ModuleAttrWithProtocolConstraint
|
||||
|> new(%{module: StinkyStruct})
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
test "module attribute with protocol constraint when the module is not compliant" do
|
||||
assert_raise Ash.Error.Invalid, fn ->
|
||||
ModuleAttrWithProtocolConstraint
|
||||
|> new(%{module: GenericModule})
|
||||
|> Api.create!()
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue