mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
test and docs
This commit is contained in:
parent
b3c609a4e0
commit
05e84102dd
31 changed files with 1424 additions and 293 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -23,4 +23,6 @@ erl_crash.dump
|
|||
ash-*.tar
|
||||
|
||||
# Ignoring Elixir Language Server
|
||||
.elixir_ls/
|
||||
.elixir_ls/
|
||||
|
||||
.DS_Store
|
||||
|
|
3
lib/ash/authorization/authorization.ex
Normal file
3
lib/ash/authorization/authorization.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Ash.Authorization do
|
||||
@moduledoc "The main documentation for authorization in ash."
|
||||
end
|
|
@ -1,22 +1,22 @@
|
|||
defmodule Ash.Error.ResourceDslError do
|
||||
defexception [:message, :path, :option, :resource, :using]
|
||||
defexception [:message, :path, :option, :using]
|
||||
|
||||
def message(%{message: message, path: nil, option: option, resource: resource, using: using}) do
|
||||
"#{inspect(resource)}: `use #{inspect(using)}, ...` #{option} #{message} "
|
||||
def message(%{message: message, path: nil, option: option, using: using}) do
|
||||
"`use #{inspect(using)}, ...` #{option} #{message} "
|
||||
end
|
||||
|
||||
def message(%{message: message, path: nil, option: option, resource: resource}) do
|
||||
"#{inspect(resource)}: #{option} #{message}"
|
||||
def message(%{message: message, path: nil, option: option}) do
|
||||
"#{option} #{message}"
|
||||
end
|
||||
|
||||
def message(%{message: message, path: dsl_path, option: nil, resource: resource}) do
|
||||
dsl_path = Enum.join(dsl_path, "->")
|
||||
"#{inspect(resource)}: #{message} at #{dsl_path}"
|
||||
def message(%{message: message, path: dsl_path, option: nil}) do
|
||||
dsl_path = Enum.join(dsl_path, " -> ")
|
||||
"#{message} at #{dsl_path}"
|
||||
end
|
||||
|
||||
def message(%{message: message, path: dsl_path, option: option, resource: resource}) do
|
||||
dsl_path = Enum.join(dsl_path, "->")
|
||||
def message(%{message: message, path: dsl_path, option: option}) do
|
||||
dsl_path = Enum.join(dsl_path, " -> ")
|
||||
|
||||
"#{inspect(resource)}: option #{option} at #{dsl_path} #{message}"
|
||||
"option #{option} at #{dsl_path} #{message}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -86,7 +86,18 @@ defmodule Ash.Resource do
|
|||
quote do
|
||||
@before_compile Ash.Resource
|
||||
|
||||
opts = Ash.Resource.validate_use_opts(__MODULE__, unquote(opts))
|
||||
opts =
|
||||
case Ashton.validate(unquote(opts), Ash.Resource.resource_opts_schema()) do
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
using: __MODULE__,
|
||||
option: key,
|
||||
message: message
|
||||
|
||||
{:ok, opts} ->
|
||||
opts
|
||||
end
|
||||
|
||||
Ash.Resource.define_resource_module_attributes(__MODULE__, opts)
|
||||
Ash.Resource.define_primary_key(__MODULE__, opts)
|
||||
|
||||
|
@ -113,33 +124,23 @@ defmodule Ash.Resource do
|
|||
def define_primary_key(mod, opts) do
|
||||
case opts[:primary_key] do
|
||||
true ->
|
||||
attribute = Ash.Resource.Attributes.Attribute.new(mod, :id, :uuid, primary_key?: true)
|
||||
{:ok, attribute} = Ash.Resource.Attributes.Attribute.new(:id, :uuid, primary_key?: true)
|
||||
Module.put_attribute(mod, :attributes, attribute)
|
||||
|
||||
false ->
|
||||
:ok
|
||||
|
||||
opts ->
|
||||
attribute =
|
||||
Ash.Resource.Attributes.Attribute.new(mod, opts[:field], opts[:type], primary_key?: true)
|
||||
{:ok, attribute} =
|
||||
Ash.Resource.Attributes.Attribute.new(opts[:field], opts[:type], primary_key?: true)
|
||||
|
||||
Module.put_attribute(mod, :attributes, attribute)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def validate_use_opts(mod, opts) do
|
||||
case Ashton.validate(opts, @resource_opts_schema) do
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
resource: mod,
|
||||
using: __MODULE__,
|
||||
option: key,
|
||||
message: message
|
||||
|
||||
{:ok, opts} ->
|
||||
opts
|
||||
end
|
||||
def resource_opts_schema() do
|
||||
@resource_opts_schema
|
||||
end
|
||||
|
||||
defmacro __before_compile__(env) do
|
||||
|
@ -147,10 +148,6 @@ defmodule Ash.Resource do
|
|||
@sanitized_actions Ash.Resource.mark_primaries(@actions)
|
||||
@ash_primary_key Ash.Resource.primary_key(@attributes)
|
||||
|
||||
unless @ash_primary_key do
|
||||
raise "Must have a primary key for a resource: #{__MODULE__}"
|
||||
end
|
||||
|
||||
require Ash.Schema
|
||||
|
||||
Ash.Schema.define_schema(@name)
|
||||
|
|
|
@ -1,4 +1,23 @@
|
|||
defmodule Ash.Resource.Actions do
|
||||
@moduledoc """
|
||||
DSL components for declaring resource actions.
|
||||
|
||||
All manipulation of data through the underlying data layer happens through actions.
|
||||
There are four types of action: `create`, `read`, `update`, and `delete`. You may
|
||||
recognize these from the acronym `CRUD`. You can have multiple actions of the same
|
||||
type, as long as they have different names. This is the primary mechanism for customizing
|
||||
your resources to conform to your business logic. It is normal and expected to have
|
||||
multiple actions of each type in a large application.
|
||||
|
||||
If you have multiple actions of the same type, one of them must be designated as the
|
||||
primary action for that type, via: `primary?: true`. This tells the ash what to do
|
||||
if an action of that type is requested, but no specific action name is given.
|
||||
|
||||
Authorization in ash is done via supplying a list of rules to actions in the
|
||||
`rules` option. To understand rules and authorization, see the documentation in `Ash.Authorization`
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro actions(do: block) do
|
||||
quote do
|
||||
import Ash.Resource.Actions
|
||||
|
@ -25,85 +44,144 @@ defmodule Ash.Resource.Actions do
|
|||
end
|
||||
end
|
||||
|
||||
defmacro defaults(:all) do
|
||||
quote do
|
||||
defaults([:create, :update, :destroy, :read])
|
||||
end
|
||||
end
|
||||
@doc """
|
||||
Sets up simple defaults for the supplied list of action types.
|
||||
|
||||
defmacro defaults(defaults, opts \\ []) do
|
||||
These defaults will have the name `:default`. If you need to configure your actions,
|
||||
you will have to declare them each separately.
|
||||
"""
|
||||
defmacro defaults(defaults) do
|
||||
quote do
|
||||
opts = unquote(opts)
|
||||
|
||||
for default <- unquote(defaults) do
|
||||
case default do
|
||||
:all ->
|
||||
create(:default)
|
||||
read(:default)
|
||||
update(:default)
|
||||
destroy(:default)
|
||||
|
||||
:create ->
|
||||
create(:default, opts)
|
||||
|
||||
:update ->
|
||||
update(:default, opts)
|
||||
|
||||
:destroy ->
|
||||
destroy(:default, opts)
|
||||
create(:default)
|
||||
|
||||
:read ->
|
||||
read(:default, opts)
|
||||
read(:default)
|
||||
|
||||
:update ->
|
||||
update(:default)
|
||||
|
||||
:destroy ->
|
||||
destroy(:default)
|
||||
|
||||
action ->
|
||||
raise "Invalid action type #{action} listed in defaults list for resource: #{
|
||||
__MODULE__
|
||||
}"
|
||||
raise Ash.Error.ResourceDslError,
|
||||
path: [:actions, :defaults],
|
||||
message: "Invalid default #{action}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Declares a `create` action. For calling this action, see the `Ash.Api` documentation.
|
||||
|
||||
#{Ashton.document(Ash.Resource.Actions.Create.opt_schema())}
|
||||
"""
|
||||
defmacro create(name, opts \\ []) do
|
||||
quote bind_quoted: [name: name, opts: opts] do
|
||||
action =
|
||||
Ash.Resource.Actions.Create.new(name,
|
||||
primary?: opts[:primary?] || false,
|
||||
rules: opts[:rules] || []
|
||||
)
|
||||
unless is_atom(name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "action name must be an atom",
|
||||
path: [:actions, :create]
|
||||
end
|
||||
|
||||
@actions action
|
||||
case Ash.Resource.Actions.Create.new(name, opts) do
|
||||
{:ok, action} ->
|
||||
@actions action
|
||||
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
path: [:actions, :create, name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro update(name, opts \\ []) do
|
||||
quote bind_quoted: [name: name, opts: opts] do
|
||||
action =
|
||||
Ash.Resource.Actions.Update.new(name,
|
||||
primary?: opts[:primary?] || false,
|
||||
rules: opts[:rules] || []
|
||||
)
|
||||
|
||||
@actions action
|
||||
end
|
||||
end
|
||||
|
||||
defmacro destroy(name, opts \\ []) do
|
||||
quote bind_quoted: [name: name, opts: opts] do
|
||||
action =
|
||||
Ash.Resource.Actions.Destroy.new(name,
|
||||
primary?: opts[:primary?] || false,
|
||||
rules: opts[:rules] || []
|
||||
)
|
||||
|
||||
@actions action
|
||||
end
|
||||
end
|
||||
@doc """
|
||||
Declares a `read` action. For calling this action, see the `Ash.Api` documentation.
|
||||
|
||||
#{Ashton.document(Ash.Resource.Actions.Read.opt_schema())}
|
||||
"""
|
||||
defmacro read(name, opts \\ []) do
|
||||
quote bind_quoted: [name: name, opts: opts] do
|
||||
action =
|
||||
Ash.Resource.Actions.Read.new(name,
|
||||
primary?: opts[:primary?] || false,
|
||||
rules: opts[:rules] || [],
|
||||
paginate?: Keyword.get(opts, :paginate?, true)
|
||||
)
|
||||
unless is_atom(name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "action name must be an atom",
|
||||
path: [:actions, :read]
|
||||
end
|
||||
|
||||
@actions action
|
||||
case Ash.Resource.Actions.Read.new(name, opts) do
|
||||
{:ok, action} ->
|
||||
@actions action
|
||||
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
path: [:actions, :read, name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Declares an `update` action. For calling this action, see the `Ash.Api` documentation.
|
||||
|
||||
#{Ashton.document(Ash.Resource.Actions.Update.opt_schema())}
|
||||
"""
|
||||
defmacro update(name, opts \\ []) do
|
||||
quote bind_quoted: [name: name, opts: opts] do
|
||||
unless is_atom(name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "action name must be an atom",
|
||||
path: [:actions, :update]
|
||||
end
|
||||
|
||||
case Ash.Resource.Actions.Update.new(name, opts) do
|
||||
{:ok, action} ->
|
||||
@actions action
|
||||
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
path: [:actions, :update, name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Declares an `destroy` action. For calling this action, see the `Ash.Api` documentation.
|
||||
|
||||
#{Ashton.document(Ash.Resource.Actions.Destroy.opt_schema())}
|
||||
"""
|
||||
defmacro destroy(name, opts \\ []) do
|
||||
quote bind_quoted: [name: name, opts: opts] do
|
||||
unless is_atom(name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "action name must be an atom",
|
||||
path: [:actions, :destroy]
|
||||
end
|
||||
|
||||
case Ash.Resource.Actions.Destroy.new(name, opts) do
|
||||
{:ok, action} ->
|
||||
@actions action
|
||||
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
path: [:actions, :destroy, name]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,48 @@
|
|||
defmodule Ash.Resource.Actions.Create do
|
||||
@moduledoc "The representation of a `create` action."
|
||||
defstruct [:type, :name, :primary?, :rules]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :create,
|
||||
name: atom,
|
||||
primary?: boolean,
|
||||
rules: list(Ash.Authorization.Rule.t())
|
||||
}
|
||||
|
||||
@opt_schema Ashton.schema(
|
||||
opts: [
|
||||
primary?: :boolean,
|
||||
rules: {:list, {:struct, Ash.Authorization.Rule}}
|
||||
],
|
||||
defaults: [
|
||||
primary?: false,
|
||||
rules: []
|
||||
],
|
||||
describe: [
|
||||
primary?:
|
||||
"Whether or not this action should be used when no action is specified by the caller.",
|
||||
rules:
|
||||
"A list of `Ash.Authorization.Rule`s declaring the authorization of the action."
|
||||
]
|
||||
)
|
||||
|
||||
@doc false
|
||||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(atom, Keyword.t()) :: {:ok, t()} | {:error, term}
|
||||
def new(name, opts \\ []) do
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :create,
|
||||
primary?: opts[:primary?],
|
||||
rules: opts[:rules]
|
||||
}
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :create,
|
||||
primary?: opts[:primary?],
|
||||
rules: opts[:rules]
|
||||
}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,49 @@
|
|||
defmodule Ash.Resource.Actions.Destroy do
|
||||
@moduledoc "The representation of a `destroy` action"
|
||||
|
||||
defstruct [:type, :name, :primary?, :rules]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :destroy,
|
||||
name: atom,
|
||||
primary?: boolean,
|
||||
rules: list(Ash.Authorization.Rule.t())
|
||||
}
|
||||
|
||||
@opt_schema Ashton.schema(
|
||||
opts: [
|
||||
primary?: :boolean,
|
||||
rules: {:list, {:struct, Ash.Authorization.Rule}}
|
||||
],
|
||||
defaults: [
|
||||
primary?: false,
|
||||
rules: []
|
||||
],
|
||||
describe: [
|
||||
primary?:
|
||||
"Whether or not this action should be used when no action is specified by the caller.",
|
||||
rules:
|
||||
"A list of `Ash.Authorization.Rule`s declaring the authorization of the action."
|
||||
]
|
||||
)
|
||||
|
||||
@doc false
|
||||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(atom, Keyword.t()) :: {:ok, t()} | {:error, term}
|
||||
def new(name, opts \\ []) do
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :destroy,
|
||||
primary?: opts[:primary?],
|
||||
rules: opts[:rules]
|
||||
}
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :destroy,
|
||||
primary?: opts[:primary?],
|
||||
rules: opts[:rules]
|
||||
}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,13 +1,49 @@
|
|||
defmodule Ash.Resource.Actions.Read do
|
||||
defstruct [:type, :name, :primary?, :paginate?, :rules]
|
||||
@moduledoc "The representation of a `read` action"
|
||||
|
||||
defstruct [:type, :name, :primary?, :rules]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :read,
|
||||
name: atom,
|
||||
primary?: boolean,
|
||||
rules: list(Ash.Authorization.Rule.t())
|
||||
}
|
||||
|
||||
@opt_schema Ashton.schema(
|
||||
opts: [
|
||||
primary?: :boolean,
|
||||
rules: {:list, {:struct, Ash.Authorization.Rule}}
|
||||
],
|
||||
defaults: [
|
||||
primary?: false,
|
||||
rules: []
|
||||
],
|
||||
describe: [
|
||||
primary?:
|
||||
"Whether or not this action should be used when no action is specified by the caller.",
|
||||
rules:
|
||||
"A list of `Ash.Authorization.Rule`s declaring the authorization of the action."
|
||||
]
|
||||
)
|
||||
|
||||
@doc false
|
||||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(atom, Keyword.t()) :: {:ok, t()} | {:error, term}
|
||||
def new(name, opts \\ []) do
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :read,
|
||||
primary?: opts[:primary?],
|
||||
paginate?: opts[:paginate?],
|
||||
rules: opts[:rules]
|
||||
}
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :read,
|
||||
primary?: opts[:primary?],
|
||||
rules: opts[:rules]
|
||||
}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,49 @@
|
|||
defmodule Ash.Resource.Actions.Update do
|
||||
@moduledoc "The representation of a `update` action"
|
||||
|
||||
defstruct [:type, :name, :primary?, :rules]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :update,
|
||||
name: atom,
|
||||
primary?: boolean,
|
||||
rules: list(Ash.Authorization.Rule.t())
|
||||
}
|
||||
|
||||
@opt_schema Ashton.schema(
|
||||
opts: [
|
||||
primary?: :boolean,
|
||||
rules: {:list, {:struct, Ash.Authorization.Rule}}
|
||||
],
|
||||
defaults: [
|
||||
primary?: false,
|
||||
rules: []
|
||||
],
|
||||
describe: [
|
||||
primary?:
|
||||
"Whether or not this action should be used when no action is specified by the caller.",
|
||||
rules:
|
||||
"A list of `Ash.Authorization.Rule`s declaring the authorization of the action."
|
||||
]
|
||||
)
|
||||
|
||||
@doc false
|
||||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(atom, Keyword.t()) :: {:ok, t()} | {:error, term}
|
||||
def new(name, opts \\ []) do
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :update,
|
||||
primary?: opts[:primary?],
|
||||
rules: opts[:rules]
|
||||
}
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :update,
|
||||
primary?: opts[:primary?],
|
||||
rules: opts[:rules]
|
||||
}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,58 +9,30 @@ defmodule Ash.Resource.Attributes.Attribute do
|
|||
primary_key?: boolean()
|
||||
}
|
||||
|
||||
@builtins Ash.Type.builtins()
|
||||
@schema Ashton.schema(opts: [primary_key?: :boolean], defaults: [primary_key?: false])
|
||||
@schema Ashton.schema(
|
||||
opts: [primary_key?: :boolean],
|
||||
defaults: [primary_key?: false],
|
||||
describe: [
|
||||
primary_key?: "Whether this field is, or is part of, the primary key of a resource."
|
||||
]
|
||||
)
|
||||
|
||||
@doc false
|
||||
def attribute_schema(), do: @schema
|
||||
|
||||
def new(resource, name, type, opts \\ [])
|
||||
|
||||
def new(resource, name, _, _) when not is_atom(name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
resource: resource,
|
||||
message: "Attribute name must be an atom, got: #{inspect(name)}",
|
||||
path: [:attributes, :attribute]
|
||||
end
|
||||
|
||||
def new(resource, _name, type, _opts) when not is_atom(type) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
resource: resource,
|
||||
message: "Attribute type must be a built in type or a type module, got: #{inspect(type)}",
|
||||
path: [:attributes, :attribute]
|
||||
end
|
||||
|
||||
def new(resource, name, type, opts) when type in @builtins do
|
||||
@spec new(atom, Ash.Type.t(), Keyword.t()) :: {:ok, t()} | {:error, term}
|
||||
def new(name, type, opts) do
|
||||
case Ashton.validate(opts, @schema) do
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
resource: resource,
|
||||
message: message,
|
||||
path: [:attributes, :attribute],
|
||||
option: key
|
||||
|
||||
{:ok, opts} ->
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: type,
|
||||
primary_key?: opts[:primary_key?] || false
|
||||
}
|
||||
end
|
||||
end
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: type,
|
||||
primary_key?: opts[:primary_key?] || false
|
||||
}}
|
||||
|
||||
def new(resource, name, type, opts) do
|
||||
if Ash.Type.ash_type?(type) do
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: type,
|
||||
primary_key?: opts[:primary_key?] || false
|
||||
}
|
||||
else
|
||||
raise Ash.Error.ResourceDslError,
|
||||
resource: resource,
|
||||
message: "Attribute type must be a built in type or a type module, got: #{inspect(type)}",
|
||||
path: [:attributes, :attribute]
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
defmodule Ash.Resource.Attributes do
|
||||
@moduledoc """
|
||||
A DSL component for declaring attributes
|
||||
|
||||
Attributes are fields on an instance of a resource. The two required
|
||||
pieces of knowledge are the field name, and the type.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro attributes(do: block) do
|
||||
quote do
|
||||
import Ash.Resource.Attributes
|
||||
|
@ -7,9 +15,39 @@ defmodule Ash.Resource.Attributes do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Declares an attribute on the resource
|
||||
|
||||
Type can be either a built in type (see `Ash.Type`) for more, or a module
|
||||
implementing the `Ash.Type` behaviour.
|
||||
|
||||
#{Ashton.document(Ash.Resource.Attributes.Attribute.attribute_schema())}
|
||||
"""
|
||||
defmacro attribute(name, type, opts \\ []) do
|
||||
quote bind_quoted: [type: type, name: name, opts: opts] do
|
||||
@attributes Ash.Resource.Attributes.Attribute.new(__MODULE__, name, type, opts)
|
||||
unless is_atom(name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "Attribute name must be an atom, got: #{inspect(name)}",
|
||||
path: [:attributes, :attribute]
|
||||
end
|
||||
|
||||
unless type in Ash.Type.builtins() or Ash.Type.ash_type?(type) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message:
|
||||
"Attribute type must be a built in type or a type module, got: #{inspect(type)}",
|
||||
path: [:attributes, :attribute, name]
|
||||
end
|
||||
|
||||
case Ash.Resource.Attributes.Attribute.new(name, type, opts) do
|
||||
{:ok, attribute} ->
|
||||
@attributes attribute
|
||||
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
path: [:attributes, :attribute],
|
||||
option: key
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,36 +1,51 @@
|
|||
defmodule Ash.Resource.Relationships.BelongsTo do
|
||||
@doc false
|
||||
|
||||
@moduledoc "The representation of a `belongs_to` relationship"
|
||||
defstruct [
|
||||
:name,
|
||||
:cardinality,
|
||||
:type,
|
||||
:destination,
|
||||
:primary_key?,
|
||||
:define_field?,
|
||||
:field_type,
|
||||
:destination_field,
|
||||
:source_field
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :belongs_to,
|
||||
cardinality: :one
|
||||
cardinality: :one,
|
||||
name: atom,
|
||||
destination: Ash.resource(),
|
||||
primary_key?: boolean,
|
||||
define_field?: boolean,
|
||||
field_type: Ash.Type.t(),
|
||||
destination_field: atom,
|
||||
source_field: atom
|
||||
}
|
||||
|
||||
@opt_schema Ashton.schema(
|
||||
opts: [
|
||||
destination_field: :atom,
|
||||
source_field: :atom,
|
||||
primary_key?: :boolean
|
||||
primary_key?: :boolean,
|
||||
define_field?: :boolean,
|
||||
field_type: :atom
|
||||
],
|
||||
defaults: [
|
||||
destination_field: :id,
|
||||
primary_key?: false
|
||||
primary_key?: false,
|
||||
define_field?: true,
|
||||
field_type: :uuid
|
||||
],
|
||||
describe: [
|
||||
define_field?:
|
||||
"If set to `false` a field is not created on the resource for this relationship, and one must be manually added in `attributes`.",
|
||||
field_type: "The field type of the automatically created field.",
|
||||
destination_field:
|
||||
"The field on the related resource that should match the `source_field` on this resource.",
|
||||
source_field:
|
||||
"The field on this resource that should match the `destination_field` on the related resource. Default: <relationship_name>_id",
|
||||
"The field on this resource that should match the `destination_field` on the related resource. Default: [relationship_name]_id",
|
||||
primary_key?:
|
||||
"Whether this field is, or is part of, the primary key of a resource."
|
||||
]
|
||||
|
@ -40,28 +55,28 @@ defmodule Ash.Resource.Relationships.BelongsTo do
|
|||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(
|
||||
resource_name :: String.t(),
|
||||
name :: atom,
|
||||
related_resource :: Ash.resource(),
|
||||
opts :: Keyword.t()
|
||||
) :: t()
|
||||
def new(resource_name, name, related_resource, opts \\ []) do
|
||||
opts =
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :belongs_to,
|
||||
cardinality: :one,
|
||||
primary_key?: opts[:primary_key?],
|
||||
destination: related_resource,
|
||||
destination_field: opts[:destination_field],
|
||||
source_field: opts[:source_field] || :"#{name}_id"
|
||||
}}
|
||||
) :: {:ok, t()} | {:error, term}
|
||||
def new(name, related_resource, opts \\ []) do
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :belongs_to,
|
||||
cardinality: :one,
|
||||
field_type: opts[:field_type],
|
||||
define_field?: opts[:define_field?],
|
||||
primary_key?: opts[:primary_key?],
|
||||
destination: related_resource,
|
||||
destination_field: opts[:destination_field],
|
||||
source_field: opts[:source_field] || :"#{name}_id"
|
||||
}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,32 +5,32 @@ defmodule Ash.Resource.Relationships.HasMany do
|
|||
:cardinality,
|
||||
:destination,
|
||||
:destination_field,
|
||||
:source_field,
|
||||
:primary_key?
|
||||
:source_field
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :has_many,
|
||||
cardinality: :many
|
||||
cardinality: :many,
|
||||
name: atom,
|
||||
type: Ash.Type.t(),
|
||||
destination: Ash.resource(),
|
||||
destination_field: atom,
|
||||
source_field: atom
|
||||
}
|
||||
|
||||
@opt_schema Ashton.schema(
|
||||
opts: [
|
||||
destination_field: :atom,
|
||||
source_field: :atom,
|
||||
primary_key?: :boolean
|
||||
source_field: :atom
|
||||
],
|
||||
defaults: [
|
||||
source_field: :id,
|
||||
primary_key?: false
|
||||
source_field: :id
|
||||
],
|
||||
describe: [
|
||||
destination_field:
|
||||
"The field on the related resource that should match the `source_field` on this resource. Default: <resource.name>_id",
|
||||
"The field on the related resource that should match the `source_field` on this resource. Default: [resource.name]_id",
|
||||
source_field:
|
||||
"The field on this resource that should match the `destination_field` on the related resource.",
|
||||
primary_key?:
|
||||
"Whether this field is, or is part of, the primary key of a resource."
|
||||
"The field on this resource that should match the `destination_field` on the related resource."
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -38,28 +38,25 @@ defmodule Ash.Resource.Relationships.HasMany do
|
|||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(
|
||||
resource_name :: String.t(),
|
||||
name :: atom,
|
||||
related_resource :: Ash.resource(),
|
||||
opts :: Keyword.t()
|
||||
) :: t()
|
||||
def new(resource_name, resource_type, name, related_resource, opts \\ []) do
|
||||
opts =
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :has_many,
|
||||
cardinality: :many,
|
||||
primary_key?: opts[:primary_key?],
|
||||
destination: related_resource,
|
||||
destination_field: opts[:destination_field] || :"#{resource_type}_id",
|
||||
source_field: opts[:source_field]
|
||||
}}
|
||||
) :: {:ok, t()} | {:error, term}
|
||||
def new(resource_type, name, related_resource, opts \\ []) do
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :has_many,
|
||||
cardinality: :many,
|
||||
destination: related_resource,
|
||||
destination_field: opts[:destination_field] || :"#{resource_type}_id",
|
||||
source_field: opts[:source_field]
|
||||
}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,33 +5,33 @@ defmodule Ash.Resource.Relationships.HasOne do
|
|||
:type,
|
||||
:cardinality,
|
||||
:destination,
|
||||
:primary_key?,
|
||||
:destination_field,
|
||||
:source_field
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :has_one,
|
||||
cardinality: :one
|
||||
cardinality: :one,
|
||||
name: atom,
|
||||
type: Ash.Type.t(),
|
||||
destination: Ash.resource(),
|
||||
destination_field: atom,
|
||||
source_field: atom
|
||||
}
|
||||
|
||||
@opt_schema Ashton.schema(
|
||||
opts: [
|
||||
destination_field: :atom,
|
||||
source_field: :atom,
|
||||
primary_key?: :boolean
|
||||
source_field: :atom
|
||||
],
|
||||
defaults: [
|
||||
source_field: :id,
|
||||
primary_key?: false
|
||||
source_field: :id
|
||||
],
|
||||
describe: [
|
||||
destination_field:
|
||||
"The field on the related resource that should match the `source_field` on this resource. Default: <resource.name>_id",
|
||||
"The field on the related resource that should match the `source_field` on this resource. Default: [resource.name]_id",
|
||||
source_field:
|
||||
"The field on this resource that should match the `destination_field` on the related resource.",
|
||||
primary_key?:
|
||||
"Whether this field is, or is part of, the primary key of a resource."
|
||||
"The field on this resource that should match the `destination_field` on the related resource."
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -39,29 +39,27 @@ defmodule Ash.Resource.Relationships.HasOne do
|
|||
def opt_schema(), do: @opt_schema
|
||||
|
||||
@spec new(
|
||||
resource_name :: String.t(),
|
||||
resource_type :: String.t(),
|
||||
name :: atom,
|
||||
related_resource :: Ash.resource(),
|
||||
opts :: Keyword.t()
|
||||
) :: t()
|
||||
) :: {:ok, t()} | {:error, term}
|
||||
@doc false
|
||||
def new(resource_name, name, related_resource, opts \\ []) do
|
||||
opts =
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :has_one,
|
||||
cardinality: :one,
|
||||
destination: related_resource,
|
||||
primary_key?: opts[:primary_key?],
|
||||
destination_field: opts[:destination_field] || :"#{resource_name}_id",
|
||||
source_field: opts[:source_field]
|
||||
}}
|
||||
def new(resource_type, name, related_resource, opts \\ []) do
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
type: :has_one,
|
||||
cardinality: :one,
|
||||
destination: related_resource,
|
||||
destination_field: opts[:destination_field] || :"#{resource_type}_id",
|
||||
source_field: opts[:source_field]
|
||||
}}
|
||||
|
||||
{:error, errors} ->
|
||||
{:error, errors}
|
||||
end
|
||||
{:error, errors} ->
|
||||
{:error, errors}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,14 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
|
||||
@type t :: %__MODULE__{
|
||||
type: :many_to_many,
|
||||
cardinality: :many
|
||||
cardinality: :many,
|
||||
name: atom,
|
||||
through: Ash.resource() | String.t(),
|
||||
destination: Ash.resource(),
|
||||
source_field: atom,
|
||||
destination_field: atom,
|
||||
source_field_on_join_table: atom,
|
||||
destination_field_on_join_table: atom
|
||||
}
|
||||
|
||||
@opt_schema Ashton.schema(
|
||||
|
@ -32,10 +39,12 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
:through
|
||||
],
|
||||
describe: [
|
||||
through:
|
||||
"Either a string representing a table/generic name for the join table or a module name of a resource.",
|
||||
source_field_on_join_table:
|
||||
"The field on the join table that should line up with `source_field` on this resource. Default: <resource_name>_id",
|
||||
"The field on the join table that should line up with `source_field` on this resource. Default: [resource_name]_id",
|
||||
destination_field_on_join_table:
|
||||
"The field on the join table that should line up with `destination_field` on the related resource. Default: <relationshihp_name>_id",
|
||||
"The field on the join table that should line up with `destination_field` on the related resource. Default: [relationshihp_name]_id",
|
||||
source_field:
|
||||
"The field on this resource that should line up with `source_field_on_join_table` on the join table.",
|
||||
destination_field:
|
||||
|
@ -51,7 +60,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
name :: atom,
|
||||
related_resource :: Ash.resource(),
|
||||
opts :: Keyword.t()
|
||||
) :: t()
|
||||
) :: {:ok, t()} | {:error, term}
|
||||
def new(resource_name, name, related_resource, opts \\ []) do
|
||||
case Ashton.validate(opts, @opt_schema) do
|
||||
{:ok, opts} ->
|
||||
|
@ -67,7 +76,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
source_field_on_join_table:
|
||||
opts[:source_field_on_join_table] || :"#{resource_name}_id",
|
||||
destination_field_on_join_table:
|
||||
opts[:destination_field_on_join_table] || ":#{name}_id"
|
||||
opts[:destination_field_on_join_table] || :"#{name}_id"
|
||||
}}
|
||||
|
||||
{:error, errors} ->
|
||||
|
|
|
@ -35,14 +35,30 @@ defmodule Ash.Resource.Relationships do
|
|||
|
||||
#{Ashton.document(Ash.Resource.Relationships.HasOne.opt_schema(), header_depth: 2)}
|
||||
"""
|
||||
defmacro has_one(relationship_name, resource, opts \\ []) do
|
||||
quote do
|
||||
defmacro has_one(relationship_name, destination, opts \\ []) do
|
||||
quote bind_quoted: [
|
||||
relationship_name: relationship_name,
|
||||
destination: destination,
|
||||
opts: opts
|
||||
] do
|
||||
unless is_atom(relationship_name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "relationship_name must be an atom",
|
||||
path: [:relationships, :has_one]
|
||||
end
|
||||
|
||||
unless is_atom(destination) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "related resource must be a module representing a resource",
|
||||
path: [:relationships, :has_one, relationship_name]
|
||||
end
|
||||
|
||||
relationship =
|
||||
Ash.Resource.Relationships.HasOne.new(
|
||||
@name,
|
||||
unquote(relationship_name),
|
||||
unquote(resource),
|
||||
unquote(opts)
|
||||
@resource_type,
|
||||
relationship_name,
|
||||
destination,
|
||||
opts
|
||||
)
|
||||
|
||||
case relationship do
|
||||
|
@ -53,14 +69,15 @@ defmodule Ash.Resource.Relationships do
|
|||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
resource: __MODULE__,
|
||||
path: [:relationships, :has_one, unquote(relationship_name)]
|
||||
path: [:relationships, :has_one, relationship_name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Declares a belongs_to relationship. In a relationsal database, the foreign key would be on the *source* table.
|
||||
Declares a belongs_to relationship. In a relational database, the foreign key would be on the *source* table.
|
||||
|
||||
This creates a field on the resource with the corresponding name, unless `define_field?: false` is provided.
|
||||
|
||||
Practically speaking, a belongs_to and a has_one are interchangable in every way.
|
||||
|
||||
|
@ -75,33 +92,47 @@ defmodule Ash.Resource.Relationships do
|
|||
|
||||
#{Ashton.document(Ash.Resource.Relationships.BelongsTo.opt_schema(), header_depth: 2)}
|
||||
"""
|
||||
defmacro belongs_to(relationship_name, resource, config \\ []) do
|
||||
quote do
|
||||
defmacro belongs_to(relationship_name, destination, config \\ []) do
|
||||
quote bind_quoted: [
|
||||
relationship_name: relationship_name,
|
||||
destination: destination,
|
||||
config: config
|
||||
] do
|
||||
unless is_atom(relationship_name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "relationship_name must be an atom",
|
||||
path: [:relationships, :belongs_to]
|
||||
end
|
||||
|
||||
unless is_atom(destination) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "related resource must be a module representing a resource",
|
||||
path: [:relationships, :belongs_to, relationship_name]
|
||||
end
|
||||
|
||||
relationship =
|
||||
Ash.Resource.Relationships.BelongsTo.new(
|
||||
@name,
|
||||
unquote(relationship_name),
|
||||
unquote(resource),
|
||||
unquote(config)
|
||||
)
|
||||
Ash.Resource.Relationships.BelongsTo.new(relationship_name, destination, config)
|
||||
|
||||
case relationship do
|
||||
{:ok, relationship} ->
|
||||
# TODO: This assumes binary_id
|
||||
@attributes Ash.Resource.Attributes.Attribute.new(
|
||||
__MODULE__,
|
||||
relationship.source_field,
|
||||
:uuid,
|
||||
primary_key?: relationship.primary_key?
|
||||
)
|
||||
if relationship.define_field? do
|
||||
{:ok, attribute} =
|
||||
Ash.Resource.Attributes.Attribute.new(
|
||||
relationship.source_field,
|
||||
relationship.field_type,
|
||||
primary_key?: relationship.primary_key?
|
||||
)
|
||||
|
||||
@attributes attribute
|
||||
end
|
||||
|
||||
@relationships relationship
|
||||
|
||||
{:error, [{key, message}]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
resource: __MODULE__,
|
||||
path: [:relationships, :belongs_to, unquote(relationship_name)]
|
||||
path: [:relationships, :belongs_to, relationship_name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -120,15 +151,30 @@ defmodule Ash.Resource.Relationships do
|
|||
|
||||
#{Ashton.document(Ash.Resource.Relationships.HasMany.opt_schema(), header_depth: 2)}
|
||||
"""
|
||||
defmacro has_many(relationship_name, resource, config \\ []) do
|
||||
quote do
|
||||
defmacro has_many(relationship_name, destination, opts \\ []) do
|
||||
quote bind_quoted: [
|
||||
relationship_name: relationship_name,
|
||||
destination: destination,
|
||||
opts: opts
|
||||
] do
|
||||
unless is_atom(relationship_name) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "relationship_name must be an atom",
|
||||
path: [:relationships, :has_many]
|
||||
end
|
||||
|
||||
unless is_atom(destination) do
|
||||
raise Ash.Error.ResourceDslError,
|
||||
message: "related resource must be a module representing a resource",
|
||||
path: [:relationships, :has_many, relationship_name]
|
||||
end
|
||||
|
||||
relationship =
|
||||
Ash.Resource.Relationships.HasMany.new(
|
||||
@name,
|
||||
@resource_type,
|
||||
unquote(relationship_name),
|
||||
unquote(resource),
|
||||
unquote(config)
|
||||
relationship_name,
|
||||
destination,
|
||||
opts
|
||||
)
|
||||
|
||||
case relationship do
|
||||
|
@ -139,8 +185,7 @@ defmodule Ash.Resource.Relationships do
|
|||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
resource: __MODULE__,
|
||||
path: [:relationships, :has_many, unquote(relationship_name)]
|
||||
path: [:relationships, :has_many, relationship_name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -184,7 +229,6 @@ defmodule Ash.Resource.Relationships do
|
|||
raise Ash.Error.ResourceDslError,
|
||||
message: message,
|
||||
option: key,
|
||||
resource: __MODULE__,
|
||||
path: [:relationships, :many_to_many, unquote(relationship_name)]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -76,7 +76,7 @@ defmodule Ash.Type do
|
|||
"""
|
||||
@spec cast_input(t(), term) :: {:ok, term} | {:error, keyword()} | :error
|
||||
def cast_input(type, term) when type in @builtin_names do
|
||||
Ecto.Type.cast(@builtins[term][:ecto_type], term)
|
||||
Ecto.Type.cast(@builtins[type][:ecto_type], term)
|
||||
end
|
||||
|
||||
def cast_input(type, term) do
|
||||
|
@ -189,10 +189,12 @@ defmodule Ash.Type do
|
|||
@doc "Returns true if the value is a builtin type or adopts the `Ash.Type` behaviour"
|
||||
def ash_type?(atom) when atom in @builtin_names, do: true
|
||||
|
||||
def ash_type?(module) do
|
||||
:erlang.function_exported(module, :module_info, 0) and ash_type_module?(module)
|
||||
def ash_type?(module) when is_atom(module) do
|
||||
:erlang.function_exported(module, :__info__, 1) and ash_type_module?(module)
|
||||
end
|
||||
|
||||
def ash_type?(_), do: false
|
||||
|
||||
defp ash_type_module?(module) do
|
||||
:attributes
|
||||
|> module.module_info()
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -49,7 +49,7 @@ defmodule Ash.MixProject do
|
|||
{:ecto, "~> 3.0"},
|
||||
{:ets, github: "zachdaniel/ets", ref: "b96da05e75926e340e8a0fdfea9c095d97ed8d50"},
|
||||
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
|
||||
{:ashton, "~> 0.3.9"}
|
||||
{:ashton, "~> 0.3.10"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -1,5 +1,5 @@
|
|||
%{
|
||||
"ashton": {:hex, :ashton, "0.3.9", "1c089d62d35a17c1f31db4e9130fb90f8d802c8c9078fd29138be7b6b93305b5", [:mix], [], "hexpm"},
|
||||
"ashton": {:hex, :ashton, "0.3.10", "ce0ab19f154c7fe8fefbc1486fdf7b601a0fa944555284182755197b1c073464", [:mix], [], "hexpm"},
|
||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
|
||||
"dataloader": {:hex, :dataloader, "1.0.6", "fb724d6d3fb6acb87d27e3b32dea3a307936ad2d245faf9cf5221d1323d6a4ba", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AshTest do
|
||||
use ExUnit.Case
|
||||
use ExUnit.Case, async: true
|
||||
doctest Ash
|
||||
end
|
||||
|
|
90
test/dsl/resource/actions/create_test.exs
Normal file
90
test/dsl/resource/actions/create_test.exs
Normal file
|
@ -0,0 +1,90 @@
|
|||
defmodule Ash.Test.Dsl.Resource.Actions.CreateTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "it creates an action" do
|
||||
defposts do
|
||||
actions do
|
||||
create :default
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Actions.Create{
|
||||
name: :default,
|
||||
primary?: true,
|
||||
rules: [],
|
||||
type: :create
|
||||
}
|
||||
] = Ash.actions(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validation" do
|
||||
test "it fails if `name` is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"action name must be an atom at actions -> create",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
create "default"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if `primary?` is not a boolean" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option primary? at actions -> create -> default must be of type :boolean",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
create :default, primary?: 10
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if `rules` is not a list" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option rules at actions -> create -> default must be of type {:list, {:struct, Ash.Authorization.Rule}}",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
create :default, rules: 10
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if the elements of the rules list are not rules" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option rules at actions -> create -> default must be of type {:list, {:struct, Ash.Authorization.Rule}}",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
create :default, rules: [10]
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
90
test/dsl/resource/actions/destroy_test.exs
Normal file
90
test/dsl/resource/actions/destroy_test.exs
Normal file
|
@ -0,0 +1,90 @@
|
|||
defmodule Ash.Test.Dsl.Resource.Actions.DestroyTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "it creates an action" do
|
||||
defposts do
|
||||
actions do
|
||||
destroy :default
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Actions.Destroy{
|
||||
name: :default,
|
||||
primary?: true,
|
||||
rules: [],
|
||||
type: :destroy
|
||||
}
|
||||
] = Ash.actions(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validation" do
|
||||
test "it fails if `name` is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"action name must be an atom at actions -> destroy",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
destroy "default"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if `primary?` is not a boolean" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option primary? at actions -> destroy -> default must be of type :boolean",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
destroy :default, primary?: 10
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if `rules` is not a list" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option rules at actions -> destroy -> default must be of type {:list, {:struct, Ash.Authorization.Rule}}",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
destroy :default, rules: 10
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if the elements of the rules list are not rules" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option rules at actions -> destroy -> default must be of type {:list, {:struct, Ash.Authorization.Rule}}",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
destroy :default, rules: [10]
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
90
test/dsl/resource/actions/read_test.exs
Normal file
90
test/dsl/resource/actions/read_test.exs
Normal file
|
@ -0,0 +1,90 @@
|
|||
defmodule Ash.Test.Dsl.Resource.Actions.ReadTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "it creates an action" do
|
||||
defposts do
|
||||
actions do
|
||||
read :default
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Actions.Read{
|
||||
name: :default,
|
||||
primary?: true,
|
||||
rules: [],
|
||||
type: :read
|
||||
}
|
||||
] = Ash.actions(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validation" do
|
||||
test "it fails if `name` is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"action name must be an atom at actions -> read",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
read "default"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if `primary?` is not a boolean" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option primary? at actions -> read -> default must be of type :boolean",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
read :default, primary?: 10
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if `rules` is not a list" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option rules at actions -> read -> default must be of type {:list, {:struct, Ash.Authorization.Rule}}",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
read :default, rules: 10
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if the elements of the rules list are not rules" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option rules at actions -> read -> default must be of type {:list, {:struct, Ash.Authorization.Rule}}",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
read :default, rules: [10]
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
90
test/dsl/resource/actions/update_test.exs
Normal file
90
test/dsl/resource/actions/update_test.exs
Normal file
|
@ -0,0 +1,90 @@
|
|||
defmodule Ash.Test.Dsl.Resource.Actions.UpdateTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "it creates an action" do
|
||||
defposts do
|
||||
actions do
|
||||
update :default
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Actions.Update{
|
||||
name: :default,
|
||||
primary?: true,
|
||||
rules: [],
|
||||
type: :update
|
||||
}
|
||||
] = Ash.actions(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validation" do
|
||||
test "it fails if `name` is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"action name must be an atom at actions -> update",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
update "default"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if `primary?` is not a boolean" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option primary? at actions -> update -> default must be of type :boolean",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
update :default, primary?: 10
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if `rules` is not a list" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option rules at actions -> update -> default must be of type {:list, {:struct, Ash.Authorization.Rule}}",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
update :default, rules: 10
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if the elements of the rules list are not rules" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option rules at actions -> update -> default must be of type {:list, {:struct, Ash.Authorization.Rule}}",
|
||||
fn ->
|
||||
defposts do
|
||||
actions do
|
||||
update :default, rules: [10]
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,18 +4,31 @@ defmodule Ash.Test.Dsl.Resource.AttributesTest do
|
|||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post"
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "attributes are persisted on the resource properly" do
|
||||
defposts do
|
||||
attributes do
|
||||
attribute :foo, :string
|
||||
end
|
||||
end
|
||||
|
||||
assert [%Ash.Resource.Attributes.Attribute{name: :foo, type: :string, primary_key?: false}] =
|
||||
Ash.attributes(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validation" do
|
||||
test "raises if the attribute name is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"Ash.Test.Dsl.Resource.AttributesTest.Post: Attribute name must be an atom, got: 10 at attributes->attribute",
|
||||
"Attribute name must be an atom, got: 10 at attributes -> attribute",
|
||||
fn ->
|
||||
defposts do
|
||||
attributes do
|
||||
|
@ -29,7 +42,7 @@ defmodule Ash.Test.Dsl.Resource.AttributesTest do
|
|||
test "raises if the type is not a known type" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"Ash.Test.Dsl.Resource.AttributesTest.Post: Attribute type must be a built in type or a type module, got: 10 at attributes->attribute",
|
||||
"Attribute type must be a built in type or a type module, got: 10 at attributes -> attribute -> foo",
|
||||
fn ->
|
||||
defposts do
|
||||
attributes do
|
||||
|
@ -43,7 +56,7 @@ defmodule Ash.Test.Dsl.Resource.AttributesTest do
|
|||
test "raises if you pass an invalid value for `primary_key?`" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"Ash.Test.Dsl.Resource.AttributesTest.Post: option primary_key? at attributes->attribute must be of type :boolean",
|
||||
"option primary_key? at attributes -> attribute must be of type :boolean",
|
||||
fn ->
|
||||
defposts do
|
||||
attributes do
|
||||
|
|
153
test/dsl/resource/relationships/belongs_to_test.exs
Normal file
153
test/dsl/resource/relationships/belongs_to_test.exs
Normal file
|
@ -0,0 +1,153 @@
|
|||
defmodule Ash.Test.Dsl.Resource.Relationships.BelongsToTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "it creates an attribute" do
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to :foobar, FooBar
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Attributes.Attribute{
|
||||
name: :foobar_id,
|
||||
primary_key?: false,
|
||||
type: :uuid
|
||||
}
|
||||
] = Ash.attributes(Post)
|
||||
end
|
||||
|
||||
test "it creates a relationship" do
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to :foobar, FooBar
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Relationships.BelongsTo{
|
||||
cardinality: :one,
|
||||
define_field?: true,
|
||||
destination: FooBar,
|
||||
destination_field: :id,
|
||||
field_type: :uuid,
|
||||
name: :foobar,
|
||||
primary_key?: false,
|
||||
source_field: :foobar_id,
|
||||
type: :belongs_to
|
||||
}
|
||||
] = Ash.relationships(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
test "fails if destination_field is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option destination_field at relationships -> belongs_to -> foobar must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to :foobar, FooBar, destination_field: "foo"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if source_field is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option source_field at relationships -> belongs_to -> foobar must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to :foobar, FooBar, source_field: "foo"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if the destination is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"related resource must be a module representing a resource at relationships -> belongs_to -> foobar",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to :foobar, "foobar"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if the relationship name is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"relationship_name must be an atom at relationships -> belongs_to",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to "foobar", Foobar
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if `primary_key?` is not a boolean" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option primary_key? at relationships -> belongs_to -> foobar must be of type :boolean",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to :foobar, Foobar, primary_key?: "blah"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
test "fails if `define_field?` is not a boolean" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option define_field? at relationships -> belongs_to -> foobar must be of type :boolean",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to :foobar, Foobar, define_field?: "blah"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if `field_type` is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option field_type at relationships -> belongs_to -> foobar must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
belongs_to :foobar, Foobar, field_type: "foo"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
92
test/dsl/resource/relationships/has_many_test.exs
Normal file
92
test/dsl/resource/relationships/has_many_test.exs
Normal file
|
@ -0,0 +1,92 @@
|
|||
defmodule Ash.Test.Dsl.Resource.Relationshihps.HasManyTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "it creates a relationship" do
|
||||
defposts do
|
||||
relationships do
|
||||
has_many :foobar, FooBar
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Relationships.HasMany{
|
||||
cardinality: :many,
|
||||
destination: FooBar,
|
||||
destination_field: :post_id,
|
||||
name: :foobar,
|
||||
source_field: :id,
|
||||
type: :has_many
|
||||
}
|
||||
] = Ash.relationships(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
test "fails if destination_field is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option destination_field at relationships -> has_many -> foobar must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
has_many :foobar, FooBar, destination_field: "foo"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if source_field is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option source_field at relationships -> has_many -> foobar must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
has_many :foobar, FooBar, source_field: "foo"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if the destination is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"related resource must be a module representing a resource at relationships -> has_many -> foobar",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
has_many :foobar, "foobar"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if the relationship name is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"relationship_name must be an atom at relationships -> has_many",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
has_many "foobar", Foobar
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
92
test/dsl/resource/relationships/has_one_test.exs
Normal file
92
test/dsl/resource/relationships/has_one_test.exs
Normal file
|
@ -0,0 +1,92 @@
|
|||
defmodule Ash.Test.Dsl.Resource.Relationshihps.HasOneTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "it creates a relationship" do
|
||||
defposts do
|
||||
relationships do
|
||||
has_one :foobar, FooBar
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Relationships.HasOne{
|
||||
cardinality: :one,
|
||||
destination: FooBar,
|
||||
destination_field: :post_id,
|
||||
name: :foobar,
|
||||
source_field: :id,
|
||||
type: :has_one
|
||||
}
|
||||
] = Ash.relationships(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
test "fails if destination_field is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option destination_field at relationships -> has_one -> foobar must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
has_one :foobar, FooBar, destination_field: "foo"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if source_field is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option source_field at relationships -> has_one -> foobar must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
has_one :foobar, FooBar, source_field: "foo"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if the destination is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"related resource must be a module representing a resource at relationships -> has_one -> foobar",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
has_one :foobar, "foobar"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "fails if the relationship name is not an atom" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"relationship_name must be an atom at relationships -> has_one",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
has_one "foobar", Foobar
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
117
test/dsl/resource/relationships/many_to_many_test.exs
Normal file
117
test/dsl/resource/relationships/many_to_many_test.exs
Normal file
|
@ -0,0 +1,117 @@
|
|||
defmodule Ash.Test.Dsl.Resource.Relationships.ManyToManyTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop defposts(do: body) do
|
||||
quote do
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post", primary_key: false
|
||||
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "representation" do
|
||||
test "it creates a relationship" do
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar, through: "some_table"
|
||||
end
|
||||
end
|
||||
|
||||
assert [
|
||||
%Ash.Resource.Relationships.ManyToMany{
|
||||
cardinality: :many,
|
||||
destination: Foobar,
|
||||
destination_field: :id,
|
||||
destination_field_on_join_table: :foobars_id,
|
||||
name: :foobars,
|
||||
source_field: :id,
|
||||
source_field_on_join_table: :posts_id,
|
||||
through: "some_table",
|
||||
type: :many_to_many
|
||||
}
|
||||
] = Ash.relationships(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validation" do
|
||||
test "you can pass a string to `through`" do
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar, through: "some_table"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "you can pass a module to `through`" do
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar, through: FooBars
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "it fails if you dont pass an atom for `source_field_on_join_table`" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option source_field_on_join_table at relationships -> many_to_many -> foobars must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar, through: "table", source_field_on_join_table: "what"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if you dont pass an atom for `destination_field_on_join_table`" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option destination_field_on_join_table at relationships -> many_to_many -> foobars must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar,
|
||||
through: "table",
|
||||
destination_field_on_join_table: "what"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if you dont pass an atom for `source_field`" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option source_field at relationships -> many_to_many -> foobars must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar,
|
||||
through: "table",
|
||||
source_field: "what"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "it fails if you dont pass an atom for `destination_field`" do
|
||||
assert_raise(
|
||||
Ash.Error.ResourceDslError,
|
||||
"option destination_field at relationships -> many_to_many -> foobars must be of type :atom",
|
||||
fn ->
|
||||
defposts do
|
||||
relationships do
|
||||
many_to_many :foobars, Foobar,
|
||||
through: "table",
|
||||
destination_field: "what"
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1 +1,4 @@
|
|||
ExUnit.start()
|
||||
|
||||
# We compile modules with the same name often while testing the DSL
|
||||
Code.compiler_options(ignore_module_conflict: true)
|
||||
|
|
Loading…
Reference in a new issue