James Harton
cb2d0376b5
This required a bit of a rework of how the options are validated. Now they're only validated once when `Builder.build` is called instead of inside each builder.
261 lines
8 KiB
Elixir
261 lines
8 KiB
Elixir
defmodule Smokestack do
|
|
alias Spark.{Dsl, Dsl.Extension}
|
|
|
|
@moduledoc """
|
|
Smokestack provides a way to define test factories for your [Ash
|
|
Resources](https://ash-hq.org/docs/module/ash/latest/ash-resource) using a
|
|
convenient DSL:
|
|
|
|
```
|
|
defmodule MyApp.Factory do
|
|
use Smokestack
|
|
|
|
factory Character do
|
|
attribute :name, &Faker.StarWars.character/0
|
|
attribute :affiliation, choose(["Galactic Empire", "Rebel Alliance"])
|
|
end
|
|
end
|
|
|
|
defmodule MyApp.CharacterTest do
|
|
use MyApp.DataCase
|
|
use MyApp.Factory
|
|
|
|
test "it can build a character" do
|
|
assert character = insert!(Character)
|
|
end
|
|
end
|
|
```
|
|
|
|
## Templates
|
|
|
|
Each attribute uses a template to generate a value when building a factory.
|
|
Templates can be anything that implements the `Smokestack.Template` protocol.
|
|
This protocol is automatically implemented for functions with arities zero
|
|
through two - meaning you can just drop in your own functions - or use one of
|
|
the built-in helpers from `Smokestack.Dsl.Template`.
|
|
|
|
## Variants
|
|
|
|
Sometimes you need to make slightly different factories to build a resource in
|
|
a specific state for your test scenario.
|
|
|
|
Here's an example defining an alternate `:trek` variant for the character
|
|
factory defined above:
|
|
|
|
```
|
|
factory Character, :trek do
|
|
attribute :name, choose(["J.L. Pipes", "Severn", "Slickback"])
|
|
attribute :affiliation, choose(["Entrepreneur", "Voyager"])
|
|
end
|
|
```
|
|
|
|
## Building resource records
|
|
|
|
You can use `insert/2` and `insert!/2` to build and insert records. Records
|
|
are inserted using `Ash.Seed.seed!/2`, which means that they bypass the usual
|
|
Ash lifecycle (actions, validations, etc).
|
|
|
|
### Options
|
|
|
|
- `load`: an atom, list of atoms or keyword list of the same listing
|
|
relationships, calculations and aggregates that should be loaded after the
|
|
record is created.
|
|
- `count`: rather than inserting just a single record, you can specify a
|
|
number of records to be inserted. A list of records will be returned.
|
|
- `build`: an atom, list of atoms or keyword list of the same describing
|
|
relationships which you would like built alongside the record. If the
|
|
related resource has a variant which matches the current one, it will be
|
|
used, and if not the `:default` variant will be.
|
|
- `attrs`: A map or keyword list of attributes you would like to set directly
|
|
on the created record, rather than using the value provided by the factory.
|
|
- `relate`: A keyword list of relationships to records (or lists of records)
|
|
to which you wish to directly relate the created record.
|
|
|
|
## Building parameters
|
|
|
|
As well as inserting records directly you can use `params/2` and `params!/2`
|
|
to build parameters for use testing controller actions, HTTP requests, etc.
|
|
|
|
### Options
|
|
|
|
- `encode`: rather than returning a map or maps, provide an encoder module to
|
|
serialise the parameters. Commonly you would use `Jason` or `Poison`.
|
|
- `nest`: rather than returning a map or maps directly, wrap the result in an
|
|
outer map using the provided key.
|
|
- `key_case`: change the case of the keys into one of the many cases supported
|
|
by [recase](https://hex.pm/packages/recase).
|
|
- `key_type`: specify whether the returned map or maps should use string or
|
|
atom keys (ignored when using the `encode` option).
|
|
- `count`: rather than returning just a single map, you can specify a number
|
|
of results to be returned. A list of maps will be returned.
|
|
- `build`: an atom, list of atoms or keyword list of the same describing
|
|
relationships which you would like built within the result. If the related
|
|
resource has a variant which matches the current one, it will be used, and
|
|
if not the `:default` variant will be.
|
|
- `attrs`: A map or keyword list of attributes you would like to set directly
|
|
on the result, rather than using the value provided by the factory.
|
|
|
|
<!--- ash-hq-hide-start --> <!--- -->
|
|
|
|
## DSL Documentation
|
|
|
|
### Index
|
|
|
|
#{Extension.doc_index(Smokestack.Dsl.sections())}
|
|
|
|
### Docs
|
|
|
|
#{Extension.doc(Smokestack.Dsl.sections())}
|
|
|
|
<!--- ash-hq-hide-stop --> <!--- -->
|
|
"""
|
|
|
|
use Dsl, default_extensions: [extensions: [Smokestack.Dsl]]
|
|
alias Ash.Resource
|
|
alias Smokestack.{Builder, ParamBuilder, RecordBuilder}
|
|
|
|
@type t :: module
|
|
|
|
@type recursive_atom_list :: atom | [atom | {atom, recursive_atom_list()}]
|
|
@type param_option :: variant_option | ParamBuilder.option()
|
|
@type insert_option :: variant_option | RecordBuilder.option()
|
|
|
|
@typedoc """
|
|
Choose a factory variant to use. Defaults to `:default`.
|
|
"""
|
|
@type variant_option :: {:variant, atom}
|
|
|
|
@doc """
|
|
Runs a factory and uses it to build a parameters suitable for simulating a
|
|
request.
|
|
|
|
Automatically implemented by modules which `use Smokestack`.
|
|
|
|
## Options
|
|
|
|
#{Builder.docs(ParamBuilder, nil)}
|
|
"""
|
|
@callback params(Resource.t(), [param_option]) ::
|
|
{:ok, ParamBuilder.result()} | {:error, any}
|
|
|
|
@doc """
|
|
Raising version of `c:params/2`.
|
|
|
|
Automatically implemented by modules which `use Smokestack`.
|
|
|
|
## Options
|
|
|
|
#{Builder.docs(ParamBuilder, nil)}
|
|
"""
|
|
@callback params!(Resource.t(), [param_option]) :: ParamBuilder.result() | no_return
|
|
|
|
@doc """
|
|
Runs a factory and uses it to insert Ash resources into their data layers.
|
|
|
|
Automatically implemented by modules which `use Smokestack`.
|
|
|
|
## Options
|
|
|
|
#{Builder.docs(RecordBuilder, nil)}
|
|
"""
|
|
@callback insert(Resource.t(), [insert_option]) ::
|
|
{:ok, RecordBuilder.result()} | {:error, any}
|
|
|
|
@doc """
|
|
Raising version of `c:insert/2`.
|
|
|
|
Automatically implemented by modules which `use Smokestack`.
|
|
|
|
## Options
|
|
|
|
#{Builder.docs(RecordBuilder, nil)}
|
|
"""
|
|
@callback insert!(Resource.t(), [insert_option]) :: RecordBuilder.result() | no_return
|
|
|
|
@doc false
|
|
defmacro __using__(opts) do
|
|
[
|
|
quote do
|
|
@behaviour Smokestack
|
|
|
|
@moduledoc @moduledoc ||
|
|
"""
|
|
A Smokestack factory.
|
|
|
|
See `Smokestack` for more information.
|
|
"""
|
|
|
|
@doc """
|
|
Runs a factory and uses it to build a parameters suitable for simulating a
|
|
request.
|
|
|
|
## Options
|
|
|
|
#{Builder.docs(ParamBuilder, nil)}
|
|
"""
|
|
@spec params(Resource.t(), [Smokestack.param_option()]) ::
|
|
{:ok, ParamBuilder.result()} | {:error, any}
|
|
def params(resource, options \\ []) do
|
|
Builder.build(__MODULE__, resource, ParamBuilder, options)
|
|
end
|
|
|
|
@doc """
|
|
Raising version of `params/2`.
|
|
|
|
## Options
|
|
|
|
#{Builder.docs(ParamBuilder, nil)}
|
|
"""
|
|
@spec params!(Resource.t(), [Smokestack.param_option()]) ::
|
|
ParamBuilder.result() | no_return
|
|
def params!(resource, options \\ []) do
|
|
case params(resource, options) do
|
|
{:ok, result} -> result
|
|
{:error, reason} -> raise reason
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Execute the matching factory and return an inserted Ash Resource record.
|
|
|
|
## Options
|
|
|
|
#{Builder.docs(RecordBuilder, nil)}
|
|
"""
|
|
@spec insert(Resource.t(), [Smokestack.insert_option()]) ::
|
|
{:ok, RecordBuilder.result()} | {:error, any}
|
|
def insert(resource, options \\ []) do
|
|
Builder.build(__MODULE__, resource, RecordBuilder, options)
|
|
end
|
|
|
|
@doc """
|
|
Raising version of `insert/2`.
|
|
|
|
## Options
|
|
|
|
#{Builder.docs(RecordBuilder, nil)}
|
|
"""
|
|
@spec insert!(Resource.t(), [Smokestack.insert_option()]) ::
|
|
RecordBuilder.result() | no_return
|
|
def insert!(resource, options \\ []) do
|
|
case insert(resource, options) do
|
|
{:ok, result} -> result
|
|
{:error, reason} -> raise reason
|
|
end
|
|
end
|
|
|
|
defmacro __using__(_) do
|
|
quote do
|
|
import unquote(__MODULE__)
|
|
end
|
|
end
|
|
|
|
defoverridable params: 2,
|
|
params!: 2,
|
|
insert: 2,
|
|
insert!: 2,
|
|
__using__: 1
|
|
end
|
|
] ++ super(opts)
|
|
end
|
|
end
|