Search documentation of ash

# Ash.Generator (ash v3.4.71)

View Source

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

overrides()

A map or keyword of data generators or constant values to use in place of defaults.

stream_data()

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

## Functions

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

Generate input meant to be passed into a resource action.

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

Creates the input for the provided action with `action_input/3`, and creates a changeset for that action with that input.

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

A generator of seedable records, to be passed to `generate/1` or `generate_many/1`

generate(changeset)

Takes one value from a changeset or seed generator and calls `Ash.create!` on it.

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)

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

initialize_sequence(identifier)

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

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

Generate `count` changesets and return them as a list.

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

Generate `count` queries and return them as a list.

mixed_map(map, keys)

Creates a generator of maps where all keys are required except the list provided

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

Gets the next value for a given sequence identifier.

once(identifier, generator)

Run the provided function or enumerable (i.e generator) only once.

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

Creates the input for the provided action with `action_input/3`, and returns a query for that action with that input.

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

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

seed_generator(record, opts \\ [])

A generator of seedable records, to be passed to `generate/1` or `generate_many/1`

seed_input(resource, generators \\ %{})

Generate input meant to be passed into `Ash.Seed.seed!/2`.

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)

Generate globally unique values.

stop_once(identifier)

Stops the agent for a `once/2`.

stop_sequence(identifier)

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

# 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

- `: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
- `: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 creatd.

## The `uses` option

    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

# generate(changeset)

    @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

# 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()
      | {Ash.Resource.t(), map()}
      | (map() -> Ash.Resource.record() | {Ash.Resource.t(), %{}}),
      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.

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

    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:

    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
- `: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 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 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

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:

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

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