diff --git a/.formatter.exs b/.formatter.exs index b32773d..3754057 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -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, diff --git a/documentation/dsls/DSL:-Smokestack.md b/documentation/dsls/DSL:-Smokestack.md index fa2c3ec..2cc4c64 100644 --- a/documentation/dsls/DSL:-Smokestack.md +++ b/documentation/dsls/DSL:-Smokestack.md @@ -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` + diff --git a/lib/smokestack/builders/factory_builder.ex b/lib/smokestack/builders/factory_builder.ex index 83098a7..43301ee 100644 --- a/lib/smokestack/builders/factory_builder.ex +++ b/lib/smokestack/builders/factory_builder.ex @@ -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 diff --git a/lib/smokestack/builders/record_builder.ex b/lib/smokestack/builders/record_builder.ex index fc60913..8e02dec 100644 --- a/lib/smokestack/builders/record_builder.ex +++ b/lib/smokestack/builders/record_builder.ex @@ -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 diff --git a/lib/smokestack/dsl/after_build.ex b/lib/smokestack/dsl/after_build.ex new file mode 100644 index 0000000..f6d6ba0 --- /dev/null +++ b/lib/smokestack/dsl/after_build.ex @@ -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 diff --git a/lib/smokestack/dsl/attribute.ex b/lib/smokestack/dsl/attribute.ex index 305854b..7b05363 100644 --- a/lib/smokestack/dsl/attribute.ex +++ b/lib/smokestack/dsl/attribute.ex @@ -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 diff --git a/lib/smokestack/dsl/before_build.ex b/lib/smokestack/dsl/before_build.ex new file mode 100644 index 0000000..8428230 --- /dev/null +++ b/lib/smokestack/dsl/before_build.ex @@ -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 diff --git a/lib/smokestack/dsl/factory.ex b/lib/smokestack/dsl/factory.ex index 16e31d4..d4b1081 100644 --- a/lib/smokestack/dsl/factory.ex +++ b/lib/smokestack/dsl/factory.ex @@ -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 diff --git a/mix.lock b/mix.lock index 079d709..94fb018 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/test/smokestack/dsl_test.exs b/test/smokestack/dsl_test.exs index 6c95d78..3d23c78 100644 --- a/test/smokestack/dsl_test.exs +++ b/test/smokestack/dsl_test.exs @@ -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