test and docs

This commit is contained in:
Zach Daniel 2019-12-06 02:00:26 -05:00
parent b3c609a4e0
commit 05e84102dd
No known key found for this signature in database
GPG key ID: A57053A671EE649E
31 changed files with 1424 additions and 293 deletions

4
.gitignore vendored
View file

@ -23,4 +23,6 @@ erl_crash.dump
ash-*.tar
# Ignoring Elixir Language Server
.elixir_ls/
.elixir_ls/
.DS_Store

View file

@ -0,0 +1,3 @@
defmodule Ash.Authorization do
@moduledoc "The main documentation for authorization in ash."
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"},

View file

@ -1,4 +1,4 @@
defmodule AshTest do
use ExUnit.Case
use ExUnit.Case, async: true
doctest Ash
end

View 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

View 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

View 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

View 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

View file

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

View 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

View 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

View 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

View 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

View file

@ -1 +1,4 @@
ExUnit.start()
# We compile modules with the same name often while testing the DSL
Code.compiler_options(ignore_module_conflict: true)