mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
WIP on cleanup/documentation
This commit is contained in:
parent
df70095e39
commit
030c389225
13 changed files with 174 additions and 67 deletions
|
@ -31,5 +31,7 @@
|
|||
* make ets dep optional
|
||||
* Bake in descriptions to the DSL
|
||||
* Contributor guideline and code of conduct
|
||||
* Do branch analysis of each record after authorizing it, in authorizer
|
||||
* consider moving `type` and `name` for resources out into json api (or perhaps just `name`) since only json api uses that
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
use Mix.Config
|
||||
|
||||
if Mix.env() == :test do
|
||||
config :ash,
|
||||
resources: [
|
||||
Ash.Test.Post
|
||||
]
|
||||
end
|
26
lib/ash.ex
26
lib/ash.ex
|
@ -1,5 +1,12 @@
|
|||
defmodule Ash do
|
||||
@moduledoc """
|
||||
The primary interface for interrogating apis and resources, an
|
||||
|
||||
This is not the code level interface for a resource. Instead, call functions
|
||||
on an `Api` module that contains those resources.
|
||||
"""
|
||||
alias Ash.Resource.Relationships.{BelongsTo, HasOne, HasMany, ManyToMany}
|
||||
alias Ash.Resource.Actions.{Create, Read, Update, Destroy}
|
||||
|
||||
@type record :: struct
|
||||
@type cardinality_one_relationship() :: HasOne.t() | BelongsTo.t()
|
||||
|
@ -7,20 +14,27 @@ defmodule Ash do
|
|||
@type relationship :: cardinality_one_relationship() | cardinality_many_relationship()
|
||||
@type query :: struct
|
||||
@type resource :: module
|
||||
@type data_layer :: module
|
||||
@type api :: module
|
||||
@type error :: struct
|
||||
@type filter :: map()
|
||||
@type sort :: Keyword.t()
|
||||
@type side_loads :: Keyword.t()
|
||||
@type attribute :: Ash.Attributes.Attribute.t()
|
||||
@type action :: Create.t() | Read.t() | Update.t() | Destroy.t()
|
||||
@type side_load_config :: Keyword.t()
|
||||
|
||||
@spec resources(api) :: list(resource())
|
||||
def resources(api) do
|
||||
api.resources()
|
||||
end
|
||||
|
||||
@spec primary_key(resource()) :: nil | attribute() | list(attribute)
|
||||
def primary_key(resource) do
|
||||
resource.primary_key()
|
||||
end
|
||||
|
||||
@spec relationship(resource(), atom() | String.t()) :: relationship() | nil
|
||||
def relationship(resource, relationship_name) when is_bitstring(relationship_name) do
|
||||
Enum.find(resource.relationships(), &(to_string(&1.name) == relationship_name))
|
||||
end
|
||||
|
@ -29,14 +43,17 @@ defmodule Ash do
|
|||
Enum.find(resource.relationships(), &(&1.name == relationship_name))
|
||||
end
|
||||
|
||||
@spec relationships(resource()) :: list(relationship())
|
||||
def relationships(resource) do
|
||||
resource.relationships()
|
||||
end
|
||||
|
||||
@spec side_load_config(api()) :: side_load_config()
|
||||
def side_load_config(api) do
|
||||
api.side_load_config()
|
||||
end
|
||||
|
||||
@spec primary_action(resource(), atom()) :: action() | nil
|
||||
def primary_action(resource, type) do
|
||||
resource
|
||||
|> actions()
|
||||
|
@ -47,14 +64,17 @@ defmodule Ash do
|
|||
end
|
||||
end
|
||||
|
||||
@spec action(resource(), atom(), atom()) :: action() | nil
|
||||
def action(resource, name, type) do
|
||||
Enum.find(resource.actions(), &(&1.name == name && &1.type == type))
|
||||
end
|
||||
|
||||
@spec actions(resource()) :: list(action())
|
||||
def actions(resource) do
|
||||
resource.actions()
|
||||
end
|
||||
|
||||
@spec attribute(resource(), String.t() | atom) :: attribute() | nil
|
||||
def attribute(resource, name) when is_bitstring(name) do
|
||||
Enum.find(resource.attributes, &(to_string(&1.name) == name))
|
||||
end
|
||||
|
@ -63,26 +83,32 @@ defmodule Ash do
|
|||
Enum.find(resource.attributes, &(&1.name == name))
|
||||
end
|
||||
|
||||
@spec attributes(resource()) :: list(attribute())
|
||||
def attributes(resource) do
|
||||
resource.attributes()
|
||||
end
|
||||
|
||||
@spec name(resource()) :: String.t()
|
||||
def name(resource) do
|
||||
resource.name()
|
||||
end
|
||||
|
||||
@spec type(resource()) :: String.t()
|
||||
def type(resource) do
|
||||
resource.type()
|
||||
end
|
||||
|
||||
@spec max_page_size(api(), resource()) :: non_neg_integer() | nil
|
||||
def max_page_size(api, resource) do
|
||||
min(api.max_page_size(), resource.max_page_size())
|
||||
end
|
||||
|
||||
@spec default_page_size(api(), resource()) :: non_neg_integer() | nil
|
||||
def default_page_size(api, resource) do
|
||||
min(api.default_page_size(), resource.default_page_size())
|
||||
end
|
||||
|
||||
@spec data_layer(resource()) :: data_layer()
|
||||
def data_layer(resource) do
|
||||
resource.data_layer()
|
||||
end
|
||||
|
|
4
lib/ash/constraints.ex
Normal file
4
lib/ash/constraints.ex
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule Ash.Constraints do
|
||||
def positive?(integer), do: integer >= 0
|
||||
def greater_than_zero?(integer), do: integer > 0
|
||||
end
|
|
@ -1,6 +1,10 @@
|
|||
defmodule Ash.Error.ResourceDslError do
|
||||
defexception [:message, :path, :option, :resource]
|
||||
|
||||
def message(%{message: message, path: nil, option: option, resource: resource}) do
|
||||
"#{inspect(resource)}: #{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}"
|
||||
|
|
|
@ -1,44 +1,117 @@
|
|||
defmodule Ash.Resource do
|
||||
@primary_key_schema Ashton.schema(
|
||||
opts: [field: :atom, type: :atom],
|
||||
defaults: [field: :id, type: :uuid],
|
||||
describe: [
|
||||
field: "The field name of the primary key of the resource.",
|
||||
type: "The data type of the primary key of the resource."
|
||||
]
|
||||
)
|
||||
|
||||
@resource_opts_schema Ashton.schema(
|
||||
opts: [
|
||||
name: :string,
|
||||
type: :string,
|
||||
max_page_size: :integer,
|
||||
default_page_size: :integer,
|
||||
primary_key: [
|
||||
:boolean,
|
||||
@primary_key_schema
|
||||
]
|
||||
],
|
||||
describe: [
|
||||
name:
|
||||
"The name of the resource. This will typically be the pluralized form of the type",
|
||||
type:
|
||||
"The type of the resource, e.g `post` or `author`. This is used throughout the system.",
|
||||
max_page_size:
|
||||
"The maximum page size for any read action. Any request for a higher page size will simply use this number.",
|
||||
default_page_size:
|
||||
"The default page size for any read action. If no page size is specified, this value is used.",
|
||||
primary_key:
|
||||
"If true, a default `id` uuid primary key is used. If false, none is created. See the primary_key opts for info on specifying primary key options."
|
||||
],
|
||||
required: [:name, :type],
|
||||
defaults: [
|
||||
primary_key: true
|
||||
],
|
||||
constraints: [
|
||||
max_page_size:
|
||||
{&Ash.Constraints.greater_than_zero?/1, "must be greater than zero"},
|
||||
default_page_size:
|
||||
{&Ash.Constraints.greater_than_zero?/1, "must be greater than zero"}
|
||||
]
|
||||
)
|
||||
|
||||
@moduledoc """
|
||||
The entry point for creating an `Ash.Resource`.
|
||||
|
||||
This brings in the top level DSL macros, defines module attributes for aggregating state as
|
||||
DSL functions are called, and defines a set of functions internal to the resource that can be
|
||||
used to inspect them.
|
||||
|
||||
Simply add `use Ash.Resource, ...` at the top of your resource module, and refer to the DSL documentation
|
||||
at `Ash.Resource.DSL` for the rest. The options for `use Ash.Resource` are described below.
|
||||
|
||||
#{Ashton.document(@resource_opts_schema)}
|
||||
|
||||
Note:
|
||||
*Do not* call the functions on a resource, as in `MyResource.type()` as this is a *private*
|
||||
API and can change at any time. Instead, use the `Ash` module, for example: `Ash.type(MyResource)`
|
||||
"""
|
||||
|
||||
defmacro __using__(opts) do
|
||||
quote do
|
||||
@before_compile Ash.Resource
|
||||
@skip_data_layer unquote(Keyword.get(opts, :no_data_layer, false))
|
||||
|
||||
Module.register_attribute(__MODULE__, :before_compile_hooks, accumulate: true)
|
||||
opts = Ash.Resource.validate_use_opts(__MODULE__, unquote(opts))
|
||||
Ash.Resource.define_resource_module_attributes(__MODULE__, opts)
|
||||
Ash.Resource.define_primary_key(__MODULE__, opts)
|
||||
|
||||
Module.register_attribute(__MODULE__, :actions, accumulate: true)
|
||||
Module.register_attribute(__MODULE__, :attributes, accumulate: true)
|
||||
Module.register_attribute(__MODULE__, :relationships, accumulate: true)
|
||||
Module.register_attribute(__MODULE__, :mix_ins, accumulate: true)
|
||||
use Ash.Resource.DSL
|
||||
end
|
||||
end
|
||||
|
||||
if unquote(Keyword.get(opts, :primary_key?, true)) do
|
||||
@attributes Ash.Resource.Attributes.Attribute.new(__MODULE__, :id, :uuid,
|
||||
primary_key?: true
|
||||
)
|
||||
end
|
||||
@doc false
|
||||
def define_resource_module_attributes(mod, opts) do
|
||||
Module.register_attribute(mod, :before_compile_hooks, accumulate: true)
|
||||
Module.register_attribute(mod, :actions, accumulate: true)
|
||||
Module.register_attribute(mod, :attributes, accumulate: true)
|
||||
Module.register_attribute(mod, :relationships, accumulate: true)
|
||||
Module.register_attribute(mod, :mix_ins, accumulate: true)
|
||||
|
||||
# Module.put_attribute(__MODULE__, :custom_threshold_for_lib, 10)
|
||||
import Ash.Resource
|
||||
import Ash.Resource.Actions, only: [actions: 1]
|
||||
import Ash.Resource.Attributes, only: [attributes: 1]
|
||||
import Ash.Resource.Relationships, only: [relationships: 1]
|
||||
import Ash.Authorization.Rule
|
||||
Module.put_attribute(mod, :name, opts[:name])
|
||||
Module.put_attribute(mod, :resource_type, opts[:type])
|
||||
Module.put_attribute(mod, :max_page_size, opts[:max_page_size])
|
||||
Module.put_attribute(mod, :default_page_size, opts[:default_page_size])
|
||||
end
|
||||
|
||||
name = unquote(opts[:name])
|
||||
resource_type = unquote(opts[:type])
|
||||
@doc false
|
||||
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)
|
||||
Module.put_attribute(mod, :attributes, attribute)
|
||||
|
||||
@name name
|
||||
@resource_type resource_type
|
||||
@max_page_size nil
|
||||
@default_page_size nil
|
||||
false ->
|
||||
:ok
|
||||
|
||||
unless @name do
|
||||
raise "Must set name"
|
||||
end
|
||||
opts ->
|
||||
attribute =
|
||||
Ash.Resource.Attributes.Attribute.new(mod, opts[:field], opts[:type], primary_key?: true)
|
||||
|
||||
unless @resource_type do
|
||||
raise "Must set resource type"
|
||||
end
|
||||
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, option: key, message: message
|
||||
|
||||
{:ok, opts} ->
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -91,16 +164,8 @@ defmodule Ash.Resource do
|
|||
@default_page_size
|
||||
end
|
||||
|
||||
unless @skip_data_layer || @data_layer do
|
||||
raise "Must `use` a data layer module or pass `no_data_layer: true`"
|
||||
end
|
||||
|
||||
def data_layer() do
|
||||
if @skip_data_layer do
|
||||
false
|
||||
else
|
||||
@data_layer
|
||||
end
|
||||
@data_layer || false
|
||||
end
|
||||
|
||||
Enum.map(@mix_ins || [], fn hook_module ->
|
||||
|
@ -110,18 +175,6 @@ defmodule Ash.Resource do
|
|||
end
|
||||
end
|
||||
|
||||
defmacro max_page_size(page_size) do
|
||||
quote do
|
||||
@max_page_size unquote(page_size)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro default_page_size(page_size) do
|
||||
quote do
|
||||
@default_page_size unquote(page_size)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def primary_key(attributes) do
|
||||
attributes
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
defmodule Ash.Resource.Attributes.Attribute do
|
||||
@moduledoc """
|
||||
The struct containing information about an attribute.
|
||||
See the DSL documentation for more information on their usage in the DSL
|
||||
"""
|
||||
|
||||
defstruct [:name, :type, :primary_key?]
|
||||
|
||||
@builtins Ash.Type.builtins()
|
||||
@type t :: %__MODULE__{
|
||||
name: atom(),
|
||||
type: Ash.type(),
|
||||
primary_key?: boolean()
|
||||
}
|
||||
|
||||
@option_schema Ashton.schema(opts: [primary_key?: :boolean])
|
||||
@builtins Ash.Type.builtins()
|
||||
@schema Ashton.schema(opts: [primary_key?: :boolean], defaults: [primary_key?: false])
|
||||
|
||||
@doc false
|
||||
def attribute_schema(), do: @option_schema
|
||||
def attribute_schema(), do: @schema
|
||||
|
||||
def new(resource, name, type, opts \\ [])
|
||||
|
||||
|
@ -25,7 +35,7 @@ defmodule Ash.Resource.Attributes.Attribute do
|
|||
end
|
||||
|
||||
def new(resource, name, type, opts) when type in @builtins do
|
||||
case Ashton.validate(opts, @option_schema) do
|
||||
case Ashton.validate(opts, @schema) do
|
||||
{:error, [{key, message} | _]} ->
|
||||
raise Ash.Error.ResourceDslError,
|
||||
resource: resource,
|
||||
|
|
19
lib/ash/resource/dsl.ex
Normal file
19
lib/ash/resource/dsl.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Ash.Resource.DSL do
|
||||
@moduledoc """
|
||||
The entrypoint for the Ash DSL documentation and interface.
|
||||
|
||||
Available DSL sections:
|
||||
|
||||
* `actions` - `Ash.Resource.Actions`
|
||||
* `attributes` - `Ash.Resource.Attributes`
|
||||
* `relationships` - `Ash.Resource.Relationships`
|
||||
"""
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
import Ash.Resource.Actions, only: [actions: 1]
|
||||
import Ash.Resource.Attributes, only: [attributes: 1]
|
||||
import Ash.Resource.Relationships, only: [relationships: 1]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
defmodule Ash.Test do
|
||||
end
|
2
mix.exs
2
mix.exs
|
@ -43,7 +43,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.6"}
|
||||
{:ashton, "~> 0.3.9"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -1,5 +1,5 @@
|
|||
%{
|
||||
"ashton": {:hex, :ashton, "0.3.6", "95f5d598c2e05662498349d81e7579e897b1e1cfe1aa79606d07a35305a47efe", [:mix], [], "hexpm"},
|
||||
"ashton": {:hex, :ashton, "0.3.7", "9349c196fb4302a08d5e19748f1af74bbe891b15c9a2ec11184f69ec1912f478", [: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,6 +1,5 @@
|
|||
defmodule Ash.Test.Actions.ReadTest do
|
||||
use ExUnit.Case, async: true
|
||||
# import Ash.Test
|
||||
|
||||
defmodule Post do
|
||||
use Ash.Resource, name: "posts", type: "post"
|
||||
|
|
0
test/dsl/resource/top_level_test.exs
Normal file
0
test/dsl/resource/top_level_test.exs
Normal file
Loading…
Reference in a new issue