chore/update-spark #8
13 changed files with 501 additions and 128 deletions
18
.drone.yml
18
.drone.yml
|
@ -198,6 +198,19 @@ steps:
|
|||
commands:
|
||||
- asdf mix spark.formatter --check
|
||||
|
||||
- name: mix spark.cheat_sheets
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
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
|
||||
depends_on:
|
||||
- mix compile
|
||||
commands:
|
||||
- asdf mix spark.cheat_sheets --check
|
||||
|
||||
- name: mix deps.unlock
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
environment:
|
||||
|
@ -241,8 +254,11 @@ steps:
|
|||
- name: mix git_ops.release
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
when:
|
||||
branch:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
depends_on:
|
||||
- mix test
|
||||
- mix credo
|
||||
|
|
153
documentation/dsls/DSL:-Smokestack.cheatmd
Normal file
153
documentation/dsls/DSL:-Smokestack.cheatmd
Normal file
|
@ -0,0 +1,153 @@
|
|||
# DSL: Smokestack.Dsl
|
||||
|
||||
The DSL definition for the Smokestack DSL.
|
||||
|
||||
<!--- ash-hq-hide-start --> <!--- -->
|
||||
|
||||
## DSL Documentation
|
||||
|
||||
### Index
|
||||
|
||||
* smokestack
|
||||
* factory
|
||||
* attribute
|
||||
|
||||
### Docs
|
||||
|
||||
## smokestack
|
||||
|
||||
|
||||
|
||||
* [factory](#module-factory)
|
||||
* attribute
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
* `:api` (`t:atom/0`) - The default Ash API to use when evaluating loads
|
||||
|
||||
|
||||
|
||||
### factory
|
||||
|
||||
Define factories for a resource
|
||||
|
||||
* attribute
|
||||
|
||||
|
||||
|
||||
* `:api` (`t:atom/0`) - The Ash API to use when evaluating loads
|
||||
|
||||
* `:resource` (`t:atom/0`) - Required. An Ash Resource
|
||||
|
||||
* `:variant` (`t:atom/0`) - The name of a factory variant The default value is `:default`.
|
||||
|
||||
|
||||
|
||||
##### attribute
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
* `:name` (`t:atom/0`) - Required. The name of the target attribute
|
||||
|
||||
* `:generator` - Required. A function which can generate an appropriate value for the attribute.œ
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--- ash-hq-hide-stop --> <!--- -->
|
||||
|
||||
|
||||
## smokestack
|
||||
|
||||
|
||||
### Nested DSLs
|
||||
* [factory](#smokestack-factory)
|
||||
* attribute
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Options
|
||||
| Name | Type | Default | Docs |
|
||||
| --- | --- | --- | --- |
|
||||
| `api` | `module` | | The default Ash API to use when evaluating loads |
|
||||
|
||||
|
||||
|
||||
## smokestack.factory
|
||||
```elixir
|
||||
factory resource, variant \ :default
|
||||
```
|
||||
|
||||
|
||||
Define factories for a resource
|
||||
|
||||
### Nested DSLs
|
||||
* [attribute](#smokestack-factory-attribute)
|
||||
|
||||
|
||||
|
||||
|
||||
### Arguments
|
||||
| Name | Type | Default | Docs |
|
||||
| --- | --- | --- | --- |
|
||||
| `resource`* | `module` | | An Ash Resource |
|
||||
| `variant` | `atom` | `:default` | The name of a factory variant |
|
||||
### Options
|
||||
| Name | Type | Default | Docs |
|
||||
| --- | --- | --- | --- |
|
||||
| `api` | `module` | | The Ash API to use when evaluating loads |
|
||||
|
||||
|
||||
## smokestack.factory.attribute
|
||||
```elixir
|
||||
attribute name, generator
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Arguments
|
||||
| Name | Type | Default | Docs |
|
||||
| --- | --- | --- | --- |
|
||||
| `name`* | `atom` | | The name of the target attribute |
|
||||
| `generator`* | `(-> any) \| mfa \| (any -> any) \| mfa \| (any, any -> any) \| mfa` | | A function which can generate an appropriate value for the attribute.œ |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Introspection
|
||||
|
||||
Target: `Smokestack.Dsl.Attribute`
|
||||
|
||||
|
||||
|
||||
|
||||
### Introspection
|
||||
|
||||
Target: `Smokestack.Dsl.Factory`
|
||||
|
||||
|
||||
|
||||
|
|
@ -2,8 +2,8 @@ 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)
|
||||
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:
|
||||
|
||||
```
|
||||
|
@ -28,7 +28,7 @@ defmodule Smokestack do
|
|||
|
||||
## Templates
|
||||
|
||||
Each attribute uses a template to generate a value when building a factory.
|
||||
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
|
||||
|
@ -37,7 +37,7 @@ defmodule Smokestack do
|
|||
## Variants
|
||||
|
||||
Sometimes you need to make slightly different factories to build a resource
|
||||
in a specific state for your test scenario.
|
||||
in a specific state for your test scenario.
|
||||
|
||||
Here's an example defining an alternate `:trek` variant for the character
|
||||
factory defined above:
|
||||
|
@ -57,7 +57,7 @@ defmodule Smokestack do
|
|||
|
||||
### Options
|
||||
|
||||
- `load`: an atom, list of atoms or keyword list of the same listing
|
||||
- `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
|
||||
|
@ -129,17 +129,21 @@ defmodule Smokestack do
|
|||
|
||||
Automatically implemented by modules which `use Smokestack`.
|
||||
|
||||
See `Smokestack.ParamBuilder.build/2` for more information.
|
||||
## Options
|
||||
|
||||
#{Builder.docs(ParamBuilder, nil)}
|
||||
"""
|
||||
@callback params(Resource.t(), [param_option]) ::
|
||||
{:ok, ParamBuilder.result()} | {:error, any}
|
||||
|
||||
@doc """
|
||||
Raising version of `params/2`.
|
||||
Raising version of `c:params/2`.
|
||||
|
||||
Automatically implemented by modules which `use Smokestack`.
|
||||
|
||||
See `Smokestack.ParamBuilder.build/3` for more information.
|
||||
## Options
|
||||
|
||||
#{Builder.docs(ParamBuilder, nil)}
|
||||
"""
|
||||
@callback params!(Resource.t(), [param_option]) :: ParamBuilder.result() | no_return
|
||||
|
||||
|
@ -148,17 +152,21 @@ defmodule Smokestack do
|
|||
|
||||
Automatically implemented by modules which `use Smokestack`.
|
||||
|
||||
See `Smokestack.RecordBuilder.build/3` for more information.
|
||||
## Options
|
||||
|
||||
#{Builder.docs(RecordBuilder, nil)}
|
||||
"""
|
||||
@callback insert(Resource.t(), [insert_option]) ::
|
||||
{:ok, RecordBuilder.result()} | {:error, any}
|
||||
|
||||
@doc """
|
||||
Raising version of `insert/4`.
|
||||
Raising version of `c:insert/2`.
|
||||
|
||||
Automatically implemented by modules which `use Smokestack`.
|
||||
|
||||
See `Smokestack.RecordBuilder.build/3` for more information.
|
||||
## Options
|
||||
|
||||
#{Builder.docs(RecordBuilder, nil)}
|
||||
"""
|
||||
@callback insert!(Resource.t(), [insert_option]) :: RecordBuilder.result() | no_return
|
||||
|
||||
|
@ -168,11 +176,20 @@ defmodule Smokestack 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.
|
||||
|
||||
See `c:Smokestack.build/2` for more information.
|
||||
## Options
|
||||
|
||||
#{Builder.docs(ParamBuilder, nil)}
|
||||
"""
|
||||
@spec params(Resource.t(), [Smokestack.param_option()]) ::
|
||||
{:ok, ParamBuilder.result()} | {:error, any}
|
||||
|
@ -187,7 +204,9 @@ defmodule Smokestack do
|
|||
@doc """
|
||||
Raising version of `params/2`.
|
||||
|
||||
See `c:Smokestack.build/3` for more information.
|
||||
## Options
|
||||
|
||||
#{Builder.docs(ParamBuilder, nil)}
|
||||
"""
|
||||
@spec params!(Resource.t(), [Smokestack.param_option()]) ::
|
||||
ParamBuilder.result() | no_return
|
||||
|
@ -201,7 +220,9 @@ defmodule Smokestack do
|
|||
@doc """
|
||||
Execute the matching factory and return an inserted Ash Resource record.
|
||||
|
||||
See `c:Smokestack.insert/2` for more information.
|
||||
## Options
|
||||
|
||||
#{Builder.docs(RecordBuilder, nil)}
|
||||
"""
|
||||
@spec insert(Resource.t(), [Smokestack.insert_option()]) ::
|
||||
{:ok, RecordBuilder.result()} | {:error, any}
|
||||
|
@ -216,7 +237,9 @@ defmodule Smokestack do
|
|||
@doc """
|
||||
Raising version of `insert/2`.
|
||||
|
||||
See `c:Smokestack.insert/2` for more information.
|
||||
## Options
|
||||
|
||||
#{Builder.docs(RecordBuilder, nil)}
|
||||
"""
|
||||
@spec insert!(Resource.t(), [Smokestack.insert_option()]) ::
|
||||
RecordBuilder.result() | no_return
|
||||
|
|
|
@ -18,7 +18,7 @@ defmodule Smokestack.Builder do
|
|||
@doc """
|
||||
Provide a schema for validating options.
|
||||
"""
|
||||
@callback option_schema(Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, any}
|
||||
@callback option_schema(nil | Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, any}
|
||||
|
||||
@doc """
|
||||
Given a builder and a factory, validate it's options and call the builder.
|
||||
|
@ -30,4 +30,13 @@ defmodule Smokestack.Builder do
|
|||
builder.build(factory, options)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate documentation for the available options.
|
||||
"""
|
||||
@spec docs(t, nil | Factory.t()) :: String.t()
|
||||
def docs(builder, factory) do
|
||||
{:ok, schema} = builder.option_schema(factory)
|
||||
OptionsHelpers.docs(schema)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,13 +42,17 @@ defmodule Smokestack.FactoryBuilder do
|
|||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec option_schema(Factory.t()) ::
|
||||
@spec option_schema(nil | Factory.t()) ::
|
||||
{:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
def option_schema(factory) do
|
||||
attr_keys =
|
||||
factory.resource
|
||||
|> Resource.Info.attributes()
|
||||
|> Enum.map(&{&1.name, [type: :any, required: false]})
|
||||
if factory do
|
||||
factory.resource
|
||||
|> Resource.Info.attributes()
|
||||
|> Enum.map(&{&1.name, [type: :any, required: false]})
|
||||
else
|
||||
[{:*, [type: :any, required: false]}]
|
||||
end
|
||||
|
||||
{:ok,
|
||||
[
|
||||
|
@ -56,7 +60,20 @@ defmodule Smokestack.FactoryBuilder do
|
|||
type: :map,
|
||||
required: false,
|
||||
default: %{},
|
||||
keys: attr_keys
|
||||
keys: attr_keys,
|
||||
doc: """
|
||||
Attribute overrides.
|
||||
|
||||
You can directly specify any overrides you would like set on the
|
||||
resulting record without running their normal generator.
|
||||
|
||||
For example:
|
||||
|
||||
```elixir
|
||||
post = params!(Post, attrs: %{title: "What's wrong with Huntly?"})
|
||||
assert post.title == "What's wrong with Huntly?"
|
||||
```
|
||||
"""
|
||||
]
|
||||
]}
|
||||
end
|
||||
|
|
|
@ -30,10 +30,32 @@ defmodule Smokestack.ManyBuilder do
|
|||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec option_schema(Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
@spec option_schema(nil | Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
def option_schema(factory) do
|
||||
with {:ok, schema} <- RelatedBuilder.option_schema(factory) do
|
||||
{:ok, Keyword.put(schema, :count, type: :pos_integer, required: true)}
|
||||
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory) do
|
||||
schema =
|
||||
[
|
||||
count: [
|
||||
type: :pos_integer,
|
||||
required: true,
|
||||
doc: """
|
||||
Specify the number of instances to build.
|
||||
|
||||
Use this option to run the factory a number of times and return the
|
||||
results as a list.
|
||||
|
||||
For example:
|
||||
|
||||
```elixir
|
||||
posts = params!(Post, count: 3)
|
||||
assert length(posts) == 3
|
||||
```
|
||||
"""
|
||||
]
|
||||
]
|
||||
|> OptionsHelpers.merge_schemas(related_schema, "Options for building relationships")
|
||||
|
||||
{:ok, schema}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -69,59 +69,117 @@ defmodule Smokestack.ParamBuilder do
|
|||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec option_schema(Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
@spec option_schema(nil | Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
def option_schema(factory) do
|
||||
with {:ok, schema0} <- RelatedBuilder.option_schema(factory),
|
||||
{:ok, schema1} <- ManyBuilder.option_schema(factory) do
|
||||
schema1 =
|
||||
Keyword.update!(schema1, :count, fn current ->
|
||||
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory),
|
||||
{:ok, many_schema} <- ManyBuilder.option_schema(factory) do
|
||||
many_schema =
|
||||
Keyword.update!(many_schema, :count, fn current ->
|
||||
current
|
||||
|> Keyword.update!(:type, &{:or, [&1, nil]})
|
||||
|> Keyword.put(:default, nil)
|
||||
|> Keyword.put(:required, false)
|
||||
end)
|
||||
|
||||
our_schema = [
|
||||
encode: [
|
||||
type: {:or, [nil, :module]},
|
||||
required: false,
|
||||
doc: """
|
||||
Provide an encoder module.
|
||||
|
||||
If provided the result will be passed to an `encode/1` function
|
||||
which should return an ok/error tuple with the encoded result.
|
||||
|
||||
This is primarily for use with `Jason` or `Poison`, however you may
|
||||
wish to define your own encoder for your tests.
|
||||
|
||||
For example:
|
||||
|
||||
```elixir
|
||||
post = params!(Post, encoder: Jason)
|
||||
assert Jason.decode!(post)
|
||||
```
|
||||
"""
|
||||
],
|
||||
nest: [
|
||||
type: {:or, [:atom, :string]},
|
||||
required: false,
|
||||
doc: """
|
||||
Nest the result within an arbitrary map key.
|
||||
|
||||
Mostly provided as a shorthand for building API requests where the
|
||||
built instance may need to be nested inside a key such as `data`.
|
||||
|
||||
Takes the result and nests it in a map under the provided key.
|
||||
|
||||
For example:
|
||||
|
||||
```elixir
|
||||
request = params!(Post, nest: :data)
|
||||
assert is_binary(request.data.title)
|
||||
```
|
||||
|
||||
If you need a more complicated mangling of the result, I suggest
|
||||
using the `encode` option.
|
||||
"""
|
||||
],
|
||||
key_case: [
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
{:tuple, [{:literal, :path}, :string]},
|
||||
{:in,
|
||||
[
|
||||
:camel,
|
||||
:constant,
|
||||
:dot,
|
||||
:header,
|
||||
:kebab,
|
||||
:name,
|
||||
:pascal,
|
||||
:path,
|
||||
:sentence,
|
||||
:snake,
|
||||
:title
|
||||
]}
|
||||
]},
|
||||
required: false,
|
||||
default: :snake,
|
||||
doc: """
|
||||
Change the key case.
|
||||
|
||||
Sometimes you will need to change the case of the keys before sending
|
||||
to an API. Behind the scenes we use [recase](https://hexdocs.pm/recase)
|
||||
to remap the keys as specified.
|
||||
|
||||
For example:
|
||||
|
||||
iex> params!(Post, key_case: :kebab) |> Map.keys()
|
||||
[:title, :"sub-title"]
|
||||
"""
|
||||
],
|
||||
key_type: [
|
||||
type: {:in, [:string, :atom]},
|
||||
required: false,
|
||||
default: :atom,
|
||||
doc: """
|
||||
Specify string or atom keys.
|
||||
|
||||
Allows you to specify the type of the returned keys.
|
||||
|
||||
For example:
|
||||
|
||||
iex> params!(Post, key_type: :string) |> Map.keys()
|
||||
["title", "sub_title"]
|
||||
"""
|
||||
]
|
||||
]
|
||||
|
||||
schema =
|
||||
schema0
|
||||
|> Keyword.merge(schema1)
|
||||
|> Keyword.merge(
|
||||
encode: [
|
||||
type: {:or, [nil, :module]},
|
||||
required: false
|
||||
],
|
||||
nest: [
|
||||
type: {:or, [:atom, :string]},
|
||||
required: false
|
||||
],
|
||||
key_case: [
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
{:tuple, [{:literal, :path}, :string]},
|
||||
{:in,
|
||||
[
|
||||
:camel,
|
||||
:constant,
|
||||
:dot,
|
||||
:header,
|
||||
:kebab,
|
||||
:name,
|
||||
:pascal,
|
||||
:path,
|
||||
:sentence,
|
||||
:snake,
|
||||
:title
|
||||
]}
|
||||
]},
|
||||
required: false,
|
||||
default: :snake
|
||||
],
|
||||
key_type: [
|
||||
type: {:in, [:string, :atom]},
|
||||
required: false,
|
||||
default: :atom
|
||||
]
|
||||
)
|
||||
our_schema
|
||||
|> OptionsHelpers.merge_schemas(many_schema, "Options for building multiple instances")
|
||||
|> OptionsHelpers.merge_schemas(related_schema, "Options for building relationships")
|
||||
|
||||
{:ok, schema}
|
||||
end
|
||||
|
|
|
@ -31,20 +31,35 @@ defmodule Smokestack.RecordBuilder do
|
|||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec option_schema(Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
@spec option_schema(nil | Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
def option_schema(factory) do
|
||||
with {:ok, schema0} <- RelatedBuilder.option_schema(factory),
|
||||
{:ok, schema1} <- ManyBuilder.option_schema(factory) do
|
||||
loadable_names =
|
||||
factory.resource
|
||||
|> Resource.Info.relationships()
|
||||
|> Enum.concat(Resource.Info.aggregates(factory.resource))
|
||||
|> Enum.concat(Resource.Info.calculations(factory.resource))
|
||||
|> Enum.map(& &1.name)
|
||||
|> Enum.uniq()
|
||||
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory),
|
||||
{:ok, many_schema} <- ManyBuilder.option_schema(factory) do
|
||||
load_type =
|
||||
if factory do
|
||||
loadable_names =
|
||||
factory.resource
|
||||
|> Resource.Info.relationships()
|
||||
|> Enum.concat(Resource.Info.aggregates(factory.resource))
|
||||
|> Enum.concat(Resource.Info.calculations(factory.resource))
|
||||
|> Enum.map(& &1.name)
|
||||
|> Enum.uniq()
|
||||
|
||||
schema1 =
|
||||
Keyword.update!(schema1, :count, fn current ->
|
||||
{:or,
|
||||
[
|
||||
{:wrap_list, {:in, loadable_names}},
|
||||
{:keyword_list,
|
||||
Enum.map(
|
||||
loadable_names,
|
||||
&{&1, type: {:or, [:atom, :keyword_list]}, required: false}
|
||||
)}
|
||||
]}
|
||||
else
|
||||
{:or, [{:wrap_list, :atom}, :keyword_list]}
|
||||
end
|
||||
|
||||
many_schema =
|
||||
Keyword.update!(many_schema, :count, fn current ->
|
||||
current
|
||||
|> Keyword.update!(:type, &{:or, [&1, nil]})
|
||||
|> Keyword.put(:default, nil)
|
||||
|
@ -54,22 +69,26 @@ defmodule Smokestack.RecordBuilder do
|
|||
schema =
|
||||
[
|
||||
load: [
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
{:wrap_list, {:in, loadable_names}},
|
||||
{:keyword_list,
|
||||
Enum.map(
|
||||
loadable_names,
|
||||
&{&1, type: {:or, [:atom, :keyword_list]}, required: false}
|
||||
)}
|
||||
]},
|
||||
type: load_type,
|
||||
required: false,
|
||||
default: []
|
||||
default: [],
|
||||
doc: """
|
||||
An optional Ash load statement.
|
||||
|
||||
You can request any calculations, aggregates or relationships you
|
||||
would like loaded on the returned record.
|
||||
|
||||
For example:
|
||||
|
||||
```elixir
|
||||
insert!(Post, load: [:full_title])
|
||||
assert is_binary(post.full_title)
|
||||
```
|
||||
"""
|
||||
]
|
||||
]
|
||||
|> Keyword.merge(schema0)
|
||||
|> Keyword.merge(schema1)
|
||||
|> OptionsHelpers.merge_schemas(many_schema, "Options for building multiple instances")
|
||||
|> OptionsHelpers.merge_schemas(related_schema, "Options for building relationships")
|
||||
|
||||
{:ok, schema}
|
||||
end
|
||||
|
|
|
@ -32,32 +32,72 @@ defmodule Smokestack.RelatedBuilder do
|
|||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec option_schema(Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
@spec option_schema(nil | Factory.t()) :: {:ok, OptionsHelpers.schema()} | {:error, error}
|
||||
def option_schema(factory) do
|
||||
with {:ok, factory_schema} <- FactoryBuilder.option_schema(factory) do
|
||||
relationship_names =
|
||||
factory.resource
|
||||
|> Resource.Info.relationships()
|
||||
|> Enum.map(& &1.name)
|
||||
build_type =
|
||||
if factory do
|
||||
relationship_names =
|
||||
factory.resource
|
||||
|> Resource.Info.relationships()
|
||||
|> Enum.map(& &1.name)
|
||||
|
||||
schema = [
|
||||
build: [
|
||||
type:
|
||||
{:or,
|
||||
[
|
||||
{:wrap_list, {:in, relationship_names}},
|
||||
{:keyword_list,
|
||||
Enum.map(
|
||||
relationship_names,
|
||||
&{&1, type: {:or, [:atom, :keyword_list]}, required: false}
|
||||
)}
|
||||
]},
|
||||
required: false,
|
||||
default: []
|
||||
{:or,
|
||||
[
|
||||
{:wrap_list, {:in, relationship_names}},
|
||||
{:keyword_list,
|
||||
Enum.map(
|
||||
relationship_names,
|
||||
&{&1, type: {:or, [:atom, :keyword_list]}, required: false}
|
||||
)}
|
||||
]}
|
||||
else
|
||||
{:or, [{:wrap_list, :atom}, :keyword_list]}
|
||||
end
|
||||
|
||||
schema =
|
||||
[
|
||||
build: [
|
||||
type: build_type,
|
||||
required: false,
|
||||
default: [],
|
||||
doc: """
|
||||
A (nested) list of relationships to build.
|
||||
|
||||
A (possibly nested) list of Ash resource relationships which is
|
||||
traversed building any instances as needed.
|
||||
|
||||
For example:
|
||||
|
||||
```elixir
|
||||
post = insert!(Post, build: Author)
|
||||
assert is_struct(post.author, Author)
|
||||
```
|
||||
|
||||
Caveats:
|
||||
- When building for a variant other than `:default` a matching
|
||||
variant factory will be looked for and used if present, otherwise
|
||||
it will build the default variant instead.
|
||||
|
||||
- Note that for relationships whose cardinality is "many" we only
|
||||
build one instance.
|
||||
|
||||
If these caveats are an issue, then you can build them yourself and
|
||||
pass them in using the `attrs` option.
|
||||
|
||||
For example:
|
||||
|
||||
```elixir
|
||||
posts = insert!(Post, count: 3)
|
||||
author = insert(Author, posts: posts)
|
||||
```
|
||||
|
||||
"""
|
||||
]
|
||||
]
|
||||
]
|
||||
|> OptionsHelpers.merge_schemas(factory_schema, "Options for building instances")
|
||||
|
||||
{:ok, Keyword.merge(factory_schema, schema)}
|
||||
{:ok, schema}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule Smokestack.Template.Constant do
|
|||
def init(constant), do: constant
|
||||
|
||||
def generate(constant, _, _) when is_function(constant.mapper, 1),
|
||||
do: constant.mapper(constant.value)
|
||||
do: constant.mapper.(constant.value)
|
||||
|
||||
def generate(constant, _, _),
|
||||
do: constant.value
|
||||
|
|
30
mix.exs
30
mix.exs
|
@ -23,18 +23,37 @@ defmodule Smokestack.MixProject do
|
|||
aliases: aliases(),
|
||||
dialyzer: [plt_add_apps: [:faker]],
|
||||
docs: [
|
||||
main: "Smokestack"
|
||||
main: "Smokestack",
|
||||
extra_section: "GUIDES",
|
||||
formatters: ["html"],
|
||||
filter_modules: ~r/^Elixir.Smokestack/,
|
||||
source_url_pattern:
|
||||
"https://code.harton.nz/james/smokestack/src/branch/main/%{path}#L%{line}",
|
||||
spark: [
|
||||
extensions: [
|
||||
%{
|
||||
module: Smokestack.Dsl,
|
||||
name: "Smokestack.Dsl",
|
||||
target: "Smokestack",
|
||||
type: "Smokestack"
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
def package do
|
||||
[
|
||||
name: :smokestack,
|
||||
files: ~w[lib .formatter.exs mix.exs README* LICENSE* CHANGELOG* documentation],
|
||||
maintainers: ["James Harton <james@harton.nz>"],
|
||||
licenses: ["HL3-FULL"],
|
||||
links: %{
|
||||
"Source" => "https://code.harton.nz/james/smokestack"
|
||||
}
|
||||
"Source" => "https://code.harton.nz/james/smokestack",
|
||||
"Github Mirror" => "https://github.com/jimsynz/smokestack"
|
||||
},
|
||||
source_url: "https://code.harton.nz/james/smokestack"
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -60,13 +79,14 @@ defmodule Smokestack.MixProject do
|
|||
{:git_ops, "~> 2.6", opts},
|
||||
{:mix_audit, "~> 2.1", opts},
|
||||
{:recase, "~> 0.7"},
|
||||
{:spark, "~> 1.1"}
|
||||
{:spark, "~> 1.1 and >= 1.1.39"}
|
||||
]
|
||||
end
|
||||
|
||||
defp aliases do
|
||||
[
|
||||
"spark.formatter": "spark.formatter --extensions=Smokestack.Dsl"
|
||||
"spark.formatter": "spark.formatter --extensions=Smokestack.Dsl",
|
||||
"spark.cheat_sheets": "spark.cheat_sheets --extensions=Smokestack.Dsl"
|
||||
]
|
||||
end
|
||||
|
||||
|
|
4
mix.lock
4
mix.lock
|
@ -27,8 +27,8 @@
|
|||
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
|
||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"},
|
||||
"sourceror": {:hex, :sourceror, "0.12.3", "a2ad3a1a4554b486d8a113ae7adad5646f938cad99bf8bfcef26dc0c88e8fade", [:mix], [], "hexpm", "4d4e78010ca046524e8194ffc4683422f34a96f6b82901abbb45acc79ace0316"},
|
||||
"spark": {:hex, :spark, "1.1.22", "68ba00f9acb4c8bc2c93ef82249493687ddf0f0a4f7e79c3c0e22b06719add56", [:mix], [{:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "b798b95990eed8f2409df47b818b5dbcd00e9b5c30d0355465d0b04bbf9b5c4c"},
|
||||
"sourceror": {:hex, :sourceror, "0.14.0", "b6b8552d0240400d66b6f107c1bab7ac1726e998efc797f178b7b517e928e314", [:mix], [], "hexpm", "809c71270ad48092d40bbe251a133e49ae229433ce103f762a2373b7a10a8d8b"},
|
||||
"spark": {:hex, :spark, "1.1.39", "f143b84a5b796bf2d83ec8fb4793ee9e66e67510c40d785f9a67050bb88e7677", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "d71bc26014c7e7abcdcf553f4cf7c5a5ff96f8365b1e20be3768ce503aafb203"},
|
||||
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
defmodule SmokestackTest do
|
||||
use ExUnit.Case
|
||||
doctest Smokestack
|
||||
end
|
Loading…
Reference in a new issue