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.

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.

Stops the agent for a once/2.

Stops the agent for a sequence.

Types

overrides()

@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.

stream_data()

@type stream_data() :: Enumerable.t()

An instance of StreamData, gotten from one of the functions in that module.

Functions

action_input(resource_or_record, action, generators \\ %{})

@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.

changeset(resource_or_record, action, generators \\ %{}, changeset_options \\ [])

@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.

changeset_generator(resource, action, opts \\ [])

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 of t: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.

generate(changeset)

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

generate_many(changeset_generator, count)

Takes count values from a changeset or seed generator and passes their inputs into Ash.bulk_create! or Ash.Seed.seed! respectively.

initialize_once(identifier)

@spec initialize_once(atom()) :: pid()

Starts and links an agent for a once/2, or returns the existing agent pid if it already exists.

See once/2 for more.

initialize_sequence(identifier)

@spec initialize_sequence(atom()) :: pid()

Starts and links an agent for a sequence, or returns the existing agent pid if it already exists.

See sequence/3 for more.

many_changesets(resource_or_record, action, count, generators \\ %{}, changeset_options \\ [])

@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.

many_queries(resource, action, count, generators \\ %{}, changeset_options \\ [])

@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.

mixed_map(map, keys)

@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}]

next_in_sequence(identifier, fun, sequencer \\ fn i -> (i || -1) + 1 end)

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)

once(identifier, generator)

@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

query(resource, action, generators \\ %{}, query_options \\ [])

@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.

seed!(resource, generators \\ %{})

Gets input using seed_input/2 and passes it to Ash.Seed.seed!/2, returning the result

seed_generator(record, opts \\ [])

@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 of t: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.

seed_input(resource, generators \\ %{})

@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")})

seed_many!(resource, n, generators \\ %{})

Generates an input n times, and passes them all to seed, returning the list of seeded items.

sequence(identifier, generator, sequencer \\ fn i -> (i || -1) + 1 end)

@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"]

stop_once(identifier)

Stops the agent for a once/2.

See once/2 for more.

stop_sequence(identifier)

Stops the agent for a sequence.

See sequence/3 for more.