Compare commits

...

47 commits

Author SHA1 Message Date
Renovate Bot c5adaf448b chore(deps): update dependency ash to v3.0.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-01 03:18:37 +12:00
James Harton 85aa3e2f6e chore: release version v0.9.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-30 20:54:44 +00:00
James Harton f8480ec8e7 fix: allow auto builds to be replaced by explicit relate commands.
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-31 08:54:03 +12:00
Rebecca Le d5be51556d bug: Add failing test for auto-build and relate options colliding 2024-05-31 08:54:03 +12:00
Renovate Bot ebc07800b4 chore(deps): update dependency ex_doc to v0.34.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-31 01:24:03 +12:00
James Harton 5368a030dd chore: release version v0.9.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-28 21:51:27 +00:00
James Harton ef5d6462b9 feat: Auto build/load factory options. (#83)
All checks were successful
continuous-integration/drone/push Build is passing
New factory DSL options:

1. `auto_build` allows you to provide a list of relationships which must also be built when building that factory.
2. `auto_load` allows you to provide a load statement for relationships and calculations that must be loaded when building that factory.

Reviewed-on: #83
Co-authored-by: James Harton <james@harton.nz>
Co-committed-by: James Harton <james@harton.nz>
2024-05-29 09:50:43 +12:00
Renovate Bot f46d9bb6b9 chore(deps): update dependency ash to v3.0.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-29 07:19:14 +12:00
Renovate Bot 4786ee97e6 chore(deps): update dependency recase to ~> 0.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-28 21:22:06 +12:00
James Harton 0d42238d6f chore: release version v0.8.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-28 04:48:48 +00:00
James Harton cb2d0376b5
fix: Include :variant in option schema.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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.
2024-05-28 16:40:43 +12:00
James Harton aa65f4912b
fix: bug with generator arguments. 2024-05-28 16:33:00 +12:00
James Harton 5c524faef8 chore: release version v0.8.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-26 08:51:36 +00:00
James Harton 5f8e19ee07
feat: Add before_build and after_build entities to factories.
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-26 20:50:33 +12:00
Renovate Bot 56e6fb9a4c chore(deps): update dependency ash to v3.0.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-24 18:18:27 +12:00
Renovate Bot 04f1393c7c chore(deps): update dependency ash to v3.0.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-24 11:19:07 +12:00
Renovate Bot bab42763d3 chore(deps): update dependency ash to v3.0.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-24 03:17:37 +12:00
Renovate Bot 1ed4612aec chore(deps): update dependency ash to v3.0.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-23 11:19:09 +12:00
Renovate Bot 9a5579bb50 chore(deps): update dependency ash to v3.0.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-23 10:19:03 +12:00
Renovate Bot f03b4487bf chore(deps): update dependency ex_doc to v0.33.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-22 01:20:12 +12:00
Renovate Bot 535acaa1a2 chore(deps): update dependency elixir to v1.16.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-21 11:21:16 +12:00
Renovate Bot 550178ed34 chore(deps): update dependency erlang to v27
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-20 21:42:45 +12:00
James Harton 690a388a93 chore: release version v0.7.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-20 04:59:12 +00:00
James Harton e99dec4808
feat(related): Newly build records can now be related to existing records.
All checks were successful
continuous-integration/drone/push Build is passing
Closes #14.
2024-05-20 16:57:18 +12:00
James Harton 0139a0e496
chore: re-enable auto releasing now that Ash 3.0 is out. 2024-05-20 16:56:17 +12:00
Renovate Bot 0bf111b22a chore(deps): update dependency ash to v3.0.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-16 10:18:27 +12:00
Renovate Bot 50eb88866f chore(deps): update dependency spark to v2.1.22
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-15 04:17:53 +12:00
Renovate Bot d5acff239d chore(deps): update dependency ash to v3.0.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-15 02:35:09 +12:00
James Harton 198cb16a9c
chore: release version v0.6.2
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-11 15:28:05 +12:00
Renovate Bot 13395f343c chore(deps): update dependency spark to v2.1.21
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-11 11:26:40 +12:00
Renovate Bot f6d6529f88 chore(deps): update dependency git_ops to v2.6.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-11 10:39:28 +12:00
Renovate Bot 35e0d80dbd chore(deps): update dependency ex_doc to v0.32.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-10 23:37:48 +12:00
Renovate Bot 51960bb6b6 chore(deps): update dependency credo to v1.7.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-09 23:58:22 +12:00
Renovate Bot 29eb9d2a5b chore(deps): update dependency erlang to v26.2.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-03 03:35:39 +12:00
Renovate Bot e2b88e262e chore(deps): update dependency spark to v2.1.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-22 05:30:50 +12:00
Renovate Bot 7d294a5e25 chore(deps): update dependency spark to v2.1.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-22 03:33:35 +12:00
Renovate Bot b9039bd529 chore(deps): update dependency ex_doc to v0.32.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-13 08:34:54 +12:00
Renovate Bot 2c25a1eda6 chore(deps): update dependency spark to v2.1.18
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-13 06:31:31 +12:00
Renovate Bot edfe181d7d chore(deps): update dependency erlang to v26.2.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-13 00:35:51 +12:00
Renovate Bot 9ad6337ac3 chore(deps): update dependency spark to v2.1.17
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-12 05:30:12 +12:00
Renovate Bot dd68439047 chore(deps): update dependency spark to v2.1.15
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-11 04:58:15 +12:00
Renovate Bot 25c67edccf chore(deps): update dependency spark to v2.1.14
Some checks reported errors
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build encountered an error
2024-04-11 02:30:09 +12:00
Renovate Bot 492e90badf chore(deps): update dependency ex_doc to v0.32.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-10 21:32:20 +12:00
Renovate Bot 0e895100ea chore(deps): update dependency spark to v2.1.13
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-06 12:30:56 +13:00
James Harton 5b5c7adf13
chore: release version v0.6.1-rc.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-03 16:26:58 +13:00
James Harton b2e4602376
improvement: loosen Ash RC requirement. 2024-04-03 16:25:51 +13:00
Renovate Bot a34eac9a7f chore(deps): update dependency ash to == 3.0.0-rc.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-03 14:24:59 +13:00
30 changed files with 945 additions and 173 deletions

View file

@ -252,47 +252,47 @@ steps:
- git log -1 --format=%s > .last_commit_message
- asdf mix git_ops.check_message .last_commit_message
# - name: mix git_ops.release
# image: harton.dev/james/asdf_container:latest
# when:
# branch:
# - main
# event:
# exclude:
# - pull_request
# depends_on:
# - mix test
# - mix credo
# - mix hex.audit
# - mix format
# - mix spark.formatter
# - mix spark.cheat_sheets
# - mix deps.unlock
# - mix doctor
# - mix git_ops.check_message
# environment:
# MIX_ENV: test
# HEX_HOME: /drone/src/.hex
# MIX_HOME: /drone/src/.mix
# REBAR_BASE_DIR: /drone/src/.rebar3
# ASDF_DATA_DIR: /drone/src/.asdf
# ASDF_DIR: /root/.asdf
# DRONE_TOKEN:
# from_secret: DRONE_TOKEN
# commands:
# - git fetch --tags
# - . $ASDF_DIR/asdf.sh
# - mix git_ops.project_info --format=shell > before.env
# - mix git_ops.release --yes --no-major || true
# - mix git_ops.project_info --format=shell > after.env
# - . ./before.env
# - export OLD_APP_VERSION=$${APP_VERSION}
# - . ./after.env
# - export NEW_APP_VERSION=$${APP_VERSION}
# - if [ "v$${OLD_APP_VERSION}" != "v$${NEW_APP_VERSION}" ]; then
# - export GIT_URL=$(echo $DRONE_GIT_HTTP_URL | sed -e "s/:\\/\\//:\\/\\/$DRONE_REPO_OWNER:$DRONE_TOKEN@/")
# - git push $${GIT_URL} "HEAD:${DRONE_COMMIT_REF}" "refs/tags/v$${NEW_APP_VERSION}"
# - fi
- name: mix git_ops.release
image: harton.dev/james/asdf_container:latest
when:
branch:
- main
event:
exclude:
- pull_request
depends_on:
- mix test
- mix credo
- mix hex.audit
- mix format
- mix spark.formatter
- mix spark.cheat_sheets
- mix deps.unlock
- mix doctor
- mix git_ops.check_message
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
MIX_HOME: /drone/src/.mix
REBAR_BASE_DIR: /drone/src/.rebar3
ASDF_DATA_DIR: /drone/src/.asdf
ASDF_DIR: /root/.asdf
DRONE_TOKEN:
from_secret: DRONE_TOKEN
commands:
- git fetch --tags
- . $ASDF_DIR/asdf.sh
- mix git_ops.project_info --format=shell > before.env
- mix git_ops.release --yes --no-major || true
- mix git_ops.project_info --format=shell > after.env
- . ./before.env
- export OLD_APP_VERSION=$${APP_VERSION}
- . ./after.env
- export NEW_APP_VERSION=$${APP_VERSION}
- if [ "v$${OLD_APP_VERSION}" != "v$${NEW_APP_VERSION}" ]; then
- export GIT_URL=$(echo $DRONE_GIT_HTTP_URL | sed -e "s/:\\/\\//:\\/\\/$DRONE_REPO_OWNER:$DRONE_TOKEN@/")
- git push $${GIT_URL} "HEAD:${DRONE_COMMIT_REF}" "refs/tags/v$${NEW_APP_VERSION}"
- fi
- name: build artifacts
image: harton.dev/james/asdf_container:latest

View file

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

View file

@ -1,2 +1,2 @@
elixir 1.16.2-otp-26
erlang 26.2.3
elixir 1.16.3
erlang 27.0

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"cSpell.words": [
"arities",
"Cardassia",
"recase",
"Slickback"
]
}

View file

@ -5,6 +5,67 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog -->
## [v0.9.1](https://harton.dev/james/smokestack/compare/v0.9.0...v0.9.1) (2024-05-30)
### Bug Fixes:
* allow auto builds to be replaced by explicit relate commands.
## [v0.9.0](https://harton.dev/james/smokestack/compare/v0.8.1...v0.9.0) (2024-05-28)
### Features:
* Auto build/load factory options. (#83)
## [v0.8.1](https://harton.dev/james/smokestack/compare/v0.8.0...v0.8.1) (2024-05-28)
### Bug Fixes:
* Include `:variant` in option schema.
* bug with generator arguments.
## [v0.8.0](https://harton.dev/james/smokestack/compare/v0.7.0...v0.8.0) (2024-05-26)
### Features:
* Add `before_build` and `after_build` entities to factories.
## [v0.7.0](https://harton.dev/james/smokestack/compare/v0.6.2...v0.7.0) (2024-05-20)
### Features:
* related: Newly build records can now be related to existing records.
## [v0.6.2](https://harton.dev/james/smokestack/compare/v0.6.1-rc.2...v0.6.2) (2024-05-11)
## [v0.6.1-rc.2](https://harton.dev/james/smokestack/compare/v0.6.1-rc.1...v0.6.1-rc.2) (2024-04-03)
### Improvements:
* loosen Ash RC requirement.
## [v0.6.1-rc.1](https://harton.dev/james/smokestack/compare/v0.6.1-rc.0...v0.6.1-rc.1) (2024-04-01)

View file

@ -38,7 +38,7 @@ add it directly to your `mix.exs`:
```elixir
def deps do
[
{:smokestack, "~> 0.6.1-rc.1"},
{:smokestack, "~> 0.9.1"},
]
end
```

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
@ -48,6 +54,30 @@ Define factories for a resource
* `:variant` (`t:atom/0`) - The name of a factory variant The default value is `:default`.
* `:auto_build` (one or a list of `t:atom/0`) - A list of relationships that should always be built when building this factory The default value is `[]`.
* `:auto_load` - An Ash "load statement" to always apply when building this factory The default value is `[]`.
##### 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 +97,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 +127,9 @@ Define factories for a resource
### Nested DSLs
* [factory](#smokestack-factory)
* after_build
* attribute
* before_build
@ -102,7 +152,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)
@ -118,8 +170,42 @@ Define factories for a resource
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`domain`](#smokestack-factory-domain){: #smokestack-factory-domain } | `module` | | The Ash Domain to use when evaluating loads |
| [`auto_build`](#smokestack-factory-auto_build){: #smokestack-factory-auto_build } | `atom \| list(atom)` | `[]` | A list of relationships that should always be built when building this factory |
| [`auto_load`](#smokestack-factory-auto_load){: #smokestack-factory-auto_load } | `atom \| keyword \| list(atom \| keyword)` | `[]` | An Ash "load statement" to always apply when building this factory |
## 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 +234,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

@ -2,9 +2,9 @@ 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:
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
@ -36,8 +36,8 @@ defmodule Smokestack do
## Variants
Sometimes you need to make slightly different factories to build a resource
in a specific state for your test scenario.
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:
@ -58,8 +58,8 @@ defmodule Smokestack do
### 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.
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
@ -68,6 +68,8 @@ defmodule Smokestack do
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
@ -76,20 +78,20 @@ defmodule Smokestack do
### 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).
- `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.
- `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.
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.
@ -110,7 +112,7 @@ defmodule Smokestack do
use Dsl, default_extensions: [extensions: [Smokestack.Dsl]]
alias Ash.Resource
alias Smokestack.{Builder, Dsl.Info, ParamBuilder, RecordBuilder}
alias Smokestack.{Builder, ParamBuilder, RecordBuilder}
@type t :: module
@ -194,11 +196,7 @@ defmodule Smokestack do
@spec params(Resource.t(), [Smokestack.param_option()]) ::
{:ok, ParamBuilder.result()} | {:error, any}
def params(resource, options \\ []) do
{variant, options} = Keyword.pop(options, :variant, :default)
with {:ok, factory} <- Info.factory(__MODULE__, resource, variant) do
Builder.build(ParamBuilder, factory, options)
end
Builder.build(__MODULE__, resource, ParamBuilder, options)
end
@doc """
@ -227,11 +225,7 @@ defmodule Smokestack do
@spec insert(Resource.t(), [Smokestack.insert_option()]) ::
{:ok, RecordBuilder.result()} | {:error, any}
def insert(resource, options \\ []) do
{variant, options} = Keyword.pop(options, :variant, :default)
with {:ok, factory} <- Info.factory(__MODULE__, resource, variant) do
Builder.build(RecordBuilder, factory, options)
end
Builder.build(__MODULE__, resource, RecordBuilder, options)
end
@doc """

View file

@ -3,7 +3,8 @@ defmodule Smokestack.Builder do
A generic behaviour for "building things".
"""
alias Smokestack.Dsl.Factory
alias Ash.Resource
alias Smokestack.Dsl.{Factory, Info}
alias Spark.Options
@type result :: any
@ -18,14 +19,18 @@ defmodule Smokestack.Builder do
@doc """
Provide a schema for validating options.
"""
@callback option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, any}
@callback option_schema(nil | Factory.t()) ::
{:ok, Options.schema(), String.t()} | {:error, any}
@doc """
Given a builder and a factory, validate it's options and call the builder.
Find the appropriate factory, validate options and run the builder.
"""
@spec build(t, Factory.t(), Keyword.t()) :: {:ok, result} | {:error, error}
def build(builder, factory, options) do
with {:ok, schema} <- builder.option_schema(factory),
@spec build(Smokestack.t(), Resource.t(), t, Keyword.t()) :: {:ok, result} | {:error, error}
def build(factory_module, resource, builder, options) do
with {:ok, our_schema} <- variant_schema(factory_module, resource),
{:ok, factory} <- Info.factory(factory_module, resource, options[:variant] || :default),
{:ok, builder_schema, section} <- builder.option_schema(factory),
schema <- Options.merge(our_schema, builder_schema, section),
{:ok, options} <- Options.validate(options, schema) do
builder.build(factory, options)
end
@ -36,7 +41,29 @@ defmodule Smokestack.Builder do
"""
@spec docs(t, nil | Factory.t()) :: String.t()
def docs(builder, factory) do
{:ok, schema} = builder.option_schema(factory)
{:ok, schema, _} = builder.option_schema(factory)
Options.docs(schema)
end
defp variant_schema(factory_module, resource) do
case Info.variants(factory_module, resource) do
[] ->
{:error,
"There are no factories defined for the resource `#{inspect(resource)}` in the `#{inspect(factory_module)}` module."}
variants ->
our_schema = [
variant: [
type: {:in, variants},
required: false,
default: :default,
doc: """
The name of the factory variant to use.
"""
]
]
{:ok, our_schema}
end
end
end

View file

@ -4,7 +4,7 @@ defmodule Smokestack.FactoryBuilder do
"""
alias Ash.Resource
alias Smokestack.{Builder, Dsl.Attribute, Dsl.Factory, Template}
alias Smokestack.{Builder, Dsl.Factory, Template}
alias Spark.Options
@behaviour Builder
@ -24,25 +24,31 @@ defmodule Smokestack.FactoryBuilder do
@impl true
@spec build(Factory.t(), [option]) :: {:ok, result} | {:error, error}
def build(factory, options) do
overrides = options[:attrs]
overrides = Keyword.get(options, :attrs, %{})
factory
|> Map.get(:attributes, [])
|> Enum.filter(&is_struct(&1, Attribute))
|> Enum.reduce({:ok, %{}}, fn
attr, {:ok, attrs} when is_map_key(overrides, attr.name) ->
{:ok, Map.put(attrs, attr.name, Map.get(overrides, attr.name))}
with {:ok, overrides} <- validate_overrides(factory, overrides) do
attrs =
factory.attributes
|> remove_overridden_attrs(overrides)
|> Enum.reduce(overrides, fn attr, attrs ->
generator = maybe_initialise_generator(attr)
value = Template.generate(generator, attrs, options)
Map.put(attrs, attr.name, value)
end)
attr, {:ok, attrs} ->
generator = maybe_initialise_generator(attr)
value = Template.generate(generator, attrs, options)
{:ok, 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
@doc false
@impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error}
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do
attr_keys =
if factory do
@ -74,7 +80,7 @@ defmodule Smokestack.FactoryBuilder do
```
"""
]
]}
], "Options for building instances"}
end
defp maybe_initialise_generator(attr) do
@ -84,4 +90,26 @@ defmodule Smokestack.FactoryBuilder do
generator
end
end
defp validate_overrides(factory, overrides) do
valid_attr_names =
factory.resource
|> Resource.Info.attributes()
|> Enum.map(& &1.name)
Enum.reduce_while(overrides, {:ok, overrides}, fn {key, _}, {:ok, overrides} ->
if key in valid_attr_names do
{:cont, {:ok, overrides}}
else
{:halt,
{:error,
"No attribute named `#{inspect(key)}` available on resource `#{inspect(factory.resource)}`"}}
end
end)
end
defp remove_overridden_attrs(attrs, overrides) when map_size(overrides) == 0, do: attrs
defp remove_overridden_attrs(attrs, overrides),
do: Enum.reject(attrs, &is_map_key(overrides, &1.name))
end

View file

@ -30,9 +30,9 @@ defmodule Smokestack.ManyBuilder do
@doc false
@impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error}
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory) do
with {:ok, related_schema, related_section} <- RelatedBuilder.option_schema(factory) do
schema =
[
count: [
@ -53,16 +53,16 @@ defmodule Smokestack.ManyBuilder do
"""
]
]
|> Options.merge(related_schema, "Options for building relationships")
|> Options.merge(related_schema, related_section)
{:ok, schema}
{:ok, schema, "Options for building multiple instances"}
end
end
defp do_build(factory, how_many, options) when how_many > 0 and is_integer(how_many) do
1..how_many
|> Enum.reduce_while({:ok, []}, fn _, {:ok, results} ->
case Builder.build(RelatedBuilder, factory, options) do
case RelatedBuilder.build(factory, options) do
{:ok, attrs} -> {:cont, {:ok, [attrs | results]}}
{:error, reason} -> {:halt, {:error, reason}}
end

View file

@ -69,10 +69,10 @@ defmodule Smokestack.ParamBuilder do
@doc false
@impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error}
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory),
{:ok, many_schema} <- ManyBuilder.option_schema(factory) do
with {:ok, related_schema, related_section} <- RelatedBuilder.option_schema(factory),
{:ok, many_schema, many_section} <- ManyBuilder.option_schema(factory) do
many_schema =
Keyword.update!(many_schema, :count, fn current ->
current
@ -178,10 +178,10 @@ defmodule Smokestack.ParamBuilder do
schema =
our_schema
|> Options.merge(many_schema, "Options for building multiple instances")
|> Options.merge(related_schema, "Options for building relationships")
|> Options.merge(many_schema, many_section)
|> Options.merge(related_schema, related_section)
{:ok, schema}
{:ok, schema, "Options for building parameters"}
end
end
@ -189,7 +189,7 @@ defmodule Smokestack.ParamBuilder do
{my_opts, their_opts} = split_options(options)
their_opts = Keyword.put(their_opts, :count, count)
with {:ok, attr_list} <- Builder.build(ManyBuilder, factory, their_opts) do
with {:ok, attr_list} <- ManyBuilder.build(factory, their_opts) do
attr_list
|> convert_keys(my_opts)
|> maybe_nest_result(my_opts[:nest])
@ -200,7 +200,7 @@ defmodule Smokestack.ParamBuilder do
defp do_build(factory, options, _) do
{my_opts, their_opts} = split_options(options)
with {:ok, attrs} <- Builder.build(RelatedBuilder, factory, their_opts) do
with {:ok, attrs} <- RelatedBuilder.build(factory, their_opts) do
attrs
|> convert_keys(my_opts)
|> maybe_nest_result(my_opts[:nest])

View file

@ -31,10 +31,10 @@ defmodule Smokestack.RecordBuilder do
@doc false
@impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error}
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory),
{:ok, many_schema} <- ManyBuilder.option_schema(factory) do
with {:ok, related_schema, related_section} <- RelatedBuilder.option_schema(factory),
{:ok, many_schema, many_section} <- ManyBuilder.option_schema(factory) do
load_type =
if factory do
loadable_names =
@ -87,10 +87,10 @@ defmodule Smokestack.RecordBuilder do
"""
]
]
|> Options.merge(many_schema, "Options for building multiple instances")
|> Options.merge(related_schema, "Options for building relationships")
|> Options.merge(many_schema, many_section)
|> Options.merge(related_schema, related_section)
{:ok, schema}
{:ok, schema, "Options for building records"}
end
end
@ -98,18 +98,22 @@ defmodule Smokestack.RecordBuilder do
{load, options} = Keyword.pop(options, :load, [])
options = Keyword.put(options, :count, count)
with {:ok, attr_list} <- Builder.build(ManyBuilder, factory, options),
with {:ok, attr_list} <- ManyBuilder.build(factory, options),
{:ok, record_list} <- seed(attr_list, factory) do
maybe_load(record_list, factory, load)
record_list
|> maybe_hook(factory)
|> maybe_load(factory, List.wrap(load))
end
end
defp do_build(factory, options, _count) do
{load, options} = Keyword.pop(options, :load, [])
with {:ok, attrs} <- Builder.build(RelatedBuilder, factory, options),
with {:ok, attrs} <- RelatedBuilder.build(factory, options),
{:ok, record} <- seed(attrs, factory) do
maybe_load(record, factory, load)
record
|> maybe_hook(factory)
|> maybe_load(factory, List.wrap(load))
end
end
@ -141,11 +145,30 @@ defmodule Smokestack.RecordBuilder do
|> Resource.put_metadata(:variant, factory.variant)
end
defp maybe_load(record_or_records, _factory, []), do: {:ok, record_or_records}
defp maybe_load(record_or_records, %{auto_load: []}, []), do: {:ok, record_or_records}
defp maybe_load(_record_or_records, factory, _load) when is_nil(factory.domain),
do: {:error, "Unable to perform `load` operation without an Domain."}
defp maybe_load(record_or_records, factory, load),
do: Ash.load(record_or_records, load, domain: factory.domain)
defp maybe_load(record_or_records, factory, load) do
load =
factory.auto_load
|> Enum.concat(load)
Ash.load(record_or_records, load, domain: factory.domain)
end
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

@ -9,13 +9,18 @@ defmodule Smokestack.RelatedBuilder do
alias Spark.Options
@behaviour Builder
@type option :: build_option | FactoryBuilder.option()
@type option :: build_option | relate_option | FactoryBuilder.option()
@typedoc """
A nested keyword list of associations that should also be built.
"""
@type build_option :: {:build, Smokestack.recursive_atom_list()}
@typedoc """
A nested keyword list of previously built records to explicitly associate to the new record.
"""
@type relate_option :: {:relate, [{atom, Resource.record()}]}
@type result :: %{optional(atom) => any}
@type error :: FactoryBuilder.error() | Exception.t()
@ -25,16 +30,17 @@ defmodule Smokestack.RelatedBuilder do
@impl true
@spec build(Factory.t(), [option]) :: {:ok, result} | {:error, error}
def build(factory, options) do
with {:ok, attrs} <- Builder.build(FactoryBuilder, factory, Keyword.delete(options, :build)) do
maybe_build_related(factory, attrs, options)
with {:ok, attrs} <- FactoryBuilder.build(factory, Keyword.drop(options, [:build, :relate])),
{:ok, attrs} <- maybe_build_related(factory, attrs, options) do
maybe_relate(factory, attrs, options)
end
end
@doc false
@impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error}
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do
with {:ok, factory_schema} <- FactoryBuilder.option_schema(factory) do
with {:ok, factory_schema, factory_section} <- FactoryBuilder.option_schema(factory) do
build_type =
if factory do
relationship_names =
@ -55,6 +61,26 @@ defmodule Smokestack.RelatedBuilder do
{:or, [{:wrap_list, :atom}, :keyword_list]}
end
relate_type =
if factory do
factory.resource
|> Resource.Info.relationships()
|> Enum.map(fn
relationship when relationship.cardinality == :one ->
{relationship.name, [type: {:struct, relationship.destination}, required: false]}
relationship when relationship.cardinality == :many ->
{relationship.name,
[type: {:wrap_list, {:struct, relationship.destination}}, required: false]}
end)
|> case do
[] -> :keyword_list
keys -> {:or, [{:keyword_list, keys}]}
end
else
:keyword_list
end
schema =
[
build: [
@ -83,21 +109,35 @@ defmodule Smokestack.RelatedBuilder do
build one instance.
If these caveats are an issue, then you can build them yourself and
pass them in using the `attrs` option.
pass them in using the `relate` option.
For example:
```elixir
posts = insert!(Post, count: 3)
author = insert(Author, posts: posts)
author = insert(Author, relate: [posts: posts])
```
"""
],
relate: [
type: relate_type,
required: false,
default: [],
doc: """
A list of records to relate.
For example
```elixir
author = insert!(Author)
post = insert!(Post, relate: [author: Author])
```
"""
]
]
|> Options.merge(factory_schema, "Options for building instances")
|> Options.merge(factory_schema, factory_section)
{:ok, schema}
{:ok, schema, "Options for building relationships"}
end
end
@ -105,10 +145,12 @@ defmodule Smokestack.RelatedBuilder do
options
|> Keyword.get(:build, [])
|> List.wrap()
|> Enum.concat(factory.auto_build)
|> Enum.map(fn
{key, value} -> {key, value}
key when is_atom(key) -> {key, []}
end)
|> remove_explicit_relates(options)
|> Enum.reduce_while({:ok, attrs}, fn {relationship, nested_builds}, {:ok, attrs} ->
case build_related(
attrs,
@ -122,6 +164,18 @@ defmodule Smokestack.RelatedBuilder do
end)
end
defp remove_explicit_relates(builds, options) do
relates =
options[:relate]
|> List.wrap()
|> Map.new()
builds
|> Enum.reject(fn {key, _value} ->
is_map_key(relates, key)
end)
end
defp build_related(attrs, relationship, factory, options) do
ash_relationship = Resource.Info.relationship(factory.resource, relationship)
build_related(attrs, relationship, factory, options, ash_relationship)
@ -141,8 +195,7 @@ defmodule Smokestack.RelatedBuilder do
|> Keyword.put(:attrs, %{})
with {:ok, related_factory} <- find_related_factory(relationship.destination, factory),
{:ok, related_attrs} <-
Builder.build(__MODULE__, related_factory, related_options) do
{:ok, related_attrs} <- __MODULE__.build(related_factory, related_options) do
case relationship.cardinality do
:one ->
{:ok, Map.put(attrs, relationship.name, related_attrs)}
@ -153,6 +206,53 @@ defmodule Smokestack.RelatedBuilder do
end
end
defp maybe_relate(factory, attrs, options) do
options
|> Keyword.get(:relate, [])
|> List.wrap()
|> Enum.map(fn {relationship, record} ->
{relationship, record, Resource.Info.relationship(factory.resource, relationship)}
end)
|> Enum.reduce_while({:ok, attrs}, fn
{relationship_name, _record, nil}, {:ok, _attrs} ->
{:halt,
{:error,
"No relationship named `#{inspect(relationship_name)}` defined on resource `#{inspect(factory.resource)}`"}}
{_, record, relationship}, {:ok, attrs} ->
case relate(attrs, relationship, record) do
{:ok, attrs} -> {:cont, {:ok, attrs}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
defp relate(attrs, relationship, record)
when is_struct(record, relationship.destination) and relationship.cardinality == :one,
do: {:ok, Map.put(attrs, relationship.name, record)}
defp relate(_attrs, relationship, record) when relationship.cardinality == :one,
do:
{:error,
"Expected value to be a `#{inspect(relationship.destination)}` record, however it is #{inspect(record)}"}
defp relate(attrs, relationship, records) when relationship.cardinality == :many do
records
|> Enum.reduce_while({:ok, []}, fn
record, {:ok, records} when is_struct(record, relationship.destination) ->
{:cont, {:ok, [record | records]}}
record, _ ->
{:halt,
{:error,
"Expected value to be a `#{inspect(relationship.destination)}` record, however it is #{inspect(record)}"}}
end)
|> case do
{:ok, records} -> {:ok, Map.put(attrs, relationship.name, records)}
{:error, reason} -> {:error, reason}
end
end
defp find_related_factory(resource, factory) when factory.variant == :default,
do: Info.factory(factory.module, resource, :default)

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,27 @@ defmodule Smokestack.Dsl.Factory do
"""
defstruct __identifier__: nil,
after_build: [],
attributes: [],
auto_load: [],
auto_build: [],
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()],
auto_load: [atom] | Keyword.t(),
auto_build: [atom],
before_build: [BeforeBuild.t()],
domain: nil,
module: module,
resource: Resource.t(),
@ -52,9 +60,25 @@ defmodule Smokestack.Dsl.Factory do
required: false,
doc: "The name of a factory variant",
default: :default
],
auto_build: [
type: {:wrap_list, :atom},
required: false,
doc: "A list of relationships that should always be built when building this factory",
default: []
],
auto_load: [
type: {:wrap_list, {:or, [:atom, :keyword_list]}},
required: false,
doc: "An Ash \"load statement\" to always apply when building this factory",
default: []
]
],
entities: [attributes: Attribute.__entities__()]
entities: [
after_build: AfterBuild.__entities__(),
attributes: Attribute.__entities__(),
before_build: BeforeBuild.__entities__()
]
}
]
end

View file

@ -27,4 +27,24 @@ defmodule Smokestack.Dsl.Info do
{:ok, factory}
end
end
@doc "Raising version of `factory/3`"
def factory!(factory, resource, variant) do
case factory(factory, resource, variant) do
{:ok, factory} -> factory
{:error, reason} -> raise reason
end
end
@doc """
List all variants available for a resource.
"""
@spec variants(Smokestack.t(), Resource.t()) :: [atom]
def variants(factory, resource) do
factory
|> Extension.get_entities([:smokestack])
|> Enum.filter(&(is_struct(&1, Factory) && &1.resource == resource))
|> Enum.map(& &1.variant)
|> Enum.uniq()
end
end

View file

@ -9,7 +9,11 @@ defmodule Smokestack.Dsl.Verifier do
@impl true
@spec verify(Dsl.t()) :: :ok | {:error, DslError.t()}
def verify(dsl_state) do
error_info = %{module: Verifier.get_persisted(dsl_state, :module), path: [:smokestack]}
error_info = %{
module: Verifier.get_persisted(dsl_state, :module),
path: [:smokestack],
dsl_state: dsl_state
}
factories =
dsl_state
@ -68,7 +72,9 @@ defmodule Smokestack.Dsl.Verifier do
error_info =
Map.merge(error_info, %{resource: factory.resource, path: [:factory | error_info.path]})
with :ok <- verify_unique_attributes(factory, error_info) do
with :ok <- verify_unique_attributes(factory, error_info),
:ok <- verify_auto_build(factory, error_info),
:ok <- verify_auto_load(factory, error_info) do
factory
|> Map.get(:attributes, [])
|> Enum.filter(&is_struct(&1, Attribute))
@ -186,4 +192,76 @@ defmodule Smokestack.Dsl.Verifier do
)}
end
end
defp verify_auto_build(factory, error_info) do
error_info = %{error_info | path: [:auto_build | error_info.path]}
Enum.reduce_while(factory.auto_build, :ok, fn relationship, :ok ->
error_info = %{error_info | path: [relationship | error_info.path]}
with {:ok, relationship} <- verify_relationship(factory.resource, relationship, error_info),
:ok <- verify_factory_exists(relationship.destination, error_info) do
{:cont, :ok}
else
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp verify_relationship(resource, relationship, error_info) do
case Info.relationship(resource, relationship) do
nil ->
{:error,
DslError.exception(
module: error_info.module,
path: Enum.reverse(error_info.path),
message:
"The resource `#{inspect(resource)}` has no relationship named `#{inspect(relationship)}`."
)}
relationship ->
{:ok, relationship}
end
end
defp verify_factory_exists(resource, error_info) do
factory_exists? =
error_info.dsl_state
|> Verifier.get_entities([:smokestack])
|> Enum.any?(&(is_struct(&1, Factory) && &1.resource == resource))
if factory_exists? do
:ok
else
{:error,
DslError.exception(
module: error_info.module,
path: Enum.reverse(error_info.path),
message: "No factories defined for resource `#{inspect(resource)}`."
)}
end
end
defp verify_auto_load(factory, error_info) do
error_info = %{error_info | path: [:auto_load | error_info.path]}
Enum.reduce_while(factory.auto_load, :ok, fn load, :ok ->
error_info = %{error_info | path: [load | error_info.path]}
with nil <- Info.calculation(factory.resource, load),
nil <- Info.aggregate(factory.resource, load),
nil <- Info.relationship(factory.resource, load) do
{:halt,
{:error,
DslError.exception(
module: error_info.module,
path: Enum.reverse(error_info.path),
message:
"Expected an aggregate, calculation or relationship named `#{inspect(load)}` on resource `#{inspect(factory.resource)}`"
)}}
else
_ -> {:cont, :ok}
end
end)
end
end

View file

@ -1,7 +1,7 @@
defmodule Smokestack.MixProject do
use Mix.Project
@version "0.6.1-rc.1"
@version "0.9.1"
@moduledoc """
Test factories for Ash resources.
@ -93,7 +93,7 @@ defmodule Smokestack.MixProject do
opts = [only: ~w[dev test]a, runtime: false]
[
{:ash, "== 3.0.0-rc.7"},
{:ash, "~> 3.0"},
{:credo, "~> 1.7", opts},
{:dialyxir, "~> 1.3", opts},
{:doctor, "~> 0.21", opts},
@ -103,7 +103,7 @@ defmodule Smokestack.MixProject do
{:faker, "~> 0.18", opts},
{:git_ops, "~> 2.6", opts},
{:mix_audit, "~> 2.1", opts},
{:recase, "~> 0.7"},
{:recase, "~> 0.8"},
{:spark, "~> 2.1"}
]
end

View file

@ -1,8 +1,8 @@
%{
"ash": {:hex, :ash, "3.0.0-rc.7", "f72a64fba1acdce7016984fe9cdf5e3ed856939fb14611d0c168e9971fe9bbed", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.8", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.7 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6edc89cd39ec68bd765bfea271df011b20ab86f181cfab59ef6159e3a1577b4"},
"ash": {:hex, :ash, "3.0.9", "f98266488f9f152130a6015c7158f70ba8262939c4dd0728bb55ab48b6d282eb", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "adb0853558c13cd77a4489e755df149e0e43bde9c069811ec244dd5dcbe62cd0"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
"credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
"credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
@ -12,24 +12,24 @@
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
"ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"},
"ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.6.0", "e0791ee1cf5db03f2c61b7ebd70e2e95cba2bb9b9793011f26609f22c0900087", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "b98fca849b18aaf490f4ac7d1dd8c6c469b0cc3e6632562d366cab095e666ffe"},
"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.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"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, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
"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.1", "1aec71d16083901277727c8162f6dd0f07e80f5ca98911b6ef4f2c95e6e62758", [: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", "ae3936d97a3e4a316744f70c77b85345b08b70da334024c26e6b5eb8ede1246b"},
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"},
"sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"},
"spark": {:hex, :spark, "2.1.11", "8093149dfd583b5ce2c06e1fea1faaf4125b50e4703138b2cbefb78c8f4aa07f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "1877d92ab993b860e9d828bfd72d50367c0d3a53dd84f4de5d221baf66ae8723"},
"splode": {:hex, :splode, "0.2.1", "020079ec06c9e00f8b6586852e781b5e07aee6ba588f3f45dd993831c87b0511", [:mix], [], "hexpm", "d232a933666061fe1f659d9906042fa94b9b393bb1129a4fde6fa680033b2611"},
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
"reactor": {:hex, :reactor, "0.8.4", "344d02ba4a0010763851f4e4aa0ff190ebe7e392e3c27c6cd143dde077b986e7", [: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", "49c1fd3c786603cec8140ce941c41c7ea72cc4411860ccdee9876c4ca2204f81"},
"recase": {:hex, :recase, "0.8.0", "ec9500abee5d493d41e3cbfd7d51a4e10957a164570be0c805d5c6661b8cdbae", [:mix], [], "hexpm", "0d4b67b81e7897af77552bd1e6d6148717a4b45ec5c7b014a48b0ba9a28946b5"},
"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.1.0", "ef3a7cac0f200c43caf3e6caf9be63115851b4f1cde3f21afaab220adc40e3d7", [:mix], [], "hexpm", "cccc411d5facf1bab86e7c671382d164f05f8992574c95349d3c8b317e14d953"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},

View file

@ -7,14 +7,13 @@ defmodule Smokestack.FactoryBuilderTest do
test "it can build attributes from a factory" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, attrs} = Builder.build(FactoryBuilder, factory, [])
assert {:ok, attrs} = FactoryBuilder.build(factory, [])
assert byte_size(attrs[:title]) > 0
end
test "it allows attributes to be overridden" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, %{title: "wat"}} =
Builder.build(FactoryBuilder, factory, attrs: %{title: "wat"})
assert {:ok, %{title: "wat"}} = FactoryBuilder.build(factory, attrs: %{title: "wat"})
end
end

View file

@ -7,14 +7,14 @@ defmodule Smokestack.ManyBuilderTest do
test "it can build a factory more than once" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, results} = Builder.build(ManyBuilder, factory, count: 2)
assert {:ok, results} = ManyBuilder.build(factory, count: 2)
assert length(results) == 2
assert Enum.all?(results, &(byte_size(&1.title) > 0))
end
test "it errors when asked to build less than one instance" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:error, reason} = Builder.build(ManyBuilder, factory, count: 0)
assert Exception.message(reason) =~ ~r/expected positive integer/i
assert {:error, reason} = ManyBuilder.build(factory, count: 0)
assert Exception.message(reason) =~ ~r/positive integer/i
end
end

View file

@ -7,35 +7,35 @@ defmodule Smokestack.RecordBuilderTest do
test "it can build a single record" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = Builder.build(RecordBuilder, factory, [])
assert {:ok, record} = RecordBuilder.build(factory, [])
assert is_struct(record, Post)
assert record.__meta__.state == :loaded
end
test "it can build multiple records" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, records} = Builder.build(RecordBuilder, factory, count: 2)
assert {:ok, records} = RecordBuilder.build(factory, count: 2)
assert length(records) == 2
assert Enum.all?(records, &(is_struct(&1, Post) && &1.__meta__.state == :loaded))
end
test "it can build directly related records" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = Builder.build(RecordBuilder, factory, build: :author)
assert {:ok, record} = RecordBuilder.build(factory, build: :author)
assert is_struct(record.author, Author)
assert record.author.__meta__.state == :loaded
end
test "it can build indirectly related records" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = Builder.build(RecordBuilder, factory, build: [author: :posts])
assert {:ok, record} = RecordBuilder.build(factory, build: [author: :posts])
assert [%Post{} = post] = record.author.posts
assert post.__meta__.state == :loaded
end
test "it can load calculations" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = Builder.build(RecordBuilder, factory, load: :full_title)
assert {:ok, record} = RecordBuilder.build(factory, load: :full_title)
assert record.full_title == record.title <> ": " <> record.sub_title
end
@ -43,7 +43,7 @@ defmodule Smokestack.RecordBuilderTest do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} =
Builder.build(RecordBuilder, factory,
RecordBuilder.build(factory,
load: [author: :count_of_posts],
build: [author: :posts]
)
@ -55,7 +55,7 @@ defmodule Smokestack.RecordBuilderTest do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} =
Builder.build(RecordBuilder, factory, build: :author, load: [author: :posts])
RecordBuilder.build(factory, build: :author, load: [author: :posts])
assert [post] = record.author.posts
assert post.id == record.id

View file

@ -2,19 +2,26 @@ defmodule Smokestack.RelatedBuilderTest do
@moduledoc false
use ExUnit.Case, async: true
alias Smokestack.{Builder, Dsl.Info, RelatedBuilder}
alias Support.{Factory, Post}
alias Smokestack.{Builder, Dsl.Info, RecordBuilder, RelatedBuilder}
alias Support.{Author, Factory, Post}
test "it can build attributes from directly related factories" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, attrs} = Builder.build(RelatedBuilder, factory, build: :author)
assert {:ok, attrs} = RelatedBuilder.build(factory, build: :author)
assert byte_size(attrs[:author][:name]) > 0
end
test "it can build attributes from indirectly related factories" do
{:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, attrs} = Builder.build(RelatedBuilder, factory, build: [author: :posts])
assert {:ok, attrs} = RelatedBuilder.build(factory, build: [author: :posts])
assert [post] = attrs[:author][:posts]
assert byte_size(post[:title]) > 0
end
test "it can attach directly related records" do
{:ok, factory} = Info.factory(Factory, Post, :default)
{:ok, author} = RecordBuilder.build(Info.factory!(Factory, Author, :default), [])
{:ok, attrs} = RelatedBuilder.build(factory, relate: [author: author])
assert attrs[:author] == author
end
end

View file

@ -1,6 +1,7 @@
defmodule Smokestack.DslTest do
use ExUnit.Case, async: true
alias Spark.Error.DslError
alias Support.Author
defmodule Post do
@moduledoc false
@ -18,6 +19,18 @@ defmodule Smokestack.DslTest do
attribute :title, :string
end
relationships do
belongs_to :author, Author
end
calculations do
calculate :title_first_word, :string, expr(title |> string_split() |> at(0))
end
actions do
defaults [:read]
end
end
defmodule Factory do
@ -107,4 +120,101 @@ 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
test "auto builds can be specified in the factory" do
defmodule AutoBuildFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
auto_build :author
end
factory Author do
attribute :name, &Faker.Internet.email/0
attribute :email, &Faker.Person.name/0
end
end
assert %Post{author: %Author{}} = AutoBuildFactory.insert!(Post)
end
test "auto builds can be overridden at runtime" do
defmodule AutoBuildRelateFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
auto_build :author
end
factory Author do
attribute :name, &Faker.Internet.email/0
attribute :email, &Faker.Person.name/0
end
end
author = AutoBuildRelateFactory.insert!(Author)
post = AutoBuildRelateFactory.insert!(Post, relate: [author: author])
# The auto-build should not be used - the existing author should be used
assert Ash.count!(Author, domain: Support.Domain) == 1
assert post.author.id == author.id
end
test "auto loads can be specified in the factory" do
defmodule AutoLoadFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
auto_load :title_first_word
domain Support.Domain
end
end
assert post = AutoLoadFactory.insert!(Post)
assert post.title_first_word == post.title |> String.split(" ") |> List.first()
end
end

View file

@ -0,0 +1,87 @@
defmodule Smokestack.OptionTest do
@moduledoc false
use ExUnit.Case, async: true
use Support.Factory
alias Support.{Author, Post}
describe "no options" do
test "a record can be generated directly from the factory" do
assert {:ok, author} = insert(Author)
assert is_binary(author.id)
assert is_binary(author.name)
assert is_binary(to_string(author.email))
end
end
describe "attrs" do
test "a record can be generated from the factory with some attributes overridden" do
assert {:ok, author} = insert(Author, attrs: %{name: "J.M. Dillard"})
assert author.name == "J.M. Dillard"
end
end
describe "count" do
test "many records can be generated from the factory" do
assert {:ok, authors} = insert(Author, count: 3)
assert length(authors) == 3
end
end
describe "build" do
test "it can build directly related records from the factory" do
assert {:ok, author} = insert(Author, build: [:posts])
assert [%Post{}] = author.posts
end
test "it can build indirectly directly related records from the factory" do
assert {:ok, post} = insert(Post, build: [author: :posts])
assert %Author{} = post.author
assert [other_post] = post.author.posts
assert other_post.id != post.id
end
end
describe "load" do
test "it can load related records at build time" do
assert {:ok, author} = insert(Author)
assert {:ok, post} = insert(Post, attrs: %{author_id: author.id}, load: [:author])
assert post.author.id == author.id
end
test "it can load calculations at build time" do
assert {:ok, post} = insert(Post, load: [:full_title])
assert post.full_title == "#{post.title}: #{post.sub_title}"
end
test "it can load aggregates at build time" do
assert {:ok, post} = insert(Post, build: [:author], load: [author: :count_of_posts])
assert post.author.count_of_posts == 1
end
end
describe "relate" do
test "it can relate records at build time" do
assert {:ok, author} = insert(Author)
assert {:ok, post} = insert(Post, relate: [author: author])
assert post.author.id == author.id
end
end
describe "variant" do
test "it can select a variant at build time" do
assert {:ok, author} = insert(Author, variant: :trek)
assert to_string(author.email) =~ ~r/\.(starfleet|rebellion)$/
end
end
test "it validates all options" do
assert {:error, error} = insert(Author, sss: 2)
message = Exception.message(error)
assert message =~ "unknown options [:sss]"
for key <- ~w[:variant :load :count :build :relate :attrs] do
assert message =~ key
end
end
end

View file

@ -13,9 +13,9 @@ defmodule Support.Factory do
attribute :email, fn
%{name: "JL"} -> "captain@entrepreneur.starfleet"
%{name: "Doc Holoday"} -> "cheifmed@voyager.starfleet"
%{name: "Dr Mark"} -> "cheifmed@voyager.starfleet"
%{name: "BLT"} -> "cheifeng@voyager.starfleet"
%{name: "Cal Hudson"} -> "cal@maquis.stfu"
%{name: "Cal Hudson"} -> "cal@maquis.rebellion"
end
end

View file

@ -38,7 +38,7 @@ defmodule Support.Post do
end
relationships do
belongs_to :author, Support.Author
belongs_to :author, Support.Author, attribute_writable?: true
end
actions do