Create function to return "example data set" in Ash

Hey there!

Goal

I like to have a function in each of my Models/Schemas/Domain-Objects/… that returns a pre-filled dataset to be used as an example.

Example

For a Person Ash resource:

defmodule MyApp.Person do
  use Ash.Resource, data_layer: :embedded

  actions do
    defaults create: :*, update: :*
  end

  attributes do
    attribute :salutation, :atom, constraints: [one_of: [:mr, :mrs, :ms, :other, :unknown]]
    attribute :given_name, :string
    attribute :name, :string
  end
end

Problem

I want to add a function like the following within the resource:

  def example_person do
    %MyApp.Person{
      salutation: :mr,
      given_name: "Erik",
      name: "Example"
    }
  end

This leads to compilation errors with a cyclic dependency, because the function requires MyApp.Person, but MyApp.Person contains the function.

Other attempts

I attempted to just return Maps, which works fine of course.

Then, at some point, multiple of those Maps are combined to a bigger resource. In this case, multiple pieces of information (like Person, Address, Files) are combined to an Application (for a job). Person, Address, Files are embedded resources.

When I have the pre-filled Map with all the example pieces

params = %{
  person: MyApp.Person.example_data(), # map
  address: MyApp.Address.example_data(), # map
  files: [MyApp.File.example_data(), MyApp.File.example_data()] # list of maps
}

and do

Ash.Changeset.for_create(MyApp.Application, :create, params)

I get an invalid changeset and for each field saying the following:

    %Ash.Error.Invalid.NoSuchInput{
      calculation: nil,
      resource: MyApp.Application,
      action: :create,
      input: :person,
      inputs: MapSet.new([]),
      did_you_mean: [],
      splode: nil,
      bread_crumbs: [],
      vars: [],
      path: [],
      stacktrace: #Splode.Stacktrace<>,
      class: :invalid
    }

How to?

There is probably a very fundamental thing about Ash I do not understand yet.

  1. How would you go about creating example data for Ash Resources?
  2. Why do I get Ash.Error.Invalid.NoSuchInput?

Instead of using having the “literal” struct there you could do

  def example_person do
    struct(__MODULE__, %{
      salutation: :mr,
      given_name: "Erik",
      name: "Example"
    })
  end

That being said there are multiple ways to generate example data.

There is Ash.Generator which has multiple functions from writing directly to the database to creating inputs for your actions.

Another excellent library to create example data for Ash Resources is Smokestack

Do you want to have these examples for tests?

  1. On your bigger resource do your actions accept the attributes? Either with :* if they are all public or as a list with the attribute names accept [:person, :address, :files]

:sparkles: hanks for the quick reply!

Not sure why I did not think about the struct() approach, I’ll give it a try, thanks for the reminder!

I’ll check out Ash.Generator and Smokestack; The example data is primarily for dev-experience, not so much for tests. I have no requirement for the data to be “dynamic”.

My “bigger ressource”, the Application in the above example, has the following actions:

  actions do
    defaults [:read, :destroy, create: :*, update: :*]
  end

I assume the create: :* is a shorthand? For

actions do
  action :create do
    accepts :*
  end
end

Are the attributes public?: true? :* is a shorthand for all public attributes

For resources, you might also want to call a create action to create the resource structure instead of just creating it yourself. This would execute potential changes and validations.

With embedded resources or resources without a datalayer there is no real side-effect so it’s safe to do. For Resources that are backed by e.g. AshPostgres what you can do is something along these lines

Resource
|> Ash.Changeset.for_create(:create, example_params)
|> Ash.Changeset.apply_attributes()

Create Action

Maybe I misunderstood, but: Trying to create a Changeset for an embedded ressource leads to the following error:

** (ArgumentError) Could not determine domain for changeset. Provide the `domain` option or configure a domain in the resource directly.

But I understand that datalayer: :embedded and Domain: MyDomain are exclusive options. Currently, I do not see a way to “create” an embedded ressource ← I guess that is the point of being embedded, as in it cannot exist alone.

public?: true

I now see that the examples for embedded ressources have public?: true in the documentation, but it seems there is no explanation for what that does.

What does public?: true mean? The value can be passed in from an external source, rather than just being populated internally?

I went ahead and just added public?: true to all fields in all embedded ressources and to all embedded fields on the “bigger” ressource as well.

→ Now works as expected; Thanks a lot!

We’ve fixed that could not determine domain issue with embeds to allow you to create one w/o a domain.

Wait just now, or am I on an old version?

Edit: I think I found it, few days ago: improvement: automatically fall back to a default domain when working… · ash-project/ash@516ff8e · GitHub

:muscle: :rocket: Thanks!

public?: true signals to extensions like AshGraphql or AshJsonApi that the attribute can be exposed to the outside world.

– (deleted) –