mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 21:13:10 +12:00
add an ets data layer, and start on some testing
This commit is contained in:
parent
e3cc1f9b3a
commit
4cabb8a838
14 changed files with 497 additions and 123 deletions
|
@ -24,3 +24,4 @@
|
|||
* Since actions contain rules now, consider making it possible to list each action as its own `do` block, with an internal DSL for configuring the action. (overkill?)
|
||||
* Validate rules at creation
|
||||
* Maybe fix the crappy parts of optimal and bring it in for opts validation?
|
||||
* The ecto internals that live on structs are going to cause problems w/ pluggability of backends, like the `%Ecto.Association.NotLoaded{}`. That backend may need to scrub the ecto specifics off of those structs.
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
use Mix.Config
|
||||
|
||||
if Mix.env() == :test do
|
||||
config :ash,
|
||||
resources: [
|
||||
Ash.Test.Post
|
||||
]
|
||||
end
|
||||
|
|
19
lib/ash.ex
19
lib/ash.ex
|
@ -89,8 +89,23 @@ defmodule Ash do
|
|||
end
|
||||
|
||||
def read(resource, params \\ %{}) do
|
||||
action = Map.get(params, :action) || primary_action(resource, :read)
|
||||
Ash.DataLayer.Actions.run_read_action(resource, action, params)
|
||||
case Map.get(params, :action) || primary_action(resource, :read) do
|
||||
nil ->
|
||||
{:error, "no action provided, and no primary action found"}
|
||||
|
||||
action ->
|
||||
Ash.DataLayer.Actions.run_read_action(resource, action, params)
|
||||
end
|
||||
end
|
||||
|
||||
def create(resource, params) do
|
||||
case Map.get(params, :action) || primary_action(resource, :create) do
|
||||
nil ->
|
||||
{:error, "no action provided, and no primary action found"}
|
||||
|
||||
action ->
|
||||
Ash.DataLayer.Actions.run_create_action(resource, action, params)
|
||||
end
|
||||
end
|
||||
|
||||
# # TODO: auth
|
||||
|
|
109
lib/ash/data.ex
109
lib/ash/data.ex
|
@ -1,109 +0,0 @@
|
|||
defmodule Ash.Data do
|
||||
# @spec create(Ash.resource(), Ash.action(), Ash.attributes(), Ash.relationships(), Ash.params()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def create(resource, action, attributes, relationships, params) do
|
||||
# Ash.data_layer(resource).create(resource, action, attributes, relationships, params)
|
||||
# end
|
||||
|
||||
# @spec update(Ash.record(), Ash.action(), Ash.attributes(), Ash.relationships(), Ash.params()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def update(%resource{} = record, action, attributes, relationships, params) do
|
||||
# Ash.data_layer(resource).update(record, action, attributes, relationships, params)
|
||||
# end
|
||||
|
||||
# @spec delete(Ash.record(), Ash.action(), Ash.params()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def delete(%resource{} = record, action, params) do
|
||||
# Ash.data_layer(resource).delete(record, action, params)
|
||||
# end
|
||||
|
||||
# @spec append_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def append_related(%resource{} = record, relationship, resource_identifiers) do
|
||||
# Ash.data_layer(resource).append_related(record, relationship, resource_identifiers)
|
||||
# end
|
||||
|
||||
# @spec delete_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def delete_related(%resource{} = record, relationship, resource_identifiers) do
|
||||
# Ash.data_layer(resource).delete_related(record, relationship, resource_identifiers)
|
||||
# end
|
||||
|
||||
# @spec replace_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def replace_related(%resource{} = record, relationship, resource_identifiers) do
|
||||
# Ash.data_layer(resource).replace_related(record, relationship, resource_identifiers)
|
||||
# end
|
||||
|
||||
@spec resource_to_query(Ash.resource()) :: Ash.query()
|
||||
def resource_to_query(resource) do
|
||||
Ash.data_layer(resource).resource_to_query(resource)
|
||||
end
|
||||
|
||||
@spec filter(Ash.resource(), Ash.query(), Ash.params()) ::
|
||||
{:ok, Ash.query()} | {:error, Ash.error()}
|
||||
# TODO This is a really dumb implementation of this.
|
||||
def filter(resource, query, params) do
|
||||
data_layer = Ash.data_layer(resource)
|
||||
|
||||
filtered_query =
|
||||
params
|
||||
|> Map.get(:filter, %{})
|
||||
|> Enum.reduce(query, fn {key, value}, query ->
|
||||
case query do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
query ->
|
||||
case data_layer.filter(query, key, value, resource) do
|
||||
{:ok, query} -> query
|
||||
{:error, query} -> {:error, query}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, filtered_query}
|
||||
end
|
||||
|
||||
@spec limit(Ash.query(), limit :: non_neg_integer, Ash.resource()) ::
|
||||
{:ok, Ash.query()} | {:error, Ash.error()}
|
||||
def limit(query, limit, resource) do
|
||||
data_layer = Ash.data_layer(resource)
|
||||
data_layer.limit(query, limit, resource)
|
||||
end
|
||||
|
||||
@spec offset(Ash.query(), offset :: non_neg_integer, Ash.resource()) ::
|
||||
{:ok, Ash.query()} | {:error, Ash.error()}
|
||||
def offset(query, offset, resource) do
|
||||
data_layer = Ash.data_layer(resource)
|
||||
data_layer.limit(query, offset, resource)
|
||||
end
|
||||
|
||||
# @spec get_related(Ash.record(), Ash.relationship()) ::
|
||||
# {:ok, list(Ash.record()) | Ash.record() | nil} | {:error, Ash.error()}
|
||||
# def get_related(record, %{cardinality: :many} = relationship) do
|
||||
# case relationship_query(record, relationship) do
|
||||
# {:ok, query} ->
|
||||
# get_many(query, Ash.to_resource(record))
|
||||
|
||||
# {:error, error} ->
|
||||
# {:error, error}
|
||||
# end
|
||||
# end
|
||||
|
||||
# def get_related(record, %{cardinality: :one} = relationship) do
|
||||
# case relationship_query(record, relationship) do
|
||||
# {:ok, query} ->
|
||||
# get_one(query, Ash.to_resource(record))
|
||||
|
||||
# {:error, error} ->
|
||||
# {:error, error}
|
||||
# end
|
||||
# end
|
||||
|
||||
@spec run_query(Ash.query(), central_resource :: Ash.resource()) ::
|
||||
{:ok, list(Ash.record())} | {:error, Ash.error()}
|
||||
def run_query(query, central_resource) do
|
||||
Ash.data_layer(central_resource).run_query(query, central_resource)
|
||||
end
|
||||
end
|
|
@ -36,11 +36,11 @@ defmodule Ash.DataLayer.Actions do
|
|||
with {%{prediction: prediction} = instructions, per_check_data}
|
||||
when prediction != :unauthorized <-
|
||||
maybe_authorize_precheck(auth?, user, action.rules, auth_context),
|
||||
query <- Ash.Data.resource_to_query(resource),
|
||||
{:ok, filtered_query} <- Ash.Data.filter(resource, query, params),
|
||||
query <- Ash.DataLayer.resource_to_query(resource),
|
||||
{:ok, filtered_query} <- Ash.DataLayer.filter(resource, query, params),
|
||||
{:ok, paginator} <-
|
||||
Ash.DataLayer.Paginator.paginate(resource, action, filtered_query, params),
|
||||
{:ok, found} <- Ash.Data.run_query(paginator.query, resource),
|
||||
{:ok, found} <- Ash.DataLayer.run_query(paginator.query, resource),
|
||||
{:ok, side_loaded_for_auth} <-
|
||||
Ash.DataLayer.SideLoader.side_load(
|
||||
resource,
|
||||
|
@ -79,6 +79,109 @@ defmodule Ash.DataLayer.Actions do
|
|||
end
|
||||
end
|
||||
|
||||
def run_create_action(resource, action, params) do
|
||||
auth_context = %{
|
||||
resource: resource,
|
||||
action: action,
|
||||
params: params
|
||||
}
|
||||
|
||||
user = Map.get(params, :user)
|
||||
auth? = Map.get(params, :authorize?, false)
|
||||
|
||||
# TODO: no instrutions relevant to creates right now?
|
||||
with {:ok, attributes, relationships} <- prepare_create_params(resource, params),
|
||||
{%{prediction: prediction}, per_check_data}
|
||||
when prediction != :unauthorized <-
|
||||
maybe_authorize_precheck(auth?, user, action.rules, auth_context),
|
||||
{:ok, created} <-
|
||||
Ash.DataLayer.create(resource, attributes, relationships),
|
||||
:allow <-
|
||||
maybe_authorize(
|
||||
auth?,
|
||||
user,
|
||||
created,
|
||||
action.rules,
|
||||
auth_context,
|
||||
per_check_data
|
||||
),
|
||||
{:ok, side_loaded} <-
|
||||
Ash.DataLayer.SideLoader.side_load(
|
||||
resource,
|
||||
created,
|
||||
Map.get(params, :side_load, []),
|
||||
Map.take(params, [:authorize?, :user])
|
||||
) do
|
||||
{:ok, side_loaded}
|
||||
else
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
{%{prediction: :unauthorized}, _} ->
|
||||
# TODO: Nice errors here!
|
||||
{:error, :unauthorized}
|
||||
|
||||
{:unauthorized, _data} ->
|
||||
# TODO: Nice errors here!
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_create_params(resource, params) do
|
||||
attributes = Map.get(params, :attributes, %{})
|
||||
relationships = Map.get(params, :relationships, %{})
|
||||
|
||||
with {:ok, attributes} <- prepare_create_attributes(resource, attributes),
|
||||
{:ok, relationships} <- prepare_create_relationships(resource, relationships) do
|
||||
{:ok, attributes, relationships}
|
||||
else
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_create_attributes(resource, attributes) do
|
||||
# resource_attributes = Ash.attributes(resource)
|
||||
|
||||
attributes
|
||||
# Eventually we'll have to just copy changeset's logic
|
||||
# and/or use it directly (now that ecto is split up, maybe thats the way to do all of this?)
|
||||
|> Enum.reduce({%{}, []}, fn {key, value}, {changes, errors} ->
|
||||
case Ash.attribute(resource, key) do
|
||||
nil ->
|
||||
{changes, ["unknown attribute #{key}" | errors]}
|
||||
|
||||
_attribute ->
|
||||
# TODO do actual value validation here
|
||||
{Map.put(changes, key, value), errors}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{changes, []} -> {:ok, changes}
|
||||
{_, errors} -> {:error, errors}
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_create_relationships(resource, relationships) do
|
||||
relationships
|
||||
# Eventually we'll have to just copy changeset's logic
|
||||
# and/or use it directly (now that ecto is split up, maybe thats the way to do all of this?)
|
||||
|> Enum.reduce({%{}, []}, fn {key, value}, {changes, errors} ->
|
||||
case Ash.attribute(resource, key) do
|
||||
nil ->
|
||||
{changes, ["unknown attribute #{key}" | errors]}
|
||||
|
||||
_attribute ->
|
||||
# TODO do actual value validation here
|
||||
{Map.put(changes, key, value), errors}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{changes, []} -> {:ok, changes}
|
||||
{_, errors} -> {:error, errors}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_authorize(false, _, _, _, _, _), do: :allow
|
||||
|
||||
defp maybe_authorize(true, user, data, rules, auth_context, per_check_data) do
|
||||
|
|
|
@ -6,11 +6,11 @@ defmodule Ash.DataLayer do
|
|||
@callback offset(Ash.query(), offset :: non_neg_integer(), resource :: Ash.resource()) ::
|
||||
{:ok, Ash.query()} | {:error, Ash.error()}
|
||||
@callback resource_to_query(Ash.resource()) :: Ash.query()
|
||||
# @callback relationship_query(Ash.record() | list(Ash.record()), Ash.relationship()) ::
|
||||
# Ash.query()
|
||||
@callback can_query_async?(Ash.resource()) :: boolean
|
||||
@callback run_query(Ash.query(), Ash.resource()) ::
|
||||
{:ok, list(Ash.resource())} | {:error, Ash.error()}
|
||||
@callback create(Ash.resource(), attributes :: map(), relationships :: map()) ::
|
||||
{:ok, Ash.resource()} | {:error, Ash.error()}
|
||||
|
||||
# @callback create(
|
||||
# Ash.resource(),
|
||||
|
@ -41,4 +41,117 @@ defmodule Ash.DataLayer do
|
|||
|
||||
# @callback replace_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
|
||||
# @spec create(Ash.resource(), Ash.action(), Ash.attributes(), Ash.relationships(), Ash.params()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def create(resource, action, attributes, relationships, params) do
|
||||
# Ash.data_layer(resource).create(resource, action, attributes, relationships, params)
|
||||
# end
|
||||
|
||||
# @spec update(Ash.record(), Ash.action(), Ash.attributes(), Ash.relationships(), Ash.params()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def update(%resource{} = record, action, attributes, relationships, params) do
|
||||
# Ash.data_layer(resource).update(record, action, attributes, relationships, params)
|
||||
# end
|
||||
|
||||
# @spec delete(Ash.record(), Ash.action(), Ash.params()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def delete(%resource{} = record, action, params) do
|
||||
# Ash.data_layer(resource).delete(record, action, params)
|
||||
# end
|
||||
|
||||
# @spec append_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def append_related(%resource{} = record, relationship, resource_identifiers) do
|
||||
# Ash.data_layer(resource).append_related(record, relationship, resource_identifiers)
|
||||
# end
|
||||
|
||||
# @spec delete_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def delete_related(%resource{} = record, relationship, resource_identifiers) do
|
||||
# Ash.data_layer(resource).delete_related(record, relationship, resource_identifiers)
|
||||
# end
|
||||
|
||||
# @spec replace_related(Ash.record(), Ash.relationship(), Ash.resource_identifiers()) ::
|
||||
# {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
# def replace_related(%resource{} = record, relationship, resource_identifiers) do
|
||||
# Ash.data_layer(resource).replace_related(record, relationship, resource_identifiers)
|
||||
# end
|
||||
|
||||
@spec resource_to_query(Ash.resource()) :: Ash.query()
|
||||
def resource_to_query(resource) do
|
||||
Ash.data_layer(resource).resource_to_query(resource)
|
||||
end
|
||||
|
||||
@spec create(Ash.resource(), map, map) :: {:ok, Ash.record()} | {:error, Ash.error()}
|
||||
def create(resource, attributes, relationships) do
|
||||
Ash.data_layer(resource).create(resource, attributes, relationships)
|
||||
end
|
||||
|
||||
@spec filter(Ash.resource(), Ash.query(), Ash.params()) ::
|
||||
{:ok, Ash.query()} | {:error, Ash.error()}
|
||||
# TODO This is a really dumb implementation of this.
|
||||
def filter(resource, query, params) do
|
||||
data_layer = Ash.data_layer(resource)
|
||||
|
||||
filtered_query =
|
||||
params
|
||||
|> Map.get(:filter, %{})
|
||||
|> Enum.reduce(query, fn {key, value}, query ->
|
||||
case query do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
query ->
|
||||
case data_layer.filter(query, key, value, resource) do
|
||||
{:ok, query} -> query
|
||||
{:error, query} -> {:error, query}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, filtered_query}
|
||||
end
|
||||
|
||||
@spec limit(Ash.query(), limit :: non_neg_integer, Ash.resource()) ::
|
||||
{:ok, Ash.query()} | {:error, Ash.error()}
|
||||
def limit(query, limit, resource) do
|
||||
data_layer = Ash.data_layer(resource)
|
||||
data_layer.limit(query, limit, resource)
|
||||
end
|
||||
|
||||
@spec offset(Ash.query(), offset :: non_neg_integer, Ash.resource()) ::
|
||||
{:ok, Ash.query()} | {:error, Ash.error()}
|
||||
def offset(query, offset, resource) do
|
||||
data_layer = Ash.data_layer(resource)
|
||||
data_layer.limit(query, offset, resource)
|
||||
end
|
||||
|
||||
# @spec get_related(Ash.record(), Ash.relationship()) ::
|
||||
# {:ok, list(Ash.record()) | Ash.record() | nil} | {:error, Ash.error()}
|
||||
# def get_related(record, %{cardinality: :many} = relationship) do
|
||||
# case relationship_query(record, relationship) do
|
||||
# {:ok, query} ->
|
||||
# get_many(query, Ash.to_resource(record))
|
||||
|
||||
# {:error, error} ->
|
||||
# {:error, error}
|
||||
# end
|
||||
# end
|
||||
|
||||
# def get_related(record, %{cardinality: :one} = relationship) do
|
||||
# case relationship_query(record, relationship) do
|
||||
# {:ok, query} ->
|
||||
# get_one(query, Ash.to_resource(record))
|
||||
|
||||
# {:error, error} ->
|
||||
# {:error, error}
|
||||
# end
|
||||
# end
|
||||
|
||||
@spec run_query(Ash.query(), central_resource :: Ash.resource()) ::
|
||||
{:ok, list(Ash.record())} | {:error, Ash.error()}
|
||||
def run_query(query, central_resource) do
|
||||
Ash.data_layer(central_resource).run_query(query, central_resource)
|
||||
end
|
||||
end
|
||||
|
|
112
lib/ash/data_layer/ets.ex
Normal file
112
lib/ash/data_layer/ets.ex
Normal file
|
@ -0,0 +1,112 @@
|
|||
defmodule Ash.DataLayer.Ets do
|
||||
@moduledoc """
|
||||
An ETS backed Ash Datalayer. Should only be used for testing, or for
|
||||
unimportant/small datasets.
|
||||
"""
|
||||
|
||||
defmacro __using__(opts) do
|
||||
quote bind_quoted: [opts: opts] do
|
||||
@data_layer Ash.DataLayer.Ets
|
||||
@mix_ins Ash.DataLayer.Ets
|
||||
|
||||
@ets_private? Keyword.get(opts, :private?, false)
|
||||
|
||||
def ets_private?() do
|
||||
@ets_private?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def before_compile_hook(_env) do
|
||||
quote do
|
||||
struct_fields =
|
||||
@attributes
|
||||
|> Enum.map(fn attr ->
|
||||
{attr.name, nil}
|
||||
end)
|
||||
|> Enum.concat(Enum.map(@relationships, fn rel -> {rel.name, :not_loaded} end))
|
||||
|
||||
defstruct struct_fields
|
||||
end
|
||||
end
|
||||
|
||||
def private?(resource) do
|
||||
resource.ets_private?()
|
||||
end
|
||||
|
||||
defmodule Query do
|
||||
defstruct [:resource, :limit, match_spec: {:_, :"$2"}, offset: 0]
|
||||
end
|
||||
|
||||
def resource_to_query(resource), do: %Query{resource: resource}
|
||||
def limit(query, limit, _), do: {:ok, %Query{query | limit: limit}}
|
||||
def offset(query, offset, _), do: {:ok, %{query | offset: offset}}
|
||||
def can_query_async?(_), do: false
|
||||
|
||||
def filter(query, :id, id, _resource) do
|
||||
{:ok, %{query | match_spec: {id, :"$2"}}}
|
||||
end
|
||||
|
||||
def filter(_query, _field, _value, _resource) do
|
||||
{:error, "filter not supported for anything other than id"}
|
||||
end
|
||||
|
||||
def run_query(
|
||||
%Query{resource: resource, match_spec: match_spec, offset: offset, limit: limit},
|
||||
_
|
||||
) do
|
||||
with {:ok, table} <- wrap_or_create_table(resource),
|
||||
{:ok, {results, _}} <- match_limit(table, match_spec, limit, offset) do
|
||||
ret = results |> Enum.drop(offset) |> Enum.map(&List.first/1)
|
||||
|
||||
{:ok, ret}
|
||||
end
|
||||
end
|
||||
|
||||
def create(_resource, _attributes, relationships) when relationships != %{} do
|
||||
{:error, "#{inspect(__MODULE__)} does not support creating with relationships"}
|
||||
end
|
||||
|
||||
def create(resource, attributes, _relationships) do
|
||||
with {:ok, table} <- wrap_or_create_table(resource),
|
||||
attrs <- Map.put_new_lazy(attributes, :id, &Ash.UUID.generate/0),
|
||||
record <- struct(resource, attrs),
|
||||
{:ok, _} <- Ets.Set.put(table, {attrs.id, record}) do
|
||||
{:ok, record}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp match_limit(table, match_spec, limit, offset) do
|
||||
# TODO: Fix this
|
||||
# This is a hack :(
|
||||
# Either implement cursor based pagination
|
||||
# or find a way to skip in ETS
|
||||
if limit do
|
||||
Ets.Set.match(table, match_spec, limit + offset)
|
||||
else
|
||||
Ets.Set.match(table, match_spec)
|
||||
end
|
||||
end
|
||||
|
||||
defp wrap_or_create_table(resource) do
|
||||
case Ets.Set.wrap_existing(resource) do
|
||||
{:error, :table_not_found} ->
|
||||
protection =
|
||||
if private?(resource) do
|
||||
:private
|
||||
else
|
||||
:public
|
||||
end
|
||||
|
||||
Ets.Set.new(name: resource, protection: protection, ordered: true, read_concurrency: true)
|
||||
|
||||
{:ok, table} ->
|
||||
{:ok, table}
|
||||
|
||||
{:error, other} ->
|
||||
{:error, other}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,8 +26,8 @@ defmodule Ash.DataLayer.Paginator do
|
|||
|
||||
def paginate(resource, _action, query, params) do
|
||||
with %__MODULE__{limit: limit, offset: offset} = paginator <- paginator(params),
|
||||
{:ok, query} <- Ash.Data.offset(query, offset, resource),
|
||||
{:ok, query} <- Ash.Data.limit(query, limit, resource) do
|
||||
{:ok, query} <- Ash.DataLayer.offset(query, offset, resource),
|
||||
{:ok, query} <- Ash.DataLayer.limit(query, limit, resource) do
|
||||
{:ok, %{paginator | query: query}}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
|
|
64
lib/ash/uuid.ex
Normal file
64
lib/ash/uuid.ex
Normal file
|
@ -0,0 +1,64 @@
|
|||
defmodule Ash.UUID do
|
||||
@moduledoc "UUID behaviour ripped directly from ecto."
|
||||
|
||||
@typedoc """
|
||||
A hex-encoded UUID string.
|
||||
"""
|
||||
@type t :: <<_::288>>
|
||||
|
||||
@typedoc """
|
||||
A raw binary representation of a UUID.
|
||||
"""
|
||||
@type raw :: <<_::128>>
|
||||
|
||||
@doc """
|
||||
Generates a version 4 (random) UUID.
|
||||
"""
|
||||
@spec generate() :: t
|
||||
def generate() do
|
||||
{:ok, uuid} = encode(bingenerate())
|
||||
uuid
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a version 4 (random) UUID in the binary format.
|
||||
"""
|
||||
@spec bingenerate() :: raw
|
||||
def bingenerate() do
|
||||
<<u0::48, _::4, u1::12, _::2, u2::62>> = :crypto.strong_rand_bytes(16)
|
||||
<<u0::48, 4::4, u1::12, 2::2, u2::62>>
|
||||
end
|
||||
|
||||
defp encode(
|
||||
<<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4,
|
||||
c1::4, c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4,
|
||||
e5::4, e6::4, e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
|
||||
) do
|
||||
<<e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-, e(b1), e(b2), e(b3), e(b4), ?-,
|
||||
e(c1), e(c2), e(c3), e(c4), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3), e(e4),
|
||||
e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
|
||||
catch
|
||||
:error -> :error
|
||||
else
|
||||
encoded -> {:ok, encoded}
|
||||
end
|
||||
|
||||
@compile {:inline, e: 1}
|
||||
|
||||
defp e(0), do: ?0
|
||||
defp e(1), do: ?1
|
||||
defp e(2), do: ?2
|
||||
defp e(3), do: ?3
|
||||
defp e(4), do: ?4
|
||||
defp e(5), do: ?5
|
||||
defp e(6), do: ?6
|
||||
defp e(7), do: ?7
|
||||
defp e(8), do: ?8
|
||||
defp e(9), do: ?9
|
||||
defp e(10), do: ?a
|
||||
defp e(11), do: ?b
|
||||
defp e(12), do: ?c
|
||||
defp e(13), do: ?d
|
||||
defp e(14), do: ?e
|
||||
defp e(15), do: ?f
|
||||
end
|
11
mix.exs
11
mix.exs
|
@ -7,12 +7,21 @@ defmodule Ash.MixProject do
|
|||
version: "0.1.0",
|
||||
elixir: "~> 1.9",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
defp elixirc_paths(:test) do
|
||||
["lib", "test/support"]
|
||||
end
|
||||
|
||||
defp elixirc_paths(_), do: ["/lib"]
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[]
|
||||
[
|
||||
{:ets, "~> 0.7.3", only: [:dev, :test]}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -3,6 +3,7 @@
|
|||
"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"},
|
||||
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
|
||||
"ets": {:hex, :ets, "0.7.3", "60862855af5ae89bb631c787ab9ba946509d59fa632442ef33947b18ac288101", [:mix], [], "hexpm"},
|
||||
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
|
||||
"plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
|
|
49
test/actions/read_test.exs
Normal file
49
test/actions/read_test.exs
Normal file
|
@ -0,0 +1,49 @@
|
|||
defmodule Ash.Test.Actions.Read do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Ash.Test.Post
|
||||
|
||||
describe "Ash.get/3" do
|
||||
setup do
|
||||
{:ok, post} = Ash.create(Post, %{attributes: %{title: "test", contents: "yeet"}})
|
||||
%{post: post}
|
||||
end
|
||||
|
||||
test "it returns a matching record", %{post: post} do
|
||||
assert {:ok, fetched_post} = Ash.get(Post, post.id)
|
||||
|
||||
assert fetched_post == post
|
||||
end
|
||||
|
||||
test "it returns nil when there is no matching record" do
|
||||
assert {:ok, nil} = Ash.get(Post, Ash.UUID.generate())
|
||||
end
|
||||
end
|
||||
|
||||
describe "Ash.read/2 with no records" do
|
||||
test "returns an empty result" do
|
||||
assert {:ok, %{results: []}} = Ash.read(Post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Ash.read/2" do
|
||||
setup do
|
||||
{:ok, post1} = Ash.create(Post, %{attributes: %{title: "test", contents: "yeet"}})
|
||||
{:ok, post2} = Ash.create(Post, %{attributes: %{title: "test1", contents: "yeet2"}})
|
||||
|
||||
%{post1: post1, post2: post2}
|
||||
end
|
||||
|
||||
test "with page size of 1, returns only 1 record" do
|
||||
assert {:ok, %{results: [_post]}} = Ash.read(Post, %{page: %{limit: 1}})
|
||||
end
|
||||
|
||||
test "with page size of 2, returns 2 records" do
|
||||
assert {:ok, %{results: [_, _]}} = Ash.read(Post, %{page: %{limit: 2}})
|
||||
end
|
||||
|
||||
test "with page size of 1 and an offset of 1, it returns 1 record" do
|
||||
assert {:ok, %{results: [_]}} = Ash.read(Post, %{page: %{limit: 1, offset: 1}})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,8 +1,4 @@
|
|||
defmodule AshTest do
|
||||
use ExUnit.Case
|
||||
doctest Ash
|
||||
|
||||
test "greets the world" do
|
||||
assert Ash.hello() == :world
|
||||
end
|
||||
end
|
||||
|
|
13
test/support/post.ex
Normal file
13
test/support/post.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Ash.Test.Post do
|
||||
use Ash.Resource, name: "posts", type: "post"
|
||||
use Ash.DataLayer.Ets, private: true
|
||||
|
||||
actions do
|
||||
defaults [:read, :create]
|
||||
end
|
||||
|
||||
attributes do
|
||||
attribute :title, :string
|
||||
attribute :contents, :string
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue