Skip to content

Commit

Permalink
feat: embedded resources
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Jan 12, 2021
1 parent db6bdfc commit d888fcc
Show file tree
Hide file tree
Showing 42 changed files with 1,610 additions and 218 deletions.
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ locals_without_parens = [
uuid_primary_key: 2,
validate: 1,
validate: 2,
warn_on_compile_failure?: 1,
writable?: 1
]

Expand Down
6 changes: 6 additions & 0 deletions lib/ash/actions/sort.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ defmodule Ash.Actions.Sort do
!attribute ->
{sorts, [NoSuchAttribute.exception(attribute: field) | errors]}

Ash.Type.embedded_type?(attribute.type) ->
{sorts, ["Cannot sort on embedded types" | errors]}

match?({:array, _}, attribute.type) ->
{sorts, ["Cannot sort on array types" | errors]}

!Ash.Resource.data_layer_can?(resource, {:sort, Ash.Type.storage_type(attribute.type)}) ->
{sorts,
[
Expand Down
7 changes: 7 additions & 0 deletions lib/ash/api/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ defmodule Ash.Api.Dsl do
examples: [
"resource MyApp.User"
],
# This is an internal tool used by embedded resources,
# so we hide it from the documentation
hide: [:warn_on_compile_failure?],
schema: [
warn_on_compile_failure?: [
type: :atom,
default: true
],
resource: [
type: :atom,
required: true,
Expand Down
2 changes: 1 addition & 1 deletion lib/ash/api/resource_reference.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Ash.Api.ResourceReference do
@moduledoc "Represents a resource in an API"
defstruct [:resource]
defstruct [:resource, warn_on_compile_failure?: true]
end
1 change: 1 addition & 0 deletions lib/ash/api/transformers/ensure_resources_compiled.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule Ash.Api.Transformers.EnsureResourcesCompiled do
def transform(_module, dsl) do
dsl
|> Transformer.get_entities([:resources])
|> Enum.filter(& &1.warn_on_compile_failure?)
|> Enum.map(& &1.resource)
|> Enum.filter(fn resource ->
case Code.ensure_compiled(resource) do
Expand Down
161 changes: 129 additions & 32 deletions lib/ash/changeset/changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ defmodule Ash.Changeset do

if action do
changeset
|> cast_params(action, params, opts)
|> cast_params(action, params || %{}, opts)
|> cast_arguments(action)
|> Map.put(:__validated_for_action__, action.name)
|> validate_attributes_accepted(action)
Expand Down Expand Up @@ -328,20 +328,28 @@ defmodule Ash.Changeset do
Enum.reduce(params, changeset, fn {name, value}, changeset ->
cond do
attr = Ash.Resource.public_attribute(changeset.resource, name) ->
change_attribute(changeset, attr.name, value)
if attr.writable? do
change_attribute(changeset, attr.name, value)
else
changeset
end

rel = Ash.Resource.public_relationship(changeset.resource, name) ->
behaviour = opts[:relationships][rel.name] || :replace
if rel.writable? do
behaviour = opts[:relationships][rel.name] || :replace

case behaviour do
:replace ->
replace_relationship(changeset, rel.name, value)
case behaviour do
:replace ->
replace_relationship(changeset, rel.name, value)

:append ->
append_to_relationship(changeset, rel.name, value)
:append ->
append_to_relationship(changeset, rel.name, value)

:remove ->
append_to_relationship(changeset, rel.name, value)
:remove ->
append_to_relationship(changeset, rel.name, value)
end
else
changeset
end

has_argument?(action, name) ->
Expand Down Expand Up @@ -451,13 +459,19 @@ defmodule Ash.Changeset do
changeset

_ ->
add_error(
changeset,
Required.exception(
field: required_relationship.name,
type: :relationship
)
)
case Map.fetch(changeset.attributes, required_relationship.source_field) do
{:ok, value} when not is_nil(value) ->
changeset

_ ->
add_error(
changeset,
Required.exception(
field: required_relationship.name,
type: :relationship
)
)
end
end
end)
end
Expand Down Expand Up @@ -610,8 +624,9 @@ defmodule Ash.Changeset do
)
else
with {:ok, casted} <- Ash.Type.cast_input(argument.type, value),
:ok <- Ash.Type.apply_constraints(argument.type, casted, argument.constraints) do
%{new_changeset | arguments: Map.put(new_changeset.arguments, argument.name, value)}
{:ok, casted} <-
Ash.Type.apply_constraints(argument.type, casted, argument.constraints) do
%{new_changeset | arguments: Map.put(new_changeset.arguments, argument.name, casted)}
else
_ ->
Ash.Changeset.add_error(
Expand Down Expand Up @@ -1002,9 +1017,12 @@ defmodule Ash.Changeset do
add_attribute_invalid_error(changeset, attribute, "Attribute is not writable")

attribute ->
with {:ok, casted} <- Ash.Type.cast_input(attribute.type, value),
with {:ok, prepared} <- prepare_change(changeset, attribute, value),
{:ok, casted} <- Ash.Type.cast_input(attribute.type, prepared),
{:ok, casted} <- handle_change(changeset, attribute, casted),
:ok <- validate_allow_nil(attribute, casted),
:ok <- Ash.Type.apply_constraints(attribute.type, casted, attribute.constraints) do
{:ok, casted} <-
Ash.Type.apply_constraints(attribute.type, casted, attribute.constraints) do
data_value = Map.get(changeset.data, attribute.name)

cond do
Expand All @@ -1022,9 +1040,14 @@ defmodule Ash.Changeset do
add_attribute_invalid_error(changeset, attribute)

{:error, error_or_errors} ->
error_or_errors
|> List.wrap()
|> Enum.reduce(changeset, &add_attribute_invalid_error(&2, attribute, &1))
errors =
if Keyword.keyword?(error_or_errors) do
[error_or_errors]
else
List.wrap(error_or_errors)
end

Enum.reduce(errors, changeset, &add_attribute_invalid_error(&2, attribute, &1))
end
end
end
Expand Down Expand Up @@ -1054,8 +1077,11 @@ defmodule Ash.Changeset do
%{changeset | attributes: Map.put(changeset.attributes, attribute.name, nil)}

attribute ->
with {:ok, casted} <- Ash.Type.cast_input(attribute.type, value),
:ok <- Ash.Type.apply_constraints(attribute.type, casted, attribute.constraints) do
with {:ok, prepared} <- prepare_change(changeset, attribute, value),
{:ok, casted} <- Ash.Type.cast_input(attribute.type, prepared),
{:ok, casted} <- handle_change(changeset, attribute, casted),
{:ok, casted} <-
Ash.Type.apply_constraints(attribute.type, casted, attribute.constraints) do
data_value = Map.get(changeset.data, attribute.name)

cond do
Expand Down Expand Up @@ -1110,6 +1136,20 @@ defmodule Ash.Changeset do
%{changeset | errors: [error | changeset.errors], valid?: false}
end

defp prepare_change(%{action_type: :create}, _attribute, value), do: {:ok, value}

defp prepare_change(changeset, attribute, value) do
old_value = Map.get(changeset.data, attribute.name)
Ash.Type.prepare_change(attribute.type, old_value, value)
end

defp handle_change(%{action_type: :create}, _attribute, value), do: {:ok, value}

defp handle_change(changeset, attribute, value) do
old_value = Map.get(changeset.data, attribute.name)
Ash.Type.handle_change(attribute.type, old_value, value)
end

defp reconcile_relationship_changes(%{replace: _, add: add} = changes) do
changes
|> Map.delete(:add)
Expand Down Expand Up @@ -1294,12 +1334,69 @@ defmodule Ash.Changeset do
defp validate_allow_nil(_, _), do: :ok

defp add_attribute_invalid_error(changeset, attribute, message \\ nil) do
error =
InvalidAttribute.exception(
field: attribute.name,
message: message
)
messages =
if Keyword.keyword?(message) do
[message]
else
List.wrap(message)
end

Enum.reduce(messages, changeset, fn message, changeset ->
opts =
case message do
keyword when is_list(keyword) ->
fields =
case List.wrap(keyword[:fields]) do
[] ->
List.wrap(keyword[:field])

fields ->
fields
end

fields
|> case do
[] ->
[
Keyword.put(keyword, :field, add_index(to_string(attribute.name), keyword))
]

fields ->
Enum.map(
fields,
&Keyword.put(
message,
:field,
add_index(to_string(attribute.name), message) <> ".#{&1}"
)
)
end

message when is_binary(message) ->
[[field: to_string(attribute.name), message: message]]

_ ->
[[field: to_string(attribute.name)]]
end

add_error(changeset, error)
Enum.reduce(opts, changeset, fn opts, changeset ->
error =
InvalidAttribute.exception(
field: Keyword.get(opts, :field),
message: Keyword.get(opts, :message),
vars: opts
)

add_error(changeset, error)
end)
end)
end

defp add_index(string, opts) do
if opts[:index] do
string <> "[#{opts[:index]}]"
else
string
end
end
end
21 changes: 0 additions & 21 deletions lib/ash/data_layer/simple.ex

This file was deleted.

83 changes: 83 additions & 0 deletions lib/ash/data_layer/simple/simple.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule Ash.DataLayer.Simple do
@moduledoc """
A data layer that simply returns structs
This is the data layer that is used under the hood
by embedded resources
"""

alias Ash.Query.Operator.{
Eq,
GreaterThan,
GreaterThanOrEqual,
In,
IsNil,
LessThan,
LessThanOrEqual
}

def can?(_, :read), do: true
def can?(_, :create), do: true
def can?(_, :update), do: true
def can?(_, :destroy), do: true
def can?(_, :sort), do: true
def can?(_, {:sort, _}), do: true
def can?(_, :filter), do: true
def can?(_, :boolean_filter), do: true
def can?(_, {:filter_operator, %In{}}), do: true
def can?(_, {:filter_operator, %Eq{}}), do: true
def can?(_, {:filter_operator, %LessThan{}}), do: true
def can?(_, {:filter_operator, %GreaterThan{}}), do: true
def can?(_, {:filter_operator, %LessThanOrEqual{}}), do: true
def can?(_, {:filter_operator, %GreaterThanOrEqual{}}), do: true
def can?(_, {:filter_operator, %IsNil{}}), do: true
def can?(_, _), do: false

defmodule Query do
@moduledoc false
defstruct [:data, :resource, :filter, :api, sort: []]
end

def resource_to_query(resource, api) do
%Query{data: [], resource: resource, api: api}
end

def run_query(%{data: data, sort: sort, api: api, filter: filter}, _resource) do
{:ok,
data
|> Enum.filter(&Ash.Filter.Runtime.matches?(api, &1, filter))
|> Ash.Actions.Sort.runtime_sort(sort)}
end

def filter(query, filter, _resource) do
{:ok, %{query | filter: filter}}
end

def sort(query, sort, _resource) do
{:ok, %{query | sort: sort}}
end

def set_context(_resource, query, context) do
data = Map.get(context, :data) || []

%{query | data: data}
end

def create(_resource, changeset) do
{:ok, Ash.Changeset.apply_attributes(changeset)}
end

def update(_resource, changeset) do
{:ok, Ash.Changeset.apply_attributes(changeset)}
end

def destroy(_resource, _changeset) do
:ok
end

@transformers [
Ash.DataLayer.Simple.Transformers.ValidateDslSections
]

use Ash.Dsl.Extension, transformers: @transformers, sections: []
end

0 comments on commit d888fcc

Please sign in to comment.