This commit is contained in:
Zach Daniel 2019-12-04 18:04:07 -05:00
parent 7a23dccdfa
commit df70095e39
No known key found for this signature in database
GPG key ID: A57053A671EE649E
11 changed files with 149 additions and 82 deletions

View file

@ -30,6 +30,6 @@
* use a process to hold constructed DSL state, and then coalesce it all at the end. This can clean things up, and also allow us to potentially eliminate the registry. This will probably go hand in hand w/ the "capabilities" layer, where the DSL confirms that your data layer is capable of performing everything that your DSL declares
* make ets dep optional
* Bake in descriptions to the DSL
* Contributor guideline and code of conduct

View file

@ -26,7 +26,6 @@ defmodule Ash do
end
def relationship(resource, relationship_name) do
# TODO: Make this happen at compile time
Enum.find(resource.relationships(), &(&1.name == relationship_name))
end

View file

@ -0,0 +1,14 @@
defmodule Ash.Error.ResourceDslError do
defexception [:message, :path, :option, :resource]
def message(%{message: message, path: dsl_path, option: nil, resource: resource}) do
dsl_path = Enum.join(dsl_path, "->")
"#{inspect(resource)}: #{message} at #{dsl_path}"
end
def message(%{message: message, path: dsl_path, option: option, resource: resource}) do
dsl_path = Enum.join(dsl_path, "->")
"#{inspect(resource)}: option #{option} at #{dsl_path} #{message}"
end
end

View file

@ -1,6 +1,6 @@
defmodule Ash.Resource do
defmacro __using__(opts) do
quote location: :keep do
quote do
@before_compile Ash.Resource
@skip_data_layer unquote(Keyword.get(opts, :no_data_layer, false))
@ -12,7 +12,9 @@ defmodule Ash.Resource do
Module.register_attribute(__MODULE__, :mix_ins, accumulate: true)
if unquote(Keyword.get(opts, :primary_key?, true)) do
@attributes Ash.Resource.Attributes.Attribute.new(:id, :uuid, primary_key?: true)
@attributes Ash.Resource.Attributes.Attribute.new(__MODULE__, :id, :uuid,
primary_key?: true
)
end
# Module.put_attribute(__MODULE__, :custom_threshold_for_lib, 10)
@ -41,7 +43,7 @@ defmodule Ash.Resource do
end
defmacro __before_compile__(env) do
quote location: :keep do
quote do
@sanitized_actions Ash.Resource.mark_primaries(@actions)
@ash_primary_key Ash.Resource.primary_key(@attributes)

View file

@ -1,21 +1,59 @@
defmodule Ash.Resource.Attributes.Attribute do
defstruct [:name, :type, :ecto_type, :primary_key?]
defstruct [:name, :type, :primary_key?]
def new(name, type, opts \\ []) do
# TODO: Remove `ecto_type` here and do that mapping in
# the database layer
ecto_type =
if type == :uuid do
:binary_id
else
type
end
@builtins Ash.Type.builtins()
%__MODULE__{
name: name,
type: type,
ecto_type: ecto_type,
primary_key?: opts[:primary_key?] || false
}
@option_schema Ashton.schema(opts: [primary_key?: :boolean])
@doc false
def attribute_schema(), do: @option_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
case Ashton.validate(opts, @option_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
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]
end
end
end

View file

@ -9,7 +9,7 @@ defmodule Ash.Resource.Attributes do
defmacro attribute(name, type, opts \\ []) do
quote bind_quoted: [type: type, name: name, opts: opts] do
@attributes Ash.Resource.Attributes.Attribute.new(name, type, opts)
@attributes Ash.Resource.Attributes.Attribute.new(__MODULE__, name, type, opts)
end
end
end

14
lib/ash/type/type.ex Normal file
View file

@ -0,0 +1,14 @@
defmodule Ash.Type do
def builtins(), do: [:string, :uuid]
def ash_type?(module) do
:erlang.function_exported(module, :module_info, 0) and ash_type_module?(module)
end
defp ash_type_module?(module) do
:attributes
|> module.module_info()
|> Keyword.get(:behaviour, [])
|> Enum.any?(&(&1 == __MODULE__))
end
end

View file

@ -1,61 +1,2 @@
defmodule Ash.Test do
defmacro test_resource(name, type, opts \\ [], do: block) do
quote do
module_name = Module.concat(__MODULE__, String.capitalize(unquote(name)))
Module.put_attribute(
__MODULE__,
unquote(opts[:attr]) || String.to_atom(unquote(name)),
module_name
)
defmodule module_name do
use Ash.Resource, name: unquote(name), type: unquote(type)
use Ash.DataLayer.Ets, private?: true
unquote(block)
end
module_name
end
end
defmacro test_api(opts, do: block) do
quote do
opts = unquote(opts)
module_name = Module.concat(__MODULE__, Api)
Module.put_attribute(__MODULE__, opts[:attr] || :api, module_name)
resources =
Enum.map(
opts[:resources],
fn
{:@, _, [{attr, _, nil}]} ->
Module.get_attribute(__MODULE__, attr)
resource when is_atom(resource) ->
Module.get_attribute(__MODULE__, resource) || resource
end
)
defmodule module_name do
use Ash.Api
api do
resources resources
end
unquote(block)
end
module_name
end
end
defmacro test_api(opts) do
quote do
test_api(unquote(opts)) do
:ok
end
end
end
end

View file

@ -42,7 +42,8 @@ defmodule Ash.MixProject do
[
{:ecto, "~> 3.0"},
{:ets, github: "zachdaniel/ets", ref: "b96da05e75926e340e8a0fdfea9c095d97ed8d50"},
{:ex_doc, "~> 0.21", only: :dev, runtime: false}
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
{:ashton, "~> 0.3.6"}
]
end
end

View file

@ -1,4 +1,5 @@
%{
"ashton": {:hex, :ashton, "0.3.6", "95f5d598c2e05662498349d81e7579e897b1e1cfe1aa79606d07a35305a47efe", [: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

@ -0,0 +1,57 @@
defmodule Ash.Test.Dsl.Resource.AttributesTest do
use ExUnit.Case, async: true
defmacrop defposts(do: body) do
quote do
defmodule Post do
use Ash.Resource, name: "posts", type: "post"
unquote(body)
end
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",
fn ->
defposts do
attributes do
attribute 10, :string
end
end
end
)
end
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",
fn ->
defposts do
attributes do
attribute :foo, 10
end
end
end
)
end
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",
fn ->
defposts do
attributes do
attribute :foo, :string, primary_key?: 10
end
end
end
)
end
end
end