View Source Ash.Generator (ash v3.4.55)
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
.
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:
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 proeprty testing via ExUnitProperties
Many functions in this module support "overrides", which allow passing down either constant values
or your own StreamData
generators.
For example:
# All generated posts will have text as `"text"`. Equivalent to providing `StreamData.constant("text")`.
Ash.Generator.seed_input(Post, %{text: "text"})
Summary
Types
A map or keyword of data generators or constant values to use in place of defaults.
An instance of StreamData
, gotten from one of the functions in that module.
Functions
Generate input meant to be passed into a resource action.
Creates the input for the provided action with action_input/3
, and creates a changeset for that action with that input.
A generator of seedable records, to be passed to generate/1
or generate_many/1
Takes one value from a changeset or seed generator and calls Ash.create!
on it.
Takes count
values from a changeset or seed generator and passes their inputs into Ash.bulk_create!
or Ash.Seed.seed!
respectively.
Starts and links an agent for a once/2
, or returns the existing agent pid if it already exists.
Starts and links an agent for a sequence, or returns the existing agent pid if it already exists.
Generate count
changesets and return them as a list.
Generate count
queries and return them as a list.
Creates a generator of maps where all keys are required except the list provided
Gets the next value for a given sequence identifier.
Run the provided function or enumerable (i.e generator) only once.
Creates the input for the provided action with action_input/3
, and returns a query for that action with that input.
Gets input using seed_input/2
and passes it to Ash.Seed.seed!/2
, returning the result
A generator of seedable records, to be passed to generate/1
or generate_many/1
Generate input meant to be passed into Ash.Seed.seed!/2
.
Generates an input n
times, and passes them all to seed, returning the list of seeded items.
Generate globally unique values.
Stops the agent for a once/2
.
Stops the agent for a sequence.
Types
@type overrides() :: %{required(term()) => stream_data() | term()} | Keyword.t(stream_data() | term())
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 stream_data() :: Enumerable.t()
An instance of StreamData
, gotten from one of the functions in that module.
Functions
@spec action_input( Ash.Resource.t() | Ash.Resource.record(), action :: atom(), generators :: overrides() ) :: map()
Generate input meant to be passed into a resource action.
Arguments that are passed to a manage_relationship
are excluded, and you will
have to generate them yourself by passing your own generators/values down. See the module documentation for more.
@spec changeset( Ash.Resource.t(), action :: atom(), overrides(), changeset_options :: Keyword.t() ) :: Ash.Changeset.t()
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.
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.
Examples
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:
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
See the Ash.Generator
moduledocs for more information.
Options
:overrides
- A keyword list or map oft:overrides()
:actor
- Passed through to the changeset:tenant
- Passed through to the changeset: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 creatd.
@spec generate(stream_data() | Ash.Changeset.t() | Ash.Resource.record()) :: Ash.Resource.record()
Takes one value from a changeset or seed generator and calls Ash.create!
on it.
Passes through resource structs without doing anything. Creates a changeset if given
Takes count
values from a changeset or seed generator and passes their inputs into Ash.bulk_create!
or Ash.Seed.seed!
respectively.
Starts and links an agent for a once/2
, or returns the existing agent pid if it already exists.
See once/2
for more.
Starts and links an agent for a sequence, or returns the existing agent pid if it already exists.
See sequence/3
for more.
@spec many_changesets( Ash.Resource.t(), action :: atom(), count :: pos_integer(), overrides(), changeset_options :: Keyword.t() ) :: [Ash.Changeset.t()]
Generate count
changesets and return them as a list.
@spec many_queries( Ash.Resource.t(), action :: atom(), count :: pos_integer(), overrides(), changeset_options :: Keyword.t() ) :: [Ash.Query.t()]
Generate count
queries and return them as a list.
@spec mixed_map(map(), [term()]) :: stream_data()
Creates a generator of maps where all keys are required except the list provided
Example
iex> mixed_map(%{a: StreamData.constant(1), b: StreamData.constant(2)}, [:b]) |> Enum.take(2)
[%{a: 1}, %{a: 1, b: 2}]
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)
@spec once(pid() | atom(), (-> value) | Enumerable.t(value)) :: StreamData.t(value) when value: term()
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 query( Ash.Resource.t(), action :: atom(), overrides(), query_options :: Keyword.t() ) :: Ash.Query.t()
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.
Gets input using seed_input/2
and passes it to Ash.Seed.seed!/2
, returning the result
@spec seed_generator(Ash.Resource.record(), opts :: Keyword.t()) :: stream_data()
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.
Examples
iex> seed_generator(%MyApp.Blog.Post{name: sequence(:blog_post_title, &"My Blog Post #{&1}")}) |> generate()
%Tunez.Music.Artist{name: "Artist 1"}
Usage in tests
This can be used to define seed generators in tests. A useful pattern is defining a function like so:
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 oft:overrides()
:actor
- Passed through to the changeset:tenant
- Passed through to the changeset: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 creatd.
@spec seed_input(Ash.Resource.t(), map()) :: StreamData.t(map())
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")})
Generates an input n
times, and passes them all to seed, returning the list of seeded items.
@spec sequence(pid() | atom(), (iterator | nil -> value), (iterator | nil -> iterator)) :: StreamData.t(value) when iterator: term(), value: term()
Generate globally unique values.
This is useful for generating values that are unique across all resources, 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.
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
.
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"]
Stops the agent for a once/2
.
See once/2
for more.
Stops the agent for a sequence.
See sequence/3
for more.