views:

21

answers:

0

I have a model, Card, with multiple subclasses through single table inheritance. I have another model, CardSet, with multiple subclasses through single table inheritance. Cards have many CardSets, and CardSets have many Cards, both through Memberships.

Given that a class FooCard is a subclass of Card, and class FooSet is a subclass of CardSet, I wanted to validate that FooSet instances only have associations with Cards of type FooCard.

After a lot of experimentation, study and liberal plagiarism of the Ruby on Rails ActiveRecord::Validations code, I wrote the following custom validation in lib/custom_validators.rb, which I load with require 'custom_validators' at the end of config/environment.rb.

The below code is working exactly as I intended! The only problem I have is that I don't like using code in my apps that I don't understand. The call to validatein lib/custom_validators.rb (below) was stolen piecemeal from different places in ActiveRecord::Validations and I just don't understand how it works. It seems that it calls an empty method that somehow accepts a block, even though the method is empty?(!) I understand the Membership.find() part fine, it's the block that wraps around it I'm having trouble with.

Can anyone help me understand what is happening in that validate method call?

module ActiveRecord
  module Validations
    module ClassMethods
      def validates_association_type( *attr_names )
        attr_name = attr_names.first.to_s
        attr_name_singular = attr_name.singularize
        attr_type = attr_names.last[:is].to_s

        validate do |record|
          membership = Membership.find(
                  :first,
                  :include => attr_name_singular.to_sym, :conditions => [
                          "#{attr_name_singular}_id IN (?) AND #{attr_name}.type <> ?",
                          record.send( attr_name ).map(&:id),
                          attr_type
                  ]
          )

          unless membership.nil?
            record.errors.add_to_base( "allows only #{attr_name} of type #{attr_type}" )
          end
        end
      end

    end
  end
end

This validator is added to the FooSet class like this:

class FooSet < CardSet
  validates_association_type :cards, :is => "FooCard"
end

If you are interested, the various models are set up like this:

class Card < ActiveRecord::Base
  has_many :memberships
  has_many :card_sets, :through => :memberships
end
class Membership < ActiveRecord::Base
  belongs_to :card
  belongs_to :card_set
end
class CardSet < ActiveRecord::Base
  has_many :memberships
  has_many :cards, :through => :memberships
end
class FooCard < Card
end
class BarCard < Card
end
class FooSet < CardSet
  validates_association_type :cards, :is => "FooCard"
end
class BarSet < CardSet
  validates_association_type :cards, :is => "BarCard"
end