4

I have two plain Ruby classes, Account and Contact. I am using Simple Form's simple_form_for and simple_fields_for to create nested attributes. I am looking to fulfill the following validation requirements:

  1. An associated Contact must exist for the new Account
  2. The associated Contact must be valid (i.e., account.contact.valid?)

It looks like ActiveModel no longer includes the validates_associated method, as using that method results in an undefined method error. I considered requiring ActiveRecord::Validations, but this led down a stretch of various errors (e.g., undefined method `marked_for_destruction?')

I also considered defining validate on the Account class and calling valid? on the associated object, but that only prevented the form from submitting if there was also an error on the parent object.

validate do |account|
  account.contact.valid?

  # required for form to fail
  errors.add(:base, "some error")
end

Is there something I'm not aware of to solve this? Thanks.

Eric M.
  • 5,399
  • 6
  • 41
  • 67

2 Answers2

4

I recently (7 years after this question has been asked!) faced the same issue and solved it by implementing the AssociatedValidator based on the ActiveRecord one. I simply included it in config/initializers folder:

module ActiveModel
  module Validations
    class AssociatedValidator < ActiveModel::EachValidator
      def validate_each(record, attribute, value)
        if Array(value).reject { |r| valid_object?(r) }.any?
          record.errors.add(attribute, :invalid, **options.merge(value: value))
        end
      end

      private

      def valid_object?(record)
        record.valid?
      end
    end

    module ClassMethods
      def validates_associated(*attr_names)
        validates_with AssociatedValidator, _merge_attributes(attr_names)
      end
    end
  end
end

now you can use validates_associated in ActiveModel too.

coorasse
  • 5,278
  • 1
  • 34
  • 45
  • You can get rid of the `valid_object?` method: `def validate_each(record, attribute, value) return if Array(value).reject(&:valid?).none? record.errors.add(attribute, :invalid, **options.merge(value:)) end` – Maxence De Rous Jan 11 '23 at 19:26
  • Works like a charm in Rails 7, thank you – user1519240 Jul 06 '23 at 14:44
1
class Person
  include Virtus
  include ActiveModel::Model

  attribute :address, Address, :default => Address.new

  validate :address_valid

  private

  def address_valid
    errors.add(:base, 'address is not valid') unless address.valid?
  end
end

class Address
  include Virtus::ValueObject
  include ActiveModel::Validations

  attribute :line_1, String
  attribute :line_2, String

  validates :line_1, :presence => true
  validates :line_2, :presence => true
end

The errors show up in the form if you pass an object to simple_fields_for:

 = form.simple_fields_for person.address do |af|      
   = af.input :line_1

Another option is overriding valid?:

def valid?
  super & address.valid?
end

Note its & not && so the conditions are not short circuited if the first returns false.

Kris
  • 19,188
  • 9
  • 91
  • 111