mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
WIP
This commit is contained in:
parent
7a23dccdfa
commit
df70095e39
11 changed files with 149 additions and 82 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
14
lib/ash/error/resource_dsl_error.ex
Normal file
14
lib/ash/error/resource_dsl_error.ex
Normal 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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
14
lib/ash/type/type.ex
Normal 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
|
59
lib/test.ex
59
lib/test.ex
|
@ -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
|
||||
|
|
3
mix.exs
3
mix.exs
|
@ -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
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -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"},
|
||||
|
|
57
test/dsl/resource/attributes_test.exs
Normal file
57
test/dsl/resource/attributes_test.exs
Normal 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
|
Loading…
Reference in a new issue