feat: Auto build/load factory options. #83
7 changed files with 164 additions and 7 deletions
|
@ -3,6 +3,8 @@ spark_locals_without_parens = [
|
||||||
after_build: 2,
|
after_build: 2,
|
||||||
attribute: 2,
|
attribute: 2,
|
||||||
attribute: 3,
|
attribute: 3,
|
||||||
|
auto_build: 1,
|
||||||
|
auto_load: 1,
|
||||||
before_build: 1,
|
before_build: 1,
|
||||||
before_build: 2,
|
before_build: 2,
|
||||||
domain: 1,
|
domain: 1,
|
||||||
|
|
|
@ -54,6 +54,10 @@ Define factories for a resource
|
||||||
|
|
||||||
* `:variant` (`t:atom/0`) - The name of a factory variant The default value is `:default`.
|
* `:variant` (`t:atom/0`) - The name of a factory variant The default value is `:default`.
|
||||||
|
|
||||||
|
* `:auto_build` (one or a list of `t:atom/0`) - A list of relationships that should always be built when building this factory The default value is `[]`.
|
||||||
|
|
||||||
|
* `:auto_load` - An Ash "load statement" to always apply when building this factory The default value is `[]`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### after_build
|
##### after_build
|
||||||
|
@ -166,6 +170,8 @@ Define factories for a resource
|
||||||
| Name | Type | Default | Docs |
|
| Name | Type | Default | Docs |
|
||||||
|------|------|---------|------|
|
|------|------|---------|------|
|
||||||
| [`domain`](#smokestack-factory-domain){: #smokestack-factory-domain } | `module` | | The Ash Domain to use when evaluating loads |
|
| [`domain`](#smokestack-factory-domain){: #smokestack-factory-domain } | `module` | | The Ash Domain to use when evaluating loads |
|
||||||
|
| [`auto_build`](#smokestack-factory-auto_build){: #smokestack-factory-auto_build } | `atom \| list(atom)` | `[]` | A list of relationships that should always be built when building this factory |
|
||||||
|
| [`auto_load`](#smokestack-factory-auto_load){: #smokestack-factory-auto_load } | `atom \| keyword \| list(atom \| keyword)` | `[]` | An Ash "load statement" to always apply when building this factory |
|
||||||
|
|
||||||
|
|
||||||
## smokestack.factory.after_build
|
## smokestack.factory.after_build
|
||||||
|
|
|
@ -102,7 +102,7 @@ defmodule Smokestack.RecordBuilder do
|
||||||
{:ok, record_list} <- seed(attr_list, factory) do
|
{:ok, record_list} <- seed(attr_list, factory) do
|
||||||
record_list
|
record_list
|
||||||
|> maybe_hook(factory)
|
|> maybe_hook(factory)
|
||||||
|> maybe_load(factory, load)
|
|> maybe_load(factory, List.wrap(load))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ defmodule Smokestack.RecordBuilder do
|
||||||
{:ok, record} <- seed(attrs, factory) do
|
{:ok, record} <- seed(attrs, factory) do
|
||||||
record
|
record
|
||||||
|> maybe_hook(factory)
|
|> maybe_hook(factory)
|
||||||
|> maybe_load(factory, load)
|
|> maybe_load(factory, List.wrap(load))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -145,13 +145,18 @@ defmodule Smokestack.RecordBuilder do
|
||||||
|> Resource.put_metadata(:variant, factory.variant)
|
|> Resource.put_metadata(:variant, factory.variant)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_load(record_or_records, _factory, []), do: {:ok, record_or_records}
|
defp maybe_load(record_or_records, %{auto_load: []}, []), do: {:ok, record_or_records}
|
||||||
|
|
||||||
defp maybe_load(_record_or_records, factory, _load) when is_nil(factory.domain),
|
defp maybe_load(_record_or_records, factory, _load) when is_nil(factory.domain),
|
||||||
do: {:error, "Unable to perform `load` operation without an Domain."}
|
do: {:error, "Unable to perform `load` operation without an Domain."}
|
||||||
|
|
||||||
defp maybe_load(record_or_records, factory, load),
|
defp maybe_load(record_or_records, factory, load) do
|
||||||
do: Ash.load(record_or_records, load, domain: factory.domain)
|
load =
|
||||||
|
factory.auto_load
|
||||||
|
|> Enum.concat(load)
|
||||||
|
|
||||||
|
Ash.load(record_or_records, load, domain: factory.domain)
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_hook(records, factory) when is_list(records) do
|
defp maybe_hook(records, factory) when is_list(records) do
|
||||||
Enum.map(records, fn record ->
|
Enum.map(records, fn record ->
|
||||||
|
|
|
@ -145,6 +145,8 @@ defmodule Smokestack.RelatedBuilder do
|
||||||
options
|
options
|
||||||
|> Keyword.get(:build, [])
|
|> Keyword.get(:build, [])
|
||||||
|> List.wrap()
|
|> List.wrap()
|
||||||
|
|> Enum.concat(factory.auto_build)
|
||||||
|
|> List.wrap()
|
||||||
|> Enum.map(fn
|
|> Enum.map(fn
|
||||||
{key, value} -> {key, value}
|
{key, value} -> {key, value}
|
||||||
key when is_atom(key) -> {key, []}
|
key when is_atom(key) -> {key, []}
|
||||||
|
|
|
@ -8,6 +8,8 @@ defmodule Smokestack.Dsl.Factory do
|
||||||
defstruct __identifier__: nil,
|
defstruct __identifier__: nil,
|
||||||
after_build: [],
|
after_build: [],
|
||||||
attributes: [],
|
attributes: [],
|
||||||
|
auto_load: [],
|
||||||
|
auto_build: [],
|
||||||
before_build: [],
|
before_build: [],
|
||||||
domain: nil,
|
domain: nil,
|
||||||
module: nil,
|
module: nil,
|
||||||
|
@ -22,6 +24,8 @@ defmodule Smokestack.Dsl.Factory do
|
||||||
__identifier__: any,
|
__identifier__: any,
|
||||||
after_build: [AfterBuild.t()],
|
after_build: [AfterBuild.t()],
|
||||||
attributes: [Attribute.t()],
|
attributes: [Attribute.t()],
|
||||||
|
auto_load: [atom] | Keyword.t(),
|
||||||
|
auto_build: [atom],
|
||||||
before_build: [BeforeBuild.t()],
|
before_build: [BeforeBuild.t()],
|
||||||
domain: nil,
|
domain: nil,
|
||||||
module: module,
|
module: module,
|
||||||
|
@ -56,6 +60,18 @@ defmodule Smokestack.Dsl.Factory do
|
||||||
required: false,
|
required: false,
|
||||||
doc: "The name of a factory variant",
|
doc: "The name of a factory variant",
|
||||||
default: :default
|
default: :default
|
||||||
|
],
|
||||||
|
auto_build: [
|
||||||
|
type: {:wrap_list, :atom},
|
||||||
|
required: false,
|
||||||
|
doc: "A list of relationships that should always be built when building this factory",
|
||||||
|
default: []
|
||||||
|
],
|
||||||
|
auto_load: [
|
||||||
|
type: {:wrap_list, {:or, [:atom, :keyword_list]}},
|
||||||
|
required: false,
|
||||||
|
doc: "An Ash \"load statement\" to always apply when building this factory",
|
||||||
|
default: []
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
entities: [
|
entities: [
|
||||||
|
|
|
@ -9,7 +9,11 @@ defmodule Smokestack.Dsl.Verifier do
|
||||||
@impl true
|
@impl true
|
||||||
@spec verify(Dsl.t()) :: :ok | {:error, DslError.t()}
|
@spec verify(Dsl.t()) :: :ok | {:error, DslError.t()}
|
||||||
def verify(dsl_state) do
|
def verify(dsl_state) do
|
||||||
error_info = %{module: Verifier.get_persisted(dsl_state, :module), path: [:smokestack]}
|
error_info = %{
|
||||||
|
module: Verifier.get_persisted(dsl_state, :module),
|
||||||
|
path: [:smokestack],
|
||||||
|
dsl_state: dsl_state
|
||||||
|
}
|
||||||
|
|
||||||
factories =
|
factories =
|
||||||
dsl_state
|
dsl_state
|
||||||
|
@ -68,7 +72,9 @@ defmodule Smokestack.Dsl.Verifier do
|
||||||
error_info =
|
error_info =
|
||||||
Map.merge(error_info, %{resource: factory.resource, path: [:factory | error_info.path]})
|
Map.merge(error_info, %{resource: factory.resource, path: [:factory | error_info.path]})
|
||||||
|
|
||||||
with :ok <- verify_unique_attributes(factory, error_info) do
|
with :ok <- verify_unique_attributes(factory, error_info),
|
||||||
|
:ok <- verify_auto_build(factory, error_info),
|
||||||
|
:ok <- verify_auto_load(factory, error_info) do
|
||||||
factory
|
factory
|
||||||
|> Map.get(:attributes, [])
|
|> Map.get(:attributes, [])
|
||||||
|> Enum.filter(&is_struct(&1, Attribute))
|
|> Enum.filter(&is_struct(&1, Attribute))
|
||||||
|
@ -186,4 +192,76 @@ defmodule Smokestack.Dsl.Verifier do
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp verify_auto_build(factory, error_info) do
|
||||||
|
error_info = %{error_info | path: [:auto_build | error_info.path]}
|
||||||
|
|
||||||
|
Enum.reduce_while(factory.auto_build, :ok, fn relationship, :ok ->
|
||||||
|
error_info = %{error_info | path: [relationship | error_info.path]}
|
||||||
|
|
||||||
|
with {:ok, relationship} <- verify_relationship(factory.resource, relationship, error_info),
|
||||||
|
:ok <- verify_factory_exists(relationship.destination, error_info) do
|
||||||
|
{:cont, :ok}
|
||||||
|
else
|
||||||
|
{:error, error} -> {:halt, {:error, error}}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp verify_relationship(resource, relationship, error_info) do
|
||||||
|
case Info.relationship(resource, relationship) do
|
||||||
|
nil ->
|
||||||
|
{:error,
|
||||||
|
DslError.exception(
|
||||||
|
module: error_info.module,
|
||||||
|
path: Enum.reverse(error_info.path),
|
||||||
|
message:
|
||||||
|
"The resource `#{inspect(resource)}` has no relationship named `#{inspect(relationship)}`."
|
||||||
|
)}
|
||||||
|
|
||||||
|
relationship ->
|
||||||
|
{:ok, relationship}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp verify_factory_exists(resource, error_info) do
|
||||||
|
factory_exists? =
|
||||||
|
error_info.dsl_state
|
||||||
|
|> Verifier.get_entities([:smokestack])
|
||||||
|
|> Enum.any?(&(is_struct(&1, Factory) && &1.resource == resource))
|
||||||
|
|
||||||
|
if factory_exists? do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error,
|
||||||
|
DslError.exception(
|
||||||
|
module: error_info.module,
|
||||||
|
path: Enum.reverse(error_info.path),
|
||||||
|
message: "No factories defined for resource `#{inspect(resource)}`."
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp verify_auto_load(factory, error_info) do
|
||||||
|
error_info = %{error_info | path: [:auto_load | error_info.path]}
|
||||||
|
|
||||||
|
Enum.reduce_while(factory.auto_load, :ok, fn load, :ok ->
|
||||||
|
error_info = %{error_info | path: [load | error_info.path]}
|
||||||
|
|
||||||
|
with nil <- Info.calculation(factory.resource, load),
|
||||||
|
nil <- Info.aggregate(factory.resource, load),
|
||||||
|
nil <- Info.relationship(factory.resource, load) do
|
||||||
|
{:halt,
|
||||||
|
{:error,
|
||||||
|
DslError.exception(
|
||||||
|
module: error_info.module,
|
||||||
|
path: Enum.reverse(error_info.path),
|
||||||
|
message:
|
||||||
|
"Expected an aggregate, calculation or relationship named `#{inspect(load)}` on resource `#{inspect(factory.resource)}`"
|
||||||
|
)}}
|
||||||
|
else
|
||||||
|
_ -> {:cont, :ok}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule Smokestack.DslTest do
|
defmodule Smokestack.DslTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
alias Spark.Error.DslError
|
alias Spark.Error.DslError
|
||||||
|
alias Support.Author
|
||||||
|
|
||||||
defmodule Post do
|
defmodule Post do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
@ -18,6 +19,18 @@ defmodule Smokestack.DslTest do
|
||||||
|
|
||||||
attribute :title, :string
|
attribute :title, :string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
relationships do
|
||||||
|
belongs_to :author, Author
|
||||||
|
end
|
||||||
|
|
||||||
|
calculations do
|
||||||
|
calculate :title_first_word, :string, expr(title |> string_split() |> at(0))
|
||||||
|
end
|
||||||
|
|
||||||
|
actions do
|
||||||
|
defaults [:read]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule Factory do
|
defmodule Factory do
|
||||||
|
@ -145,4 +158,39 @@ defmodule Smokestack.DslTest do
|
||||||
|
|
||||||
assert %Post{__metadata__: %{wat: true}} = AfterBuildFactory.insert!(Post)
|
assert %Post{__metadata__: %{wat: true}} = AfterBuildFactory.insert!(Post)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "auto builds can be specified in the factory" do
|
||||||
|
defmodule AutoBuildFactory do
|
||||||
|
@moduledoc false
|
||||||
|
use Smokestack
|
||||||
|
|
||||||
|
factory Post do
|
||||||
|
attribute :title, &Faker.Company.catch_phrase/0
|
||||||
|
auto_build :author
|
||||||
|
end
|
||||||
|
|
||||||
|
factory Author do
|
||||||
|
attribute :name, &Faker.Internet.email/0
|
||||||
|
attribute :email, &Faker.Person.name/0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert %Post{author: %Author{}} = AutoBuildFactory.insert!(Post)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "auto loads can be specified in the factory" do
|
||||||
|
defmodule AutoLoadFactory do
|
||||||
|
@moduledoc false
|
||||||
|
use Smokestack
|
||||||
|
|
||||||
|
factory Post do
|
||||||
|
attribute :title, &Faker.Company.catch_phrase/0
|
||||||
|
auto_load :title_first_word
|
||||||
|
domain Support.Domain
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert post = AutoLoadFactory.insert!(Post)
|
||||||
|
assert post.title_first_word == post.title |> String.split(" ") |> List.first()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue