feat: Add before_build and after_build entities to factories.
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
James Harton 2024-05-26 20:50:33 +12:00
parent 56e6fb9a4c
commit 5f8e19ee07
Signed by: james
GPG key ID: 90E82DAA13F624F4
10 changed files with 286 additions and 15 deletions

View file

@ -1,6 +1,10 @@
spark_locals_without_parens = [
after_build: 1,
after_build: 2,
attribute: 2,
attribute: 3,
before_build: 1,
before_build: 2,
domain: 1,
factory: 1,
factory: 2,

View file

@ -13,7 +13,9 @@ The DSL definition for the Smokestack DSL.
* smokestack
* factory
* after_build
* attribute
* before_build
### Docs
@ -22,7 +24,9 @@ The DSL definition for the Smokestack DSL.
* [factory](#module-factory)
* after_build
* attribute
* before_build
@ -38,7 +42,9 @@ The DSL definition for the Smokestack DSL.
Define factories for a resource
* after_build
* attribute
* before_build
@ -50,6 +56,26 @@ Define factories for a resource
##### after_build
Modify the record after building.
Allows you to provide a function which can modify the built record before returning.
These hooks are only applied when building records and not parameters.
* `:hook` (mfa or function of arity 1) - Required. A function which returns an updated record
##### attribute
@ -67,6 +93,24 @@ Define factories for a resource
##### before_build
Modify the attributes before building.
Allows you to provide a function which can modify the the attributes before building.
* `:hook` (mfa or function of arity 1) - Required. A function which returns an updated record
@ -79,7 +123,9 @@ Define factories for a resource
### Nested DSLs
* [factory](#smokestack-factory)
* after_build
* attribute
* before_build
@ -102,7 +148,9 @@ factory resource, variant \\ :default
Define factories for a resource
### Nested DSLs
* [after_build](#smokestack-factory-after_build)
* [attribute](#smokestack-factory-attribute)
* [before_build](#smokestack-factory-before_build)
@ -120,6 +168,38 @@ Define factories for a resource
| [`domain`](#smokestack-factory-domain){: #smokestack-factory-domain } | `module` | | The Ash Domain to use when evaluating loads |
## smokestack.factory.after_build
```elixir
after_build hook
```
Modify the record after building.
Allows you to provide a function which can modify the built record before returning.
These hooks are only applied when building records and not parameters.
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`hook`](#smokestack-factory-after_build-hook){: #smokestack-factory-after_build-hook .spark-required} | `(any -> any) \| mfa` | | A function which returns an updated record |
### Introspection
Target: `Smokestack.Dsl.AfterBuild`
## smokestack.factory.attribute
```elixir
attribute name, generator
@ -148,6 +228,36 @@ attribute name, generator
Target: `Smokestack.Dsl.Attribute`
## smokestack.factory.before_build
```elixir
before_build hook
```
Modify the attributes before building.
Allows you to provide a function which can modify the the attributes before building.
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`hook`](#smokestack-factory-before_build-hook){: #smokestack-factory-before_build-hook .spark-required} | `(any -> any) \| mfa` | | A function which returns an updated record |
### Introspection
Target: `Smokestack.Dsl.BeforeBuild`

View file

@ -27,13 +27,22 @@ defmodule Smokestack.FactoryBuilder do
overrides = Keyword.get(options, :attrs, %{})
with {:ok, overrides} <- validate_overrides(factory, overrides) do
factory.attributes
|> remove_overridden_attrs(overrides)
|> Enum.reduce({:ok, overrides}, fn attr, {:ok, attrs} ->
generator = maybe_initialise_generator(attr)
value = Template.generate(generator, attr, options)
{:ok, Map.put(attrs, attr.name, value)}
end)
attrs =
factory.attributes
|> remove_overridden_attrs(overrides)
|> Enum.reduce(overrides, fn attr, attrs ->
generator = maybe_initialise_generator(attr)
value = Template.generate(generator, attr, options)
Map.put(attrs, attr.name, value)
end)
attrs =
factory.before_build
|> Enum.reduce(attrs, fn hook, attrs ->
hook.hook.(attrs)
end)
{:ok, attrs}
end
end

View file

@ -100,7 +100,9 @@ defmodule Smokestack.RecordBuilder do
with {:ok, attr_list} <- Builder.build(ManyBuilder, factory, options),
{:ok, record_list} <- seed(attr_list, factory) do
maybe_load(record_list, factory, load)
record_list
|> maybe_hook(factory)
|> maybe_load(factory, load)
end
end
@ -109,7 +111,9 @@ defmodule Smokestack.RecordBuilder do
with {:ok, attrs} <- Builder.build(RelatedBuilder, factory, options),
{:ok, record} <- seed(attrs, factory) do
maybe_load(record, factory, load)
record
|> maybe_hook(factory)
|> maybe_load(factory, load)
end
end
@ -148,4 +152,18 @@ defmodule Smokestack.RecordBuilder do
defp maybe_load(record_or_records, factory, load),
do: Ash.load(record_or_records, load, domain: factory.domain)
defp maybe_hook(records, factory) when is_list(records) do
Enum.map(records, fn record ->
Enum.reduce(factory.after_build, record, fn hook, record ->
hook.(record)
end)
end)
end
defp maybe_hook(record, factory) when is_map(record) do
Enum.reduce(factory.after_build, record, fn hook, record ->
hook.hook.(record)
end)
end
end

View file

@ -0,0 +1,43 @@
defmodule Smokestack.Dsl.AfterBuild do
@moduledoc """
The `after_build` DSL entity.
See `d:Smokestack.factory.after_build` for more information.
"""
defstruct __identifier__: nil, hook: nil
alias Ash.Resource
alias Spark.Dsl.Entity
@type t :: %__MODULE__{
__identifier__: any,
hook: mfa | (Resource.record() -> Resource.record())
}
@doc false
@spec __entities__ :: [Entity.t()]
def __entities__,
do: [
%Entity{
name: :after_build,
describe: """
Modify the record after building.
Allows you to provide a function which can modify the built record before returning.
These hooks are only applied when building records and not parameters.
""",
target: __MODULE__,
args: [:hook],
identifier: {:auto, :unique_integer},
schema: [
hook: [
type: {:mfa_or_fun, 1},
required: true,
doc: "A function which returns an updated record"
]
]
}
]
end

View file

@ -2,7 +2,7 @@ defmodule Smokestack.Dsl.Attribute do
@moduledoc """
The `attribute ` DSL entity.
See `d:Smokestack.factory.default.attribute` for more information.
See `d:Smokestack.factory.attribute` for more information.
"""
defstruct __identifier__: nil, generator: nil, name: nil
@ -11,7 +11,7 @@ defmodule Smokestack.Dsl.Attribute do
alias Spark.Dsl.Entity
@type t :: %__MODULE__{
__identifier__: nil,
__identifier__: any,
generator:
mfa | (-> any) | (Resource.record() -> any) | (Resource.record(), keyword -> any),
name: atom

View file

@ -0,0 +1,41 @@
defmodule Smokestack.Dsl.BeforeBuild do
@moduledoc """
The `before_build` DSL entity.
See `d:Smokestack.factory.before_build` for more information.
"""
defstruct __identifier__: nil, hook: nil
alias Spark.Dsl.Entity
@type attrs :: %{required(String.t() | atom) => any}
@type t :: %__MODULE__{
__identifier__: any,
hook: mfa | (attrs -> attrs)
}
@doc false
@spec __entities__ :: [Entity.t()]
def __entities__,
do: [
%Entity{
name: :before_build,
describe: """
Modify the attributes before building.
Allows you to provide a function which can modify the the attributes before building.
""",
target: __MODULE__,
args: [:hook],
identifier: {:auto, :unique_integer},
schema: [
hook: [
type: {:mfa_or_fun, 1},
required: true,
doc: "A function which returns an updated record"
]
]
}
]
end

View file

@ -6,19 +6,23 @@ defmodule Smokestack.Dsl.Factory do
"""
defstruct __identifier__: nil,
after_build: [],
attributes: [],
before_build: [],
domain: nil,
module: nil,
resource: nil,
variant: :default
alias Ash.Resource
alias Smokestack.Dsl.{Attribute, Template}
alias Smokestack.Dsl.{AfterBuild, Attribute, BeforeBuild, Template}
alias Spark.Dsl.Entity
@type t :: %__MODULE__{
__identifier__: any,
after_build: [AfterBuild.t()],
attributes: [Attribute.t()],
before_build: [BeforeBuild.t()],
domain: nil,
module: module,
resource: Resource.t(),
@ -54,7 +58,11 @@ defmodule Smokestack.Dsl.Factory do
default: :default
]
],
entities: [attributes: Attribute.__entities__()]
entities: [
after_build: AfterBuild.__entities__(),
attributes: Attribute.__entities__(),
before_build: BeforeBuild.__entities__()
]
}
]
end

View file

@ -19,14 +19,14 @@
"git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"reactor": {:hex, :reactor, "0.8.2", "b2be82b1c3402537d06a8f85bb1849f72cb6b4be140495cb8956de7aec2fdebd", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c35eb23b77cc77ba922af108722ac93257899e35cfdd18882f0e659ad2cac9f3"},
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"},
"sourceror": {:hex, :sourceror, "1.2.1", "b415255ad8bd05f0e859bb3d7ea617f6c2a4a405f2a534a231f229bd99b89f8b", [], [], "hexpm", "e4d97087e67584a7585b5fe3d5a71bf8e7332f795dd1a44983d750003d5e750c"},
"sourceror": {:hex, :sourceror, "1.2.1", "b415255ad8bd05f0e859bb3d7ea617f6c2a4a405f2a534a231f229bd99b89f8b", [:mix], [], "hexpm", "e4d97087e67584a7585b5fe3d5a71bf8e7332f795dd1a44983d750003d5e750c"},
"spark": {:hex, :spark, "2.1.22", "a36400eede64c51af578de5fdb5a5aaa3e0811da44bcbe7545fce059bd2a990b", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "f764611d0b15ac132e72b2326539acc11fc4e63baa3e429f541bca292b5f7064"},
"splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"},
"stream_data": {:hex, :stream_data, "1.0.0", "c1380747a4650902732696861d5cb66ad3cb1cc93f31c2c8498bf87cddbabe2d", [:mix], [], "hexpm", "acd53e27c66c617d466f42ec77a7f59e5751f6051583c621ccdb055b9690435d"},

View file

@ -107,4 +107,42 @@ defmodule Smokestack.DslTest do
assert %Post{title: title} = FactoryUser.test()
assert title =~ ~r/[a-z]+/i
end
test "before build hooks can be applied" do
defmodule BeforeBuildFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
before_build &capitalise_title/1
end
def capitalise_title(record) do
%{record | title: String.upcase(record.title)}
end
end
title = Faker.Company.catch_phrase()
upper_title = String.upcase(title)
assert %Post{title: ^upper_title} = BeforeBuildFactory.insert!(Post, attrs: %{title: title})
end
test "after build hooks can be applied" do
defmodule AfterBuildFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
after_build &add_metadata/1
end
def add_metadata(record) do
Ash.Resource.put_metadata(record, :wat, true)
end
end
assert %Post{__metadata__: %{wat: true}} = AfterBuildFactory.insert!(Post)
end
end