Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 72 additions & 24 deletions lib/ash/generator/generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,10 @@ defmodule Ash.Generator do
* `: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.
* `:generate_rest?` - If `false`, only generates values for attributes explicitly provided
in the attributes map (and overrides). Unlisted attributes receive their resource-level
default or `nil` (if `allow_nil?`). Raises if a required attribute with no default is
missing. Only applies to the tuple form `{Resource, attrs}`. Defaults to `true`.
"""
@spec seed_generator(
Ash.Resource.record()
Expand Down Expand Up @@ -447,21 +451,28 @@ defmodule Ash.Generator do
|> 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,
[]
)
merged = Map.merge(attributes, Map.new(opts[:overrides] || %{}))

if Keyword.get(opts, :generate_rest?, true) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.reject(&(!&1.writable? && &1.generated?))
|> generate_attributes(
to_generators(
Map.put(merged, :__will_be_struct__, resource)
),
false,
:create,
[]
)
else
validate_required_attributes(resource, merged, :create)

merged
|> to_generators()
|> Map.put(:__will_be_struct__, resource)
|> StreamData.fixed_map()
end
end
end)
|> StreamData.map(fn keys ->
Expand Down Expand Up @@ -502,15 +513,22 @@ defmodule Ash.Generator do
|> 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,
[]
)
merged = Map.merge(attributes, Map.new(opts[:overrides] || %{}))

if Keyword.get(opts, :generate_rest?, true) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.reject(&(!&1.writable? && &1.generated?))
|> generate_attributes(
to_generators(merged),
false,
:create,
[]
)
else
validate_required_attributes(resource, merged, :create)
merged |> to_generators() |> StreamData.fixed_map()
end
end
|> StreamData.map(fn keys ->
Ash.Resource.set_metadata(
Expand Down Expand Up @@ -1123,6 +1141,36 @@ defmodule Ash.Generator do
end)
end

defp validate_required_attributes(resource, generators, action_type) do
action = Ash.Resource.Info.primary_action(resource, action_type)
allow_nil_input = (action && Map.get(action, :allow_nil_input)) || []
require_attributes = (action && Map.get(action, :require_attributes)) || []

resource
|> Ash.Resource.Info.attributes()
|> Enum.reject(&(!&1.writable? && &1.generated?))
|> Enum.each(fn attribute ->
default =
if action_type == :create, do: attribute.default, else: attribute.update_default

required? =
cond do
attribute.name in allow_nil_input -> false
attribute.name in require_attributes -> true
attribute.allow_nil? -> false
!is_nil(default) -> false
true -> true
end

if required? && !Map.has_key?(generators, attribute.name) do
raise ArgumentError,
"Attribute #{inspect(attribute.name)} on #{inspect(resource)} is required " <>
"and has no default, but no generator was provided. " <>
"Add a generator for this attribute or set generate_rest?: true."
end
end)
end

defp to_generators(generators) do
Map.new(generators, fn {key, value} ->
case value do
Expand Down
158 changes: 158 additions & 0 deletions test/generator/generator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,46 @@ defmodule Ash.Test.GeneratorTest do
end
end

defmodule ActionOverrides do
@moduledoc false
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

ets do
private?(true)
end

actions do
default_accept :*

create :create do
primary? true
allow_nil_input [:required_attr]
require_attributes [:defaulted_attr]
end

defaults [:read, :destroy, update: :*]
end

attributes do
uuid_primary_key :id

attribute :required_attr, :string do
public?(true)
allow_nil? false
end

attribute :defaulted_attr, :string do
public?(true)
default "default value"
end

attribute :regular_required, :string do
public?(true)
allow_nil? false
end
end
end

defmodule Generator do
use Ash.Generator

Expand Down Expand Up @@ -680,6 +720,124 @@ defmodule Ash.Test.GeneratorTest do
end
end

describe "seed_generator generate_rest?" do
test "default behavior still generates all attributes" do
result =
Ash.Generator.seed_generator({Author, %{meta: %{}}})
|> Ash.Generator.generate()

assert %Author{} = result
end

test "generate_rest?: true generates all attributes" do
result =
Ash.Generator.seed_generator({Author, %{meta: %{}}}, generate_rest?: true)
|> Ash.Generator.generate()

assert %Author{} = result
end

test "generate_rest?: false only generates listed attributes" do
result =
Ash.Generator.seed_generator(
{Author, %{meta: %{key: "value"}}},
generate_rest?: false
)
|> Ash.Generator.generate()

assert %Author{} = result
assert result.meta == %{key: "value"}
assert result.metadata == nil
end

test "generate_rest?: false applies resource defaults" do
result =
Ash.Generator.seed_generator(
{Author, %{meta: %{}}},
generate_rest?: false
)
|> Ash.Generator.generate()

assert %Author{} = result
assert result.name == "Fred"
end

test "generate_rest?: false raises for missing required attributes" do
assert_raise ArgumentError,
~r/Attribute :meta .* is required/,
fn ->
Ash.Generator.seed_generator(
{Author, %{}},
generate_rest?: false
)
|> Ash.Generator.generate()
end
end

test "generate_rest?: false works with overrides" do
result =
Ash.Generator.seed_generator(
{Author, %{meta: %{}}},
generate_rest?: false,
overrides: %{name: "Custom Name"}
)
|> Ash.Generator.generate()

assert %Author{} = result
assert result.name == "Custom Name"
end

test "generate_rest?: false works with uses" do
result =
Ash.Generator.seed_generator(
fn uses -> {Author, %{meta: %{}, name: uses.name}} end,
generate_rest?: false,
uses: %{name: StreamData.constant("Generated Name")}
)
|> Ash.Generator.generate()

assert %Author{} = result
assert result.name == "Generated Name"
assert result.metadata == nil
end

test "generate_rest?: false respects primary action's allow_nil_input" do
result =
Ash.Generator.seed_generator(
{ActionOverrides, %{defaulted_attr: "x", regular_required: "y"}},
generate_rest?: false
)
|> Ash.Generator.generate()

assert %ActionOverrides{} = result
assert result.required_attr == nil
end

test "generate_rest?: false respects primary action's require_attributes" do
assert_raise ArgumentError,
~r/Attribute :defaulted_attr .* is required/,
fn ->
Ash.Generator.seed_generator(
{ActionOverrides, %{regular_required: "y"}},
generate_rest?: false
)
|> Ash.Generator.generate()
end
end

test "generate_rest?: false still raises for resource-level required attrs not in overrides" do
assert_raise ArgumentError,
~r/Attribute :regular_required .* is required/,
fn ->
Ash.Generator.seed_generator(
{ActionOverrides, %{defaulted_attr: "x"}},
generate_rest?: false
)
|> Ash.Generator.generate()
end
end
end

describe "changeset_generator" do
test "it correctly seeds a record" do
assert %Author{} =
Expand Down
Loading