paulelliott / fabrication

This project has moved to GitLab! Please check there for the latest updates.
https://gitlab.com/fabrication-gem/fabrication
MIT License
1k stars 97 forks source link

Handling nested attributes in Fabricate.attributes_for. #215

Open thomasfedb opened 10 years ago

thomasfedb commented 10 years ago

I have a fabricator defined as so:

Fabricator(:record) do
  # Study Allocation
  study_id { sequence(:study_id) }

  # Violations
  is_violation { [true, false].sample }
  violations {|attrs| attrs[:is_violation] ? Fabricate.build_times(2, :violation) : [] }

  # Demographics
  demo_date_of_birth { (rand(6) + 6).years.ago + rand(365).days }
  demo_weight { (20 + rand(40) + rand).round(2) }
  demo_height { (60 + rand(90)).to_f / 100 }
end

The issue I'm facing is when I use Fabricate.attributes_for(:record). Currently the following output results:

{
  "study_id" => 2,
  "is_violation" => true,
  "violations" => [
    #<Violation id: nil, violation_type_id: nil, record_id: nil, comment: nil, created_at: nil, updated_at: nil>,
    #<Violation id: nil, violation_type_id: nil, record_id: nil, comment: nil, created_at: nil, updated_at: nil>
  ],
  "demo_date_of_birth" => Mon, 26 Mar 2007 05:12:57 UTC +00:00,
  "demo_weight" => 21.14,
  "demo_height" => 1.4
}

Whereas I was hoping for output that I could feed into a controller's parameters, and would thus be more like:

{
  "study_id" => 2,
  "is_violation" => true,
  "violations_attributes" => [
    {
      "violation_type_id" => 17,
      "comment" => "Lorem ipsum."
    }
  ],
  "demo_date_of_birth" => Mon, 26 Mar 2007 05:12:57 UTC +00:00,
  "demo_weight" => 21.14,
  "demo_height" => 1.4
}
thomasfedb commented 10 years ago

Ideally, as there are many case where generating attributes involves nesting attributes for some records, and committing others directly to the database, there would be callbacks available for attributes_for.

paulelliott commented 10 years ago

I agree that this is how it should work. There are some architectural decisions in the library that make this change extremely difficult though. I have it on my feature list for 3.0 though.

thomasfedb commented 7 years ago

How did you go with this end the end @paulelliott?

paulelliott commented 7 years ago

As it stands there is not a solution for this. I closed out all old lingering issues last week but since you're still interested I'll reopen it 😄

This is a challenging issue to address but now that I'm thinking through it again I have some ideas. I'll see what I can do.

paulelliott commented 7 years ago

So the issue we have is in reliably knowing when something is an association and when it isn't. Right now it is inferred when you use the count or fabricator option on an attribute, which is totally fine in today's world.

Fabricator(:thingy) do
  widgets(count: 5) // works
  wocket(fabricator: :discombobulator) // works
  huzzahs do // doesn't work :(
    (1..10).map { Fabricate.build(:huzzah) }
  end
end

In the case above we could know that widgets and wocket are associations but we wouldn't be able to tell that huzzahs is. We need to indicate this is an association and there is no way to have that without defining a new syntax construct. It could be something as simple as an association flag or something more descriptive. Here are some options:

Fabricator(:thingy) do
  huzzahs(association: true) { ... }

  huzzahs(hasMany: true) { ... }
  hasMany :huzzahs { ... }

  wocket(belongsTo: true) { ... }
  belongsTo :wocket { ... }
end

I'm kind of leaning towards introducing the hasMany and belongsTo constructs. They could work like this:

Fabricator(:thingy) do
  hasMany :huzzahs, count: 2 // builds 2 `huzzah` objects and assigns them
  hasMany :huzzahs do // assigns whatever the block returns
    (1..10).map { Fabricate.build(:something) }
  end
end

To be clear, what I am proposing here is a major change and huge chunk of work to take on. I can't promise I would be able to implement it in the near term.

thomasfedb commented 7 years ago

Hey @paulelliott, is it not possible to determine associations by inspecting the model? Certainly the information is there, can it be used in Fabrication?

paulelliott commented 7 years ago

Yeah, it could be. The problem with that is fabrication works with a lot of different systems that all do that differently. To fully support that paradigm would be just as much work as introducing the new construct.

thomasfedb commented 7 years ago

I like your proposed solution, but could you use has_many rather than hasMany?