WIP on cleanup/documentation

This commit is contained in:
Zach Daniel 2019-12-05 02:18:13 -05:00
parent df70095e39
commit 030c389225
No known key found for this signature in database
GPG key ID: A57053A671EE649E
13 changed files with 174 additions and 67 deletions

View file

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

View file

@ -1,8 +0,0 @@
use Mix.Config
if Mix.env() == :test do
config :ash,
resources: [
Ash.Test.Post
]
end

View file

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

@ -0,0 +1,4 @@
defmodule Ash.Constraints do
def positive?(integer), do: integer >= 0
def greater_than_zero?(integer), do: integer > 0
end

View file

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

View file

@ -1,45 +1,118 @@
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)
if unquote(Keyword.get(opts, :primary_key?, true)) do
@attributes Ash.Resource.Attributes.Attribute.new(__MODULE__, :id, :uuid,
primary_key?: true
)
use Ash.Resource.DSL
end
end
# 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
@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)
name = unquote(opts[:name])
resource_type = unquote(opts[:type])
@name name
@resource_type resource_type
@max_page_size nil
@default_page_size nil
unless @name do
raise "Must set name"
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
unless @resource_type do
raise "Must set resource 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)
false ->
:ok
opts ->
attribute =
Ash.Resource.Attributes.Attribute.new(mod, 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, option: key, message: message
{:ok, opts} ->
opts
end
end
defmacro __before_compile__(env) do
@ -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

View file

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

View file

@ -1,2 +0,0 @@
defmodule Ash.Test do
end

View file

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

View file

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

View file

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

View file