improvement: Add Smokestack behaviour.

This commit is contained in:
James Harton 2023-08-18 20:14:53 +12:00
parent 408c813320
commit f2df421786
Signed by: james
GPG key ID: 90E82DAA13F624F4
5 changed files with 131 additions and 21 deletions

View file

@ -19,6 +19,99 @@ defmodule Smokestack do
"""
use Dsl, default_extensions: [extensions: [Smokestack.Dsl]]
alias Ash.Resource
alias Smokestack.Builder
@type t :: module
@doc """
Runs a factory and uses it to build a map or list of results.
Automatically implemented by modules which `use Smokestack`.
See `Smokestack.Builder.params/5` for more information.
"""
@callback params(Resource.t(), map, atom, Builder.param_options()) ::
{:ok, Builder.param_result()} | {:error, any}
@doc """
Raising version of `params/4`.
Automatically implemented by modules which `use Smokestack`.
See `Smokestack.Builder.params/5` for more information.
"""
@callback params!(Resource.t(), map, atom, Builder.param_options()) ::
Builder.param_result() | no_return
@doc """
Runs a factory and uses it to insert an Ash Resource into it's data layer.
Automatically implemented by modules which `use Smokestack`.
See `Smokestack.Builder.insert/5` for more information.
"""
@callback insert(Resource.t(), map, atom, Builder.insert_options()) ::
{:ok, Resource.record()} | {:error, any}
@doc """
Raising version of `insert/4`.
Automatically implemented by modules which `use Smokestack`.
See `Smokestack.Builder.insert/5` for more information.
"""
@callback insert!(Resource.t(), map, atom, Builder.insert_options()) ::
Resource.record() | no_return
@doc false
defmacro __using__(opts) do
[
quote do
@behaviour Smokestack
@doc """
Execute the matching factory and return a map or list of params.
See `Smokestack.Builder.params/5` for more information.
"""
@spec params(Resource.t(), map, atom, Builder.param_options()) ::
{:ok, Builder.param_result()} | {:error, any}
def params(resource, overrides \\ %{}, variant \\ :default, options \\ []),
do: Builder.params(__MODULE__, resource, overrides, variant, options)
@doc """
Raising version of `params/4`.
See `Smokestack.Builder.params/5` for more information.
"""
@spec params!(Resource.t(), map, atom, Builder.param_options()) ::
Builder.param_result() | no_return
def params!(resource, overrides \\ %{}, variant \\ :default, options \\ []),
do: Builder.params!(__MODULE__, resource, overrides, variant, options)
@doc """
Execute the matching factory and return an inserted Ash Resource record.
See `Smokestack.Builder.insert/5` for more information.
"""
@spec insert(Resource.t(), map, atom, Builder.insert_options()) ::
{:ok, Resource.record()} | {:error, any}
def insert(resource, overrides \\ %{}, variant \\ :default, options \\ []),
do: Builder.insert(__MODULE__, resource, overrides, variant, options)
@doc """
Raising version of `insert/4`.
See `Smokestack.Builder.insert/5` for more information.
"""
@spec insert!(Resource.t(), map, atom, Builder.insert_options()) ::
Resource.record() | no_return
def insert!(resource, overrides \\ %{}, variant \\ :default, options \\ []),
do: Builder.insert!(__MODULE__, resource, overrides, variant, options)
defoverridable params: 4, params!: 4, insert: 4, insert!: 4
end
] ++ super(opts)
end
end

View file

@ -9,7 +9,7 @@ defmodule Smokestack.Builder do
@param_option_defaults [keys: :atom, as: :map]
@typedoc "Options that can be passed to `params/4`."
@type param_options :: [param_keys_option | param_as_option]
@type param_options :: [param_keys_option | param_as_option | build_option]
@typedoc "Key type in the result. Defaults to `#{inspect(@param_option_defaults[:keys])}`."
@type param_keys_option :: {:keys, :atom | :string | :dasherise}
@ -18,21 +18,22 @@ defmodule Smokestack.Builder do
@type param_as_option :: {:as, :map | :list}
@type param_result ::
%{required(String.t()) => any}
| %{required(atom) => any}
| [{String.t(), any}]
| [{atom, any}]
%{required(atom | String.t()) => any}
| [{atom | String.t(), any}]
@type insert_options :: []
@type insert_options :: [build_option]
@typedoc "A nested keyword list of associations that should also be built"
@type build_option :: {:build, Keyword.t(atom | Keyword.t())}
@type insert_result :: Resource.record()
@doc """
Build parameters for a resource with a factory.
"""
@spec params(Smokestack.t(), Resource.t(), atom, map, param_options) ::
@spec params(Smokestack.t(), Resource.t(), map, atom, param_options) ::
{:ok, param_result} | {:error, any}
def params(factory_module, resource, variant \\ :default, overrides \\ %{}, options \\ [])
def params(factory_module, resource, overrides \\ %{}, variant \\ :default, options \\ [])
when is_atom(factory_module) and is_atom(resource) and is_atom(variant) and is_list(options) do
with {:ok, factory} <- get_factory(factory_module, resource, variant),
{:ok, params} <- build_params(factory, overrides, options) do
@ -47,10 +48,10 @@ defmodule Smokestack.Builder do
end
@doc "Raising version of `params/2..5`."
@spec params!(Smokestack.t(), Resource.t(), atom, map, param_options) ::
@spec params!(Smokestack.t(), Resource.t(), map, atom, param_options) ::
param_result | no_return
def params!(factory_module, resource, variant \\ :default, overrides \\ %{}, options \\ []) do
case params(factory_module, resource, variant, overrides, options) do
def params!(factory_module, resource, overrides \\ %{}, variant \\ :default, options \\ []) do
case params(factory_module, resource, overrides, variant, options) do
{:ok, params} -> params
{:error, reason} -> raise reason
end
@ -59,9 +60,9 @@ defmodule Smokestack.Builder do
@doc """
Build a resource and insert it into it's datalayer.
"""
@spec insert(Smokestack.t(), Resource.t(), atom, map, insert_options) ::
@spec insert(Smokestack.t(), Resource.t(), map, atom, insert_options) ::
{:ok, insert_result} | {:error, any}
def insert(factory_module, resource, variant \\ :default, overrides \\ %{}, options \\ [])
def insert(factory_module, resource, overrides \\ %{}, variant \\ :default, options \\ [])
when is_atom(factory_module) and is_atom(resource) and is_atom(Variant) and is_list(options) do
with {:ok, factory} <- get_factory(factory_module, resource, variant),
{:ok, params} <- build_params(factory, overrides, options) do
@ -78,9 +79,9 @@ defmodule Smokestack.Builder do
end
@doc "Raising version of `insert/2..5`"
@spec insert!(Smokestack.t(), Resource.t(), atom, map, insert_options) ::
@spec insert!(Smokestack.t(), Resource.t(), map, atom, insert_options) ::
insert_result | no_return
def insert!(factory_module, resource, variant \\ :default, overrides \\ %{}, options \\ [])
def insert!(factory_module, resource, overrides \\ %{}, variant \\ :default, options \\ [])
when is_atom(factory_module) and is_atom(resource) and is_atom(variant) and
is_map(overrides) and is_list(options) do
with {:ok, factory} <- get_factory(factory_module, resource, variant),
@ -113,12 +114,21 @@ defmodule Smokestack.Builder do
{:ok, Map.put(attrs, attr.name, override)}
:error ->
value = Template.generate(attr.generator, attrs, options)
generator = maybe_initialise_generator(attr)
value = Template.generate(generator, attrs, options)
{:ok, Map.put(attrs, attr.name, value)}
end
end)
end
defp maybe_initialise_generator(attr) do
with nil <- Process.get(attr.__identifier__),
generator <- Template.init(attr.generator) do
Process.put(attr.__identifier__, generator)
generator
end
end
defp maybe_stringify_keys(attrs, options) do
if Keyword.get(options, :keys, @param_option_defaults[:keys]) == :string do
Map.new(attrs, fn {key, value} -> {Atom.to_string(key), value} end)

View file

@ -5,12 +5,13 @@ defmodule Smokestack.Dsl.Attribute do
See `d:Smokestack.factory.default.attribute` for more information.
"""
defstruct generator: nil, name: nil
defstruct __identifier__: nil, generator: nil, name: nil
alias Ash.Resource
alias Spark.Dsl.Entity
@type t :: %__MODULE__{
__identifier__: nil,
generator:
mfa | (-> any) | (Resource.record() -> any) | (Resource.record(), keyword -> any),
name: atom
@ -24,6 +25,7 @@ defmodule Smokestack.Dsl.Attribute do
name: :attribute,
target: __MODULE__,
args: [:name, :generator],
identifier: {:auto, :unique_integer},
schema: [
name: [
type: :atom,

View file

@ -5,13 +5,14 @@ defmodule Smokestack.Dsl.Factory do
See `d:Smokestack.factory` for more information.
"""
defstruct attributes: [], resource: nil, variant: :default
defstruct __identifier__: nil, attributes: [], resource: nil, variant: :default
alias Ash.Resource
alias Smokestack.Dsl.{Attribute, Template}
alias Spark.Dsl.Entity
@type t :: %__MODULE__{
__identifier__: any,
attributes: [Attribute.t()],
resource: Resource.t(),
variant: atom
@ -27,6 +28,7 @@ defmodule Smokestack.Dsl.Factory do
target: __MODULE__,
args: [:resource, {:optional, :variant, :default}],
imports: [Template],
identifier: {:auto, :unique_integer},
schema: [
resource: [
type: {:behaviour, Ash.Resource},

View file

@ -15,7 +15,7 @@ defmodule Smokestack.BuilderTest do
end
test "it honours the `as: :list` option" do
assert {:ok, params} = Builder.params(Factory, Post, :default, %{}, as: :list)
assert {:ok, params} = Builder.params(Factory, Post, %{}, :default, as: :list)
assert is_list(params)
assert is_binary(params[:body])
assert Enum.all?(params[:tags], &is_binary/1)
@ -23,14 +23,14 @@ defmodule Smokestack.BuilderTest do
end
test "it honours the `keys: :string` option" do
assert {:ok, params} = Builder.params(Factory, Post, :default, %{}, keys: :string)
assert {:ok, params} = Builder.params(Factory, Post, %{}, :default, keys: :string)
assert is_binary(params["body"])
assert Enum.all?(params["tags"], &is_binary/1)
assert is_binary(params["title"])
end
test "it honours the `keys: :dasherise` option" do
assert {:ok, params} = Builder.params(Factory, Post, :default, %{}, keys: :dasherise)
assert {:ok, params} = Builder.params(Factory, Post, %{}, :default, keys: :dasherise)
assert is_binary(params["sub-title"])
end
end
@ -40,6 +40,9 @@ defmodule Smokestack.BuilderTest do
assert {:ok, record} = Builder.insert(Factory, Post)
assert is_struct(record, Post)
assert record.inserted_at
assert is_binary(record.title)
assert is_binary(record.sub_title)
assert Enum.all?(record.tags, &is_struct(&1, Ash.CiString))
end
end
end