trailblazer / reform

Form objects decoupled from models.
https://trailblazer.to/2.1/docs/reform.html
MIT License
2.49k stars 184 forks source link

Errors of nested collection are not shown. #527

Open bayburin opened 3 years ago

bayburin commented 3 years ago

Complete Description of Issue

Hi! I'm using reform 2.5.0 with dry-validations 1.6 into rails 6.0.3 and seeing issue. I'm trying to validate a nested collection of forms from the parent form and output errors for each collection item, but form.errors.messages method returns empty hash.

Steps to reproduce

class WorkForm < Reform::Form
  property :id
  property :claim_id
  property :group_id
  collection :workflows, form: WorkflowForm, populate_if_empty: Workflow

  validation do
    option :form
    config.messages.backend = :i18n

    params do
      required(:group_id).filled(:int?)
      optional(:workflows)
    end

    rule(:group_id) do
      key.failure(:already_exist) if Work.where.not(id: form.model.id).where(group_id: value, claim_id: form.claim_id).exists?
    end

    rule(:workflows).each do |index:|
      next if value[:id]

      # For reproduce I removed any conditions and just raised error to fail validation
      key([:workflows, :sender_id, index]).failure('invalid sender_id')
    end
  end
end

class WorkflowForm < Reform::Form
  property :id
  property :work_id
  property :sender_id
  property :message
end

# Now call the form
irb(main):366:0>  params = { "group_id": 2, "workflows": [{ "sender_id": 4, "message": "workflow message" }] }
irb(main):366:0>  form = WorkForm.new(Work.find(4))
irb(main):366:0>  form.validate(params)
# false
irb(main):366:0>  form.errors.messages
# {}

Expected behavior

I did this validation according to dry-rb documentation: https://dry-rb.org/gems/dry-validation/1.6/rules/#defining-a-rule-for-each-element-of-an-array

I expect to see something like this:

irb(main):366:0> form.errors.messages
# {"workflows"=>{"sender_id"=>{0=>["invalid sender_id"]}}}

Actual behavior

But now I see an empty hash

irb(main):366:0> form.errors.messages
# {}

Although if you look at the error object, you can see that the validation worked successfully and detected an error into collection:

irb(main):366:0> form.errors.instance_variable_get(:@result)
=> #<Reform::Contract::Result:0x0000563bef736a28 @results=[#<Dry::Validation::Result{:group_id=>2, :workflows=>[{:id=>nil, :claim_id=>nil, :work_id=>nil, :sender_id=>4, :message=>"workflow message"}]} errors={:workflows=>{:sender_id=>{0=>["invalid sender_id"]}}}>], @failure=#<Dry::Validation::Result{:group_id=>2, :workflows=>[{:id=>nil, :claim_id=>nil, :work_id=>nil, :sender_id=>4, :message=>"workflow message"}]} errors={:workflows=>{:sender_id=>{0=>["invalid sender_id"]}}}>>

If I transfer the validation to the WorkflowForm and refuse to iterate the array, there is no such problem, the error is successfully displayed. But I cannot transfer validation to a nested form as I need to compare the data from WorkForm with data from WorkflowForm form.

System configuration

Reform version: 2.5.0 Rails version: 6.0.3 dry-validation version: 1.6.0

yogeshjain999 commented 3 years ago

This works for me

require 'test_helper'                                                                                                             

class ValidateCollectionTest < MiniTest::Spec
  class SongForm < TestForm
    property :title
  end 

  class AlbumForm < TestForm
    collection :songs, form: SongForm

    validation do
      option :form

      params do
        optional(:songs)
      end 

      rule(:songs).each do |index:|
        key([:title, index]).failure('Invalid') if value[:title] == "Wrong"
      end 
    end 
  end 

  let(:song)               { Song.new("Broken") }
  let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
  let(:composer)           { Artist.new("Greg Graffin") }
  let(:artist)             { Artist.new("Bad Religion") }
  let(:album)              { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }

  let(:form) { AlbumForm.new(album) }

  it do
    form.validate("songs" => [{"title" => "Wrong"}, {"title" => "Brok"}])
    _(form.errors.messages).must_equal({:title=>{0=>["Invalid"]}})
  end 
end

Try replacing next if value[:id] with next if form.model.id as id isn't present in your params.