improvement: Add Smokestack
behaviour.
This commit is contained in:
parent
408c813320
commit
f2df421786
5 changed files with 131 additions and 21 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue