Packages
ash
3.5.39
3.29.3
3.29.2
3.29.1
3.29.0
3.28.0
3.27.8
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.0
3.25.2
3.25.1
3.25.0
retired
3.24.7
3.24.6
3.24.5
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.1
3.23.0
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.0
3.19.3
3.19.2
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.0
3.15.0
3.14.1
3.14.0
retired
3.13.2
3.13.1
3.13.0
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.1
3.10.0
3.9.0
3.8.0
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
retired
3.6.3
retired
3.6.2
3.6.1
3.6.0
3.5.43
3.5.42
3.5.41
3.5.40
3.5.39
3.5.38
3.5.37
3.5.36
3.5.35
3.5.34
3.5.33
3.5.32
3.5.31
3.5.30
3.5.29
3.5.28
3.5.27
3.5.26
3.5.25
3.5.24
3.5.23
3.5.22
3.5.21
3.5.20
3.5.19
3.5.18
3.5.17
3.5.16
3.5.15
3.5.14
3.5.13
3.5.12
3.5.11
3.5.10
3.5.9
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.4.74
retired
3.4.73
3.4.72
3.4.71
3.4.70
3.4.69
3.4.68
3.4.67
3.4.66
3.4.65
3.4.64
3.4.63
3.4.62
3.4.61
3.4.60
3.4.59
3.4.58
3.4.57
3.4.56
3.4.55
3.4.54
3.4.53
3.4.52
3.4.51
3.4.50
3.4.49
3.4.48
3.4.47
3.4.46
3.4.45
3.4.44
3.4.43
3.4.42
3.4.41
3.4.40
3.4.39
3.4.38
3.4.37
3.4.36
3.4.35
3.4.34
3.4.33
3.4.32
3.4.31
3.4.30
3.4.29
3.4.28
3.4.27
3.4.26
3.4.25
3.4.24
3.4.23
3.4.22
3.4.21
3.4.20
3.4.19
3.4.18
3.4.17
3.4.16
3.4.15
3.4.14
3.4.13
3.4.12
3.4.11
3.4.10
3.4.9
3.4.8
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.3
3.3.2
3.3.1
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.16
3.0.15
3.0.14
3.0.13
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.46
3.0.0-rc.45
3.0.0-rc.44
3.0.0-rc.43
3.0.0-rc.42
3.0.0-rc.41
3.0.0-rc.40
3.0.0-rc.39
3.0.0-rc.38
3.0.0-rc.37
3.0.0-rc.36
3.0.0-rc.35
3.0.0-rc.34
3.0.0-rc.33
3.0.0-rc.32
3.0.0-rc.31
3.0.0-rc.29
3.0.0-rc.28
3.0.0-rc.27
3.0.0-rc.26
3.0.0-rc.25
3.0.0-rc.24
3.0.0-rc.23
3.0.0-rc.22
3.0.0-rc.21
3.0.0-rc.20
3.0.0-rc.19
3.0.0-rc.18
3.0.0-rc.17
3.0.0-rc.16
3.0.0-rc.15
3.0.0-rc.14
3.0.0-rc.13
3.0.0-rc.12
3.0.0-rc.11
3.0.0-rc.10
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.1
3.0.0-rc.0
2.21.15
2.21.14
2.21.13
2.21.12
2.21.11
2.21.10
2.21.9
2.21.8
2.21.7
2.21.6
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.14
2.19.13
2.19.12
2.19.11
2.19.10
2.19.9
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
retired
2.19.2
retired
2.19.1
retired
2.19.0
retired
2.18.2
2.18.1
2.18.0
2.17.24
2.17.23
2.17.22
2.17.21
2.17.20
2.17.19
2.17.18
2.17.17
2.17.16
2.17.15
2.17.14
2.17.13
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.1
2.16.0
2.15.20
2.15.19
2.15.18
2.15.17
2.15.16
2.15.15
2.15.14
2.15.13
2.15.12
2.15.11
2.15.10
2.15.9
2.15.8
2.15.7
2.15.6
2.15.5
2.15.4
2.15.2
2.15.1
2.15.0
2.14.21
2.14.20
2.14.19
2.14.18
2.14.17
2.14.16
2.14.15
2.14.14
2.14.13
2.14.12
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.5
2.14.4
2.14.3
2.14.2
2.14.1
2.14.0
2.13.4
retired
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.11
2.11.10
2.11.9
2.11.8
2.11.7
2.11.6
2.11.5
2.11.4
2.11.3
2.11.2
2.11.1
2.11.0
2.11.0-rc.3
2.11.0-rc.2
2.11.0-rc.1
2.11.0-rc.0
2.10.2
2.10.1
2.10.0
2.9.29
2.9.28
2.9.27
2.9.26
2.9.25
2.9.24
2.9.23
2.9.22
2.9.21
2.9.20
2.9.19
2.9.18
2.9.17
2.9.16
2.9.15
2.9.14
2.9.13
2.9.12
2.9.11
2.9.10
2.9.9
2.9.8
2.9.7
2.9.6
2.9.5
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.1
2.8.0
2.7.1
2.7.0
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.5.0-rc.6
2.5.0-rc.5
2.5.0-rc.4
2.5.0-rc.3
2.5.0-rc.2
2.5.0-rc.1
2.5.0-rc.0
2.4.30
2.4.29
2.4.28
2.4.27
2.4.26
2.4.25
2.4.24
2.4.23
2.4.22
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-pre.8
2.0.0-pre.7
2.0.0-pre.6
2.0.0-pre.5
2.0.0-pre.4
2.0.0-pre.3
2.0.0-pre.2
2.0.0-pre.1
2.0.0-pre.0
1.53.3
1.53.2
1.53.0
1.52.0-rc.22
1.52.0-rc.21
1.52.0-rc.20
1.52.0-rc.19
1.52.0-rc.18
1.52.0-rc.17
1.52.0-rc.16
1.52.0-rc.15
1.52.0-rc.14
1.52.0-rc.13
1.52.0-rc.12
1.52.0-rc.11
1.52.0-rc.10
1.52.0-rc.9
1.52.0-rc.8
1.52.0-rc.7
1.52.0-rc.6
1.52.0-rc.5
1.52.0-rc.4
1.52.0-rc.3
1.52.0-rc.2
1.52.0-rc.1
1.52.0-rc.0
1.51.2
1.51.1
retired
1.51.0
1.50.21
1.50.20
1.50.19
1.50.18
1.50.17
1.50.16
1.50.15
1.50.14
1.50.13
1.50.12
1.50.11
1.50.10
1.50.9
1.50.8
1.50.7
1.50.6
1.50.5
1.50.4
1.50.3
1.50.2
1.50.1
1.50.0
1.49.0
1.48.0-rc.30
1.48.0-rc.29
1.48.0-rc.28
1.48.0-rc.27
1.48.0-rc.26
1.48.0-rc.25
1.48.0-rc.24
1.48.0-rc.23
1.48.0-rc.22
1.48.0-rc.21
1.48.0-rc.20
1.48.0-rc.19
1.48.0-rc.18
1.48.0-rc.17
1.48.0-rc.16
1.48.0-rc.15
1.48.0-rc.14
1.48.0-rc.13
1.48.0-rc.12
1.48.0-rc.11
1.48.0-rc.10
1.48.0-rc.9
1.48.0-rc.8
1.48.0-rc.7
1.48.0-rc.6
1.48.0-rc.5
1.48.0-rc.4
1.48.0-rc.3
1.48.0-rc.2
1.48.0-rc.1
1.48.0-rc.0
1.47.12
1.47.11
1.47.10
1.47.9
1.47.8
1.47.7
1.47.6
1.47.5
1.47.4
1.47.3
1.47.2
1.47.1
1.47.0
1.46.13
1.46.12
1.46.11
1.46.10
1.46.9
1.46.8
1.46.7
1.46.6
1.46.5
1.46.4
1.46.3
1.46.2
1.46.1
1.46.0
1.45.0-rc9
1.45.0-rc8
1.45.0-rc7
1.45.0-rc6
1.45.0-rc5
1.45.0-rc4
1.45.0-rc3
1.45.0-rc20
1.45.0-rc2
1.45.0-rc19
1.45.0-rc18
1.45.0-rc17
1.45.0-rc16
1.45.0-rc15
1.45.0-rc14
1.45.0-rc13
1.45.0-rc12
1.45.0-rc11
1.45.0-rc10
1.45.0-rc1
1.45.0-rc0
1.44.13
1.44.12
1.44.11
1.44.10
1.44.9
1.44.8
1.44.7
1.44.6
1.44.5
1.44.4
1.44.3
1.44.2
1.44.1
1.44.0
1.43.12
1.43.11
1.43.10
1.43.9
1.43.8
1.43.7
1.43.6
1.43.5
1.43.4
1.43.3
1.43.2
1.43.1
1.43.0
1.42.0
1.41.12
1.41.11
1.41.10
1.41.9
1.41.8
1.41.7
1.41.6
1.41.5
1.41.4
1.41.3
1.41.2
1.41.1
1.41.0
1.40.0
1.39.7
1.39.6
1.39.5
1.39.4
1.39.3
1.39.2
1.39.1
1.39.0
1.38.0
1.37.2
1.37.1
1.37.0
1.36.22
1.36.21
1.36.19
1.36.18
1.36.17
1.36.16
1.36.15
1.36.14
1.36.13
1.36.12
1.36.11
1.36.10
1.36.9
1.36.8
1.36.7
1.36.6
1.36.5
1.36.4
1.36.3
1.36.2
1.36.0
1.35.1
1.35.0
1.34.9
1.34.8
1.34.7
1.34.6
1.34.5
1.34.4
1.34.3
1.34.2
1.34.1
1.34.0
1.33.0
1.32.2
1.32.1
1.32.0
1.31.1
1.31.0
1.30.2
1.30.1
1.29.0-rc1
1.29.0-rc0
1.28.1
1.28.0
1.27.1
1.27.0
1.26.13
1.26.12
1.26.11
1.26.10
1.26.9
1.26.8
1.26.7
1.26.6
1.26.5
1.26.4
1.26.2
1.26.1
1.26.0
1.25.8
1.25.7
1.25.6
1.25.5
1.25.4
1.25.3
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
1.23.3
1.23.2
1.23.1
1.23.0
1.22.1
1.22.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.1
1.18.0
1.17.1
1.17.0
1.16.2
1.15.1
1.15.0
1.14.0
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.13.1
0.13.0
0.12.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.9
0.1.8
0.1.3
0.1.1
0.1.0
A declarative, extensible framework for building Elixir applications.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash/generator/generator.ex
defmodule Ash.Generator do
@moduledoc """
Tools for generating input to Ash resource actions and for generating seed data.
## Using Ash.Generator
To define generators for your tests, `use Ash.Generator`, and define
functions that use `changeset_generator/3` and/or `seed_generator/2`.
```elixir
defmodule YourApp.Generator do
use Ash.Generator
# using `seed_generator`, bypasses the action and saves directly to the data layer
def blog_post(opts \\\\ []) do
seed_generator(
%MyApp.Blog.Post{
name: sequence(:title, &"My Blog Post \#{&1}")
text: StreamData.repeatedly(fn -> Faker.Lorem.paragraph() end)
},
overrides: opts
)
end
# using `changeset_generator`, calls the action when passed to `generate`
def blog_post_comment(opts \\\\ []) do
blog_post_id = opts[:blog_post_id] || once(:default_blog_post_id, fn -> generate(blog_post()).id end)
changeset_generator(
MyApp.Blog.Comment,
:create,
defaults: [
blog_post_id: blog_post_id
],
overrides: opts
)
end
end
```
Then, in your tests, you can `import YourApp.Generator`, and use `generate/1` and `generate_many/1` to generate data.
For example:
```elixir
import YourApp.Generator
test "`comment_count` on blog_post shows the count of comments" do
blog_post = generate(blog_post())
assert Ash.load!(blog_post, :comment_count).comment_count == 0
generate_many(blog_post_comment(blog_post_id: blog_post.id), 10)
assert Ash.load!(blog_post, :comment_count).comment_count == 10
end
```
## About Generators
These generators are backed by `StreamData`, and are ready for use with property testing via `ExUnitProperties`
Many functions in this module support "overrides", which allow passing down either constant values
or your own `StreamData` generators.
For example:
```elixir
# All generated posts will have text as `"text"`. Equivalent to providing `StreamData.constant("text")`.
Ash.Generator.seed_input(Post, %{text: "text"})
```
"""
@dialyzer {:nowarn_function,
seed_input: 2,
seed_input: 1,
seed!: 1,
seed!: 2,
seed_many!: 2,
seed_many!: 3,
action_input: 2,
action_input: 3,
changeset: 2,
changeset: 3,
changeset: 4,
do_mixed_map: 1,
query: 2,
query: 3,
query: 4,
seed_generator: 1,
seed_generator: 2,
changeset_generator: 2,
changeset_generator: 3,
generate_attributes: 5,
mixed_map: 2,
many_changesets: 3,
many_changesets: 4,
many_changesets: 5,
many_queries: 3,
many_queries: 4,
many_queries: 5,
do_changeset_or_query: 5}
@typedoc """
A map or keyword of data generators or constant values to use in place of defaults.
Many functions in `Ash.Generator` support `overrides`, allowing to customize the default
generated values.
"""
@type overrides ::
%{term() => stream_data() | term()} | Keyword.t(stream_data() | term())
@typedoc """
An instance of `StreamData`, gotten from one of the functions in that module.
"""
@type stream_data :: Enumerable.t()
defmacro __using__(_opts) do
quote do
import Ash.Generator, except: [generate: 1, generate_many: 2]
defdelegate generate(generator), to: Ash.Generator
defdelegate generate_many(generator, count), to: Ash.Generator
end
end
@doc """
A generator of changesets which call their specific actions when passed to `generate/1` or `generate_many/2`.
See `seed_generator/2` for the equivalent construct for cases when you want to seed directly to the data layer as opposed to calling resource
actions.
## Examples
```elixir
iex> changeset_generator(MyApp.Blog.Post, :create, defaults: [title: sequence(:blog_post_title, &"My Blog Post \#{&1}")]) |> generate()
%Ash.Changeset{...}
```
## Usage in tests
This can be used to define generators in tests. A useful pattern is defining a function like so:
```elixir
def blog_post(opts \\ []) do
changeset_generator(
MyApp.Blog.Post,
:create,
defaults: [
name: sequence(:blog_post_title, &"My Blog Post \#{&1}")
text: StreamData.repeatedly(fn -> Faker.Lorem.paragraph() end)
],
overrides: opts
)
end
```
When you only allow child resource to be created through a managed relationship, e.g. an update action on a parent resource,
this pattern could be expanded, yielding a resource with a new child resource:
```elixir
def post_for(author, opts \\ []) do
changeset_generator(
author,
:new_post,
uses: [
post_input:
action_input(
MyApp.Blog.Post,
:create,
title: sequence(:title, &"Post \#{&1}")
)
],
defaults: fn inputs ->
[posts: [inputs.post_input]]
end,
overrides: opts
)
end
```
See the `Ash.Generator` moduledocs for more information.
## Options
* `:defaults` - A keyword list of values or generators, used as inputs. Can also be a function
when using the `:uses` option.
* `:overrides` - A keyword list or map of `t:overrides()`
* `:actor` - Passed through to the changeset
* `:tenant` - Passed through to the changeset
* `:scope` - Passed through to the changeset
* `:uses` - A map of generators that are passed into your `defaults`. `defaults` must be a
function. This is useful when multiple things in your `defaults` need to use the same generated
value.
* `:authorize?` - Passed through to the changeset
* `:context` - Passed through to the changeset
* `:after_action` - A one argument function that takes the result and returns
a new result to run after the record is created.
* `:private_arguments` - A map of private arguments, whose values can also be generators. Can also
be a function when using the `:uses` option.
## The `uses` option
```elixir
def blog_post(opts \\ []) do
changeset_generator(
MyApp.Blog.Post,
:create,
uses: [
author: author() # A function using `changeset_generator` just like this one.
],
defaults: fn %{author: author} ->
author = generate(author)
[
name: sequence(:blog_post_title, &"My Blog Post \#{&1}")
author_name: author.name,
text: StreamData.repeatedly(fn -> Faker.Lorem.paragraph() end)
]
end
overrides: opts
)
end
```
"""
def changeset_generator(resource, action, opts \\ []) do
generator =
if opts[:uses] do
if not (is_nil(opts[:defaults]) || is_function(opts[:defaults])) do
raise ArgumentError,
"The `uses` option can only be provided if the `defaults` option is a function"
end
if not (is_nil(opts[:private_arguments]) || is_function(opts[:private_arguments])) do
raise ArgumentError,
"The `uses` option can only be provided if the `private_arguments` option is a function"
end
opts[:uses]
|> to_generators()
|> StreamData.fixed_map()
|> StreamData.bind(fn uses ->
generators =
if opts[:defaults] do
opts[:defaults].(uses)
|> Map.new()
else
%{}
end
|> Map.merge(Map.new(opts[:overrides] || %{}))
private_arguments =
if opts[:private_arguments] do
opts[:private_arguments].(uses)
|> to_generators()
|> StreamData.fixed_map()
else
StreamData.fixed_map(%{})
end
changeset_opts =
StreamData.fixed_map(
Map.put(
to_generators(
Keyword.take(opts, [
:actor,
:tenant,
:scope,
:authorize?,
:context,
:upsert?,
:upsert_identity
])
),
:private_arguments,
private_arguments
)
)
StreamData.fixed_map(%{
changeset_opts: changeset_opts,
input: action_input(resource, action, generators)
})
end)
else
if is_function(opts[:defaults]) do
raise ArgumentError,
"The `uses` option must be provided if the `defaults` option is a function"
end
generators =
opts[:defaults]
|> Kernel.||([])
|> Map.new()
|> Map.merge(Map.new(opts[:overrides] || %{}))
input = action_input(resource, action, generators)
private_arguments =
if opts[:private_arguments] do
StreamData.fixed_map(to_generators(opts[:private_arguments]))
else
StreamData.fixed_map(%{})
end
changeset_opts =
StreamData.fixed_map(
Map.put(
to_generators(
Keyword.take(opts, [
:actor,
:tenant,
:scope,
:authorize?,
:context,
:upsert?,
:upsert_identity
])
),
:private_arguments,
private_arguments
)
)
StreamData.fixed_map(%{
changeset_opts: changeset_opts,
input: input
})
end
generator
|> StreamData.map(fn %{input: input, changeset_opts: changeset_opts} ->
Ash.Changeset.for_action(
resource,
action,
input,
Map.to_list(changeset_opts)
)
|> then(fn changeset ->
if opts[:after_action] do
Ash.Changeset.after_action(changeset, fn _changeset, record ->
{:ok, opts[:after_action].(record)}
end)
|> Ash.Changeset.set_context(%{private: %{generator_after_action: opts[:after_action]}})
else
changeset
end
end)
end)
end
@doc """
A generator of seedable records, to be passed to `generate/1` or `generate_many/1`
See `changeset_generator/3` for the equivalent construct for cases when you want to call resource
actions as opposed to seed directly to the data layer.
When a struct is given, only exactly the given values/generators will be used. If you
pass a tuple, i.e `{Resource, %{field: :value}}`, all values not provided will be generated
automatically.
## Examples
```elixir
iex> seed_generator(%MyApp.Blog.Post{name: sequence(:blog_post_title, &"My Blog Post \#{&1}")}) |> generate()
%Tunez.Music.Artist{name: "Artist 1"}
iex> seed_generator({MyApp.Blog.Post, %{}}) |> generate()
%Tunez.Music.Artist{name: "A random name"}
```
## Usage in tests
This can be used to define seed generators in tests. A useful pattern is defining a function like so:
```elixir
def blog_post(opts \\ []) do
seed_generator(
%MyApp.Blog.Post{
name: sequence(:blog_post_title, &"My Blog Post \#{&1}")
text: StreamData.repeatedly(fn -> Faker.Lorem.paragraph() end)
},
overrides: opts
)
end
```
See the `Ash.Generator` moduledocs for more information.
## Options
* `:overrides` - A keyword list or map of `t:overrides()`
* `:actor` - Passed through to the changeset
* `:tenant` - Passed through to the changeset
* `:scope` - Passed through to the changeset
* `:uses` - A map of generators that are passed into the first argument, if it is a function.
* `:authorize?` - Passed through to the changeset
* `:context` - Passed through to the changeset
* `:after_action` - A one argument function that takes the result and returns
a new result to run after the record is created.
"""
@spec seed_generator(
Ash.Resource.record()
| {Ash.Resource.t(), map()}
| (map -> Ash.Resource.record() | {Ash.Resource.t(), %{}}),
opts :: Keyword.t()
) :: stream_data()
def seed_generator(record, opts \\ []) do
if opts[:uses] do
if !is_function(record) do
raise ArgumentError, "The `uses` option can only be provided if `record` is a function"
end
opts[:uses]
|> to_generators()
|> StreamData.fixed_map()
|> StreamData.bind(fn uses ->
resource_or_record =
record.(uses)
resource =
case resource_or_record do
{resource, _record} -> resource
%resource{} -> resource
end
resource_or_record
|> case do
%_{} = record ->
record
|> Map.take(Enum.to_list(Ash.Resource.Info.attribute_names(resource)))
|> Enum.reduce(%{}, fn {key, value}, acc ->
attribute = Ash.Resource.Info.attribute(resource, key)
if !attribute.writable? && attribute.generated? do
acc
else
Map.put(acc, key, value)
end
end)
|> Map.merge(Map.new(opts[:overrides] || %{}))
|> to_generators()
|> Map.put(:__will_be_struct__, resource)
|> StreamData.fixed_map()
{resource, attributes} ->
resource
|> Ash.Resource.Info.attributes()
|> Enum.reject(&(!&1.writable? && &1.generated?))
|> generate_attributes(
to_generators(
Map.put(
Map.merge(attributes, Map.new(opts[:overrides] || %{})),
:__will_be_struct__,
resource
)
),
false,
:create,
[]
)
end
end)
|> StreamData.map(fn keys ->
Ash.Resource.set_metadata(
struct(keys.__will_be_struct__, keys),
opts
|> Keyword.take([:tenant])
|> Enum.into(%{private: %{generator_after_action: opts[:after_action]}})
)
end)
else
if is_function(record) do
raise ArgumentError, "The `uses` option must be provided if `record` is a function"
end
resource =
case record do
{resource, _record} -> resource
%resource{} -> resource
end
record
|> case do
%_{} = record ->
record
|> Map.take(Enum.to_list(Ash.Resource.Info.attribute_names(resource)))
|> Enum.reduce(%{}, fn {key, value}, acc ->
attribute = Ash.Resource.Info.attribute(resource, key)
if !attribute.writable? && attribute.generated? do
acc
else
Map.put(acc, key, value)
end
end)
|> Map.merge(Map.new(opts[:overrides] || %{}))
|> to_generators()
|> StreamData.fixed_map()
{_resource, attributes} ->
resource
|> Ash.Resource.Info.attributes()
|> Enum.reject(&(!&1.writable? && &1.generated?))
|> generate_attributes(
to_generators(Map.merge(attributes, Map.new(opts[:overrides] || %{}))),
false,
:create,
[]
)
end
|> StreamData.map(fn keys ->
Ash.Resource.set_metadata(
struct(resource, keys),
opts
|> Keyword.take([:tenant])
|> Enum.into(%{private: %{generator_after_action: opts[:after_action]}})
)
end)
end
end
@doc """
Generate globally unique values.
This is useful for generating values that are unique within a given test or processes that it spawns, such as email addresses,
or for generating values that are unique across a single resource, such as identifiers. The values will be unique
for anything using the same sequence name, **within the same test**.
> ### Not Globally Unique {: .warning}
> The lifecycle of this generator is tied to the process that initially starts it. In general,
> that will be the test. In the rare case where you are running async processes that need to share a sequence
> that is not created in the test process, you can initialize a sequence in the test using `initialize_sequence/1`.
>
> If you need a globally unique value, use a value like `System.unique_integer([:positive])` in your values instead.
>
> For example:
>
> ```elixir
> StreamData.repeatedly(fn -> "email\#{System.unique_integer([:positive])}@example.com" end)
> ```
Example:
Ash.Generator.sequence(:unique_email, fn i -> "user\#\{i\}@example.com" end) |> Enum.take(3)
iex> ["user0@example.com", "user1@example.com", "user2@example.com"]
## Using a different sequencer
By default we use an incrementing integer starting at 0. However, if you want to use something else, you can provide
your own sequencer. The initial value will be `nil`, which you can use to detect that you are the start of the sequence.
Example:
Ash.Generator.sequence(:unique_email, fn i -> "user\#\{i\}@example.com" end, fn num -> (num || 1) - 1 end) |> Enum.take(3)
iex> ["user0@example.com", "user-1@example.com", "user-2@example.com"]
"""
@spec sequence(pid | atom, (iterator | nil -> value), (iterator | nil -> iterator)) ::
StreamData.t(value)
when iterator: term, value: term
def sequence(identifier, generator, sequencer \\ fn i -> (i || -1) + 1 end) do
pid =
if is_pid(identifier) do
identifier
else
initialize_sequence(identifier)
end
StreamData.repeatedly(fn ->
Agent.get_and_update(pid, fn state ->
next_in_sequence = sequencer.(state)
value = generator.(next_in_sequence)
{value, next_in_sequence}
end)
end)
end
@doc """
Run the provided function or enumerable (i.e generator) only once.
This is useful for ensuring that some piece of data is generated a single time during a test.
The lifecycle of this generator is tied to the process that initially starts it. In general,
that will be the test. In the rare case where you are running async processes that need to share a sequence
that is not created in the test process, you can initialize a sequence in the test using `initialize_once/1`.
Example:
iex> Ash.Generator.once(:user, fn ->
register_user(...)
end) |> Enum.at(0)
%User{id: 1} # created the user
iex> Ash.Generator.once(:user, fn ->
register_user(...)
end) |> Enum.at(0)
%User{id: 1} # reused the last user
"""
@spec once(pid | term, (-> value) | Enumerable.t(value)) ::
StreamData.t(value)
when value: term
def once(identifier, generator) do
pid =
if is_pid(identifier) do
identifier
else
initialize_once(identifier)
end
StreamData.repeatedly(fn ->
generate_once(pid, generator)
end)
end
defp generate_once(pid, generator) do
Agent.get_and_update(pid, fn state ->
case state do
:locked ->
{:locked, :locked}
:none ->
{{:generate, generator}, :locked}
{:some, value} ->
{{:value, value}, {:some, value}}
end
end)
|> case do
:locked ->
generate_once(pid, generator)
{:generate, generator} ->
new =
case generator do
generator when is_function(generator) ->
generator.()
value ->
Enum.at(value, 0)
end
Agent.update(pid, fn _state ->
{:some, new}
end)
new
{:value, value} ->
value
end
end
@doc """
Starts and links an agent for a `once/2`, or returns the existing agent pid if it already exists.
See `once/2` for more.
"""
# sobelow_skip ["DOS.BinToAtom"]
@spec initialize_once(term()) :: pid
def initialize_once(identifier) do
case find_in_self_or_ancestor({:ash_once, identifier}) do
nil ->
{:ok, pid} = Agent.start_link(fn -> :none end)
Process.put({:ash_once, identifier}, pid)
pid
pid ->
pid
end
end
@doc """
Stops the agent for a `once/2`.
See `once/2` for more.
"""
def stop_once(identifier) do
Agent.stop(identifier)
:ok
end
@doc """
Starts and links an agent for a sequence, or returns the existing agent pid if it already exists.
See `sequence/3` for more.
"""
@spec initialize_sequence(atom) :: pid
# sobelow_skip ["DOS.BinToAtom"]
def initialize_sequence(identifier) do
case find_in_self_or_ancestor({:ash_sequence, identifier}) do
nil ->
{:ok, pid} = Agent.start_link(fn -> nil end)
Process.put({:ash_sequence, identifier}, pid)
pid
pid ->
pid
end
end
defp find_in_self_or_ancestor(key) do
Enum.find_value([self()] ++ (Process.get(:"$ancestors", []) || []), fn pid ->
pid
|> Process.info(:dictionary)
|> elem(1)
|> Enum.find_value(fn {found_key, value} ->
if found_key == key do
value
end
end)
end)
end
@doc """
Stops the agent for a sequence.
See `sequence/3` for more.
"""
def stop_sequence(identifier) do
Agent.stop(identifier)
:ok
end
@doc """
Gets input using `seed_input/2` and passes it to `Ash.Seed.seed!/2`, returning the result
"""
def seed!(resource, generators \\ %{}) do
input =
seed_input(resource, generators)
|> Enum.at(0)
Ash.Seed.seed!(resource, input)
end
@doc """
Generates an input `n` times, and passes them all to seed, returning the list of seeded items.
"""
def seed_many!(resource, n, generators \\ %{}) do
seed_input(resource, generators)
|> Stream.take(n)
|> Enum.map(&Ash.Seed.seed!(resource, &1))
end
@doc """
Takes one value from a changeset or seed generator and calls `Ash.create!` or `Ash.update!` on it.
Passes through resource structs without doing anything.
Creates a changeset if given
"""
@spec generate(
stream_data()
| Ash.Changeset.t()
| Ash.Resource.record()
) ::
Ash.Resource.record()
def generate(%Ash.Changeset{action_type: :create} = changeset) do
Ash.create!(changeset)
end
def generate(%Ash.Changeset{action_type: :update} = changeset) do
Ash.update!(changeset)
end
def generate(changeset_generator) do
if is_struct(changeset_generator) and
Ash.Resource.Info.resource?(changeset_generator.__struct__) do
if changeset_generator.__meta__.state == :built do
result = Ash.Seed.seed!(changeset_generator)
case changeset_generator.__metadata__[:private][:generator_after_action] do
nil ->
result
after_action ->
after_action.(result)
end
else
changeset_generator
end
else
changeset_generator |> Enum.at(0) |> generate()
end
end
@doc """
Takes `count` values from a changeset or seed generator and passes their inputs into `Ash.bulk_create!` or `Ash.Seed.seed!` respectively.
"""
def generate_many(changeset_generator, count) do
changeset_generator
|> Stream.take(count)
|> Stream.chunk_every(500)
|> Enum.flat_map(fn
[%Ash.Changeset{} = first | _rest] = batch ->
after_action = first.context[:private][:generator_after_action]
opts = [
return_records?: true,
return_errors?: true,
notify?: true,
sorted?: true,
max_concurrency: 0,
stop_on_error?: true,
actor: first.context[:private][:actor],
authorize?: first.context[:private][:authorize?],
tenant: first.tenant,
tracer: first.context[:private][:tracer]
]
opts =
if after_action do
Keyword.put(opts, :after_action, fn _changeset, record ->
{:ok, after_action.(record)}
end)
else
opts
end
result =
Ash.bulk_create!(Enum.map(batch, & &1.params), first.resource, first.action.name, opts)
if result.status != :success do
raise Ash.Error.to_error_class(result.errors)
end
result.records || []
batch ->
Enum.map(batch, &generate/1)
end)
end
@doc """
Generate input meant to be passed into a resource action.
Arguments that are passed to a `manage_relationship` are not generated by default, and you will
have to generate them yourself by passing your own generators/values down. See the module documentation for more.
"""
@spec action_input(
Ash.Resource.t() | Ash.Resource.record(),
action_name :: atom,
generators :: overrides()
) :: map()
def action_input(resource_or_record, action_name, generators \\ %{}) do
resource =
case resource_or_record do
%resource{} -> resource
resource -> resource
end
action = Ash.Resource.Info.action(resource, action_name)
if !action do
raise ArgumentError,
"Invalid action #{inspect(resource)}.#{action_name}"
end
arguments = Enum.reject(action.arguments, &find_manage_change(&1, action))
resource
|> Ash.Resource.Info.attributes()
|> Enum.filter(&(&1.name in Map.get(action, :accept, [])))
|> set_allow_nil(action)
|> Enum.concat(arguments)
|> generate_attributes(generators, false, action.type, Enum.map(action.arguments, & &1.name))
end
@doc """
Creates the input for the provided action with `action_input/3`, and creates a changeset for that action with that input.
See `action_input/3` and the module documentation for more.
"""
@spec changeset(
Ash.Resource.t(),
action :: atom(),
overrides(),
changeset_options :: Keyword.t()
) :: Ash.Changeset.t()
def changeset(resource_or_record, action, generators \\ %{}, changeset_options \\ []) do
resource =
case resource_or_record do
%resource{} -> resource
resource -> resource
end
input =
action_input(resource_or_record, action, generators)
|> Enum.at(0)
do_changeset_or_query(resource, resource_or_record, action, input, changeset_options)
end
@doc """
Generate `count` changesets and return them as a list.
"""
@spec many_changesets(
Ash.Resource.t(),
action :: atom(),
count :: pos_integer(),
overrides(),
changeset_options :: Keyword.t()
) :: list(Ash.Changeset.t())
def many_changesets(
resource_or_record,
action,
count,
generators \\ %{},
changeset_options \\ []
) do
resource =
case resource_or_record do
%resource{} -> resource
resource -> resource
end
action_input(resource_or_record, action, generators)
|> Enum.take(count)
|> Enum.map(fn input ->
do_changeset_or_query(resource, resource_or_record, action, input, changeset_options)
end)
end
@doc """
Creates the input for the provided action with `action_input/3`, and returns a query for that action with that input.
See `action_input/3` and the module documentation for more.
"""
@spec query(
Ash.Resource.t(),
action :: atom(),
overrides(),
query_options :: Keyword.t()
) :: Ash.Query.t()
def query(resource, action, generators \\ %{}, query_options \\ []) do
changeset(resource, action, generators, query_options)
end
@doc """
Generate `count` queries and return them as a list.
"""
@spec many_queries(
Ash.Resource.t(),
action :: atom(),
count :: pos_integer(),
overrides(),
changeset_options :: Keyword.t()
) :: list(Ash.Query.t())
def many_queries(resource, action, count, generators \\ %{}, changeset_options \\ []) do
many_changesets(resource, action, count, generators, changeset_options)
end
@doc """
Generate input meant to be passed into `Ash.Seed.seed!/2`.
A map of custom `StreamData` generators can be provided to add to or overwrite the generated input,
for example: `Ash.Generator.seed_input(Post, %{text: StreamData.constant("Post")})`
"""
@spec seed_input(Ash.Resource.t(), map()) :: StreamData.t(map())
def seed_input(resource, generators \\ %{}) do
relationships = Ash.Resource.Info.relationships(resource)
resource
|> Ash.Resource.Info.attributes()
|> Enum.reject(fn attribute ->
Enum.any?(relationships, &(&1.source_attribute == attribute.name))
end)
|> generate_attributes(generators, true, :create, Enum.map(relationships, & &1.name))
end
@doc """
Gets the next value for a given sequence identifier.
See `sequence/3` for more.
This is equivalent to `identifier |> Ash.Generator.sequence(fun, sequencer) |> Enum.at(0)`
"""
def next_in_sequence(identifier, fun, sequencer \\ fn i -> (i || -1) + 1 end) do
identifier
|> sequence(fun, sequencer)
|> Enum.at(0)
end
@doc """
Creates a generator of maps where all keys are required except the list provided
## Example
```elixir
iex> mixed_map(%{a: StreamData.constant(1), b: StreamData.constant(2)}, [:b]) |> Enum.take(2)
[%{a: 1}, %{a: 1, b: 2}]
```
"""
@spec mixed_map(map(), list(term())) :: stream_data()
def mixed_map(map, []) do
map = to_generators(map)
StreamData.fixed_map(map)
end
def mixed_map(map, keys) do
map = to_generators(map)
{optional, required} = Map.split(map, keys)
do_mixed_map({StreamData.fixed_map(required), StreamData.optional_map(optional)})
end
defp do_changeset_or_query(resource, resource_or_record, action, input, changeset_options) do
case Ash.Resource.Info.action(resource, action).type do
:read ->
Ash.Query.for_read(resource, action, input, changeset_options)
:create ->
Ash.Changeset.for_create(resource, action, input, changeset_options)
:update ->
Ash.Changeset.for_update(resource_or_record, action, input, changeset_options)
:destroy ->
Ash.Changeset.for_destroy(resource_or_record, action, input, changeset_options)
end
end
defp generate_attributes(
attributes,
generators,
keep_nil?,
action_type,
extra_keys_to_keep
) do
generators = Map.new(generators)
attributes
|> Enum.reduce({%{}, %{}}, fn attribute, {required, optional} ->
default =
cond do
action_type == :create ->
attribute.default
action_type in [:update, :destroy] and is_struct(attribute, Ash.Resource.Attribute) ->
attribute.update_default
true ->
nil
end
# only create a value for attributes that didn't get a dedicated generator
if attribute.name in Map.keys(generators) do
{required, optional}
else
if attribute.allow_nil? || !is_nil(default) do
options = [attribute_generator(attribute, attribute.allow_nil?)]
options =
if attribute.allow_nil? do
if keep_nil? do
[StreamData.constant(:__keep_nil__) | options]
else
[StreamData.constant(nil) | options]
end
else
options
end
options =
if attribute.allow_nil? && is_nil(default) do
[StreamData.constant(nil) | options]
else
options
end
{required,
Map.put(
optional,
attribute.name,
StreamData.one_of(options)
)}
else
{Map.put(
required,
attribute.name,
attribute_generator(attribute, attribute.allow_nil?)
), optional}
end
end
end)
|> then(fn {required, optional} ->
generators =
generators
|> to_generators()
|> Map.take(Enum.map(attributes, & &1.name) ++ extra_keys_to_keep)
{Map.merge(required, generators), Map.drop(optional, Map.keys(generators))}
end)
|> do_mixed_map()
end
defp attribute_generator(attribute, false) do
attribute.type
|> Ash.Type.generator(attribute.constraints)
|> StreamData.filter(fn item ->
with {:ok, value} <- Ash.Type.cast_input(attribute.type, item, attribute.constraints),
{:ok, nil} <-
Ash.Type.apply_constraints(attribute.type, value, attribute.constraints) do
false
else
_ ->
true
end
end)
end
defp attribute_generator(attribute, true) do
Ash.Type.generator(attribute.type, attribute.constraints)
end
defp do_mixed_map({required, optional}) do
{StreamData.fixed_map(required), StreamData.optional_map(optional)}
|> StreamData.map(fn {required, optional} ->
Map.merge(required, optional)
end)
end
defp to_generators(generators) do
Map.new(generators, fn {key, value} ->
case value do
%StreamData{} ->
{key, value}
value ->
{key, StreamData.constant(value)}
end
end)
end
defp set_allow_nil(
attributes,
%{
allow_nil_input: allow_nil_input,
require_attributes: require_attributes
}
) do
Enum.map(attributes, fn attribute ->
cond do
attribute.name in allow_nil_input ->
%{attribute | allow_nil?: true}
attribute.name in require_attributes ->
%{attribute | allow_nil?: false}
true ->
attribute
end
end)
end
defp set_allow_nil(attributes, _), do: attributes
defp find_manage_change(argument, action) do
Enum.find_value(Map.get(action, :changes, []), fn
%{change: {Ash.Resource.Change.ManageRelationship, opts}} ->
if opts[:argument] == argument.name do
opts
end
_ ->
nil
end)
end
end