Validation not working with prefix and postfix options

Added by Brian Johnson 68 days ago

Validation errors do not work correctly with prefix and postfix options. If you have a dependent model address with a prefix of shipping, the errors added to the base model will be city, state, etc... instead of shipping_city, shipping_state, etc...
Here is my first try at a fix and it works correctly, but the code can probably be cleaned up.

$:.unshift(File.dirname(__FILE__)) unless
  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))

module ActiveRecord::Validations::ClassMethods
  def validates_associated_dependent(model_sym, options, configuration = {})
    configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }.update(configuration)

    validates_each(model_sym, configuration) do |record, attr_name, value|
      associate = record.send(attr_name)
      if associate && !associate.valid?
        associate.errors.each do |key, value|
          if options[:prefix]
            key = (options[:prefix] == true) ? "#{model_sym}_#{key}" : "#{options[:prefix]}_#{key}" 
          end
          if options[:postfix]
            key = (options[:postfix] == true) ? "#{key}_#{model_sym}" : "#{key}_#{options[:postfix]}" 
          end
          record.errors.add(key, value)
        end
      end
    end
  end
end

module Stonean
  module ClassyInheritance
    def self.included(base)
      base.extend Stonean::ClassyInheritance::ClassMethods
    end

    module ClassMethods
      def depends_on(model_sym, options = {}) 
        define_relationship(model_sym,options)

        # Optional presence of handling
        if options.has_key?(:validates_presence_if) && options[:validates_presence_if] != true
          if [Symbol, String, Proc].include?(options[:validates_presence_if].class)
            validates_presence_of model_sym, :if => options[:validates_presence_if]
          end
        else
          validates_presence_of model_sym
        end

        if options.has_key?(:validates_associated_if) && options[:validates_associated_if] != true
          if [Symbol, String, Proc].include?(options[:validates_associated_if].class)
            validates_associated_dependent model_sym, options, :if => options[:validates_associated_if]
          end
        else
          validates_associated_dependent model_sym, options
        end

        # Before save functionality to create/update the requisite object
        define_save_method(model_sym, options[:as])

        # Adds a find_with_<model_sym> class method
        define_find_with_method(model_sym)

        if options[:as]
          define_can_be_method_on_requisite_class(options[:class_name] || model_sym, options[:as])
        end

        options[:attrs].each{|attr| define_accessors(model_sym, attr, options)}
      end

      def can_be(model_sym, options = {})
        unless options[:as]
          raise ArgumentError, ":as attribute required when calling can_be" 
        end

        klass = model_sym.to_s.classify

        define_method "is_a_#{model_sym}?" do
          eval("self.#{options[:as]}_type == '#{klass}'")
        end

        find_with_method = "find_with_#{self.name.underscore}" 

        define_method "as_a_#{model_sym}" do
          eval("#{klass}.send(:#{find_with_method},self.#{options[:as]}_id)")
        end
      end

      private

      def classy_options
        [:as, :attrs, :prefix, :postfix, :validates_presence_if, :validates_associated_if]
      end

      def delete_classy_options(options, *keepers)
        options.delete_if do |key,value|
          classy_options.include?(key) && !keepers.include?(key)
        end
        options
      end

      def define_relationship(model_sym, options)
        opts = delete_classy_options(options.dup, :as)
        if opts[:as]
          as_opt = opts.delete(:as)
          opts = polymorphic_constraints(as_opt).merge(opts)
          has_one model_sym, opts
        else
          belongs_to model_sym, opts
        end
      end

      def define_save_method(model_sym, polymorphic_name = nil)
        define_method "save_requisite_#{model_sym}" do
          # Return unless the association exists
          eval("return unless self.#{model_sym}")

          # Set the polymorphic type and id before saving
          if polymorphic_name
            eval("self.#{model_sym}.#{polymorphic_name}_type = self.class.name")
            eval("self.#{model_sym}.#{polymorphic_name}_id = self.id")
          end

          if polymorphic_name
            # Save only if it's an update, has_one creates automatically
            eval <<-SAVEIT
              unless self.#{model_sym}.new_record?
                self.#{model_sym}.save
              end
            SAVEIT
          else
            eval("self.#{model_sym}.save")
          end
        end

        before_save "save_requisite_#{model_sym}".to_sym
      end

      def define_find_with_method(model_sym)
        self.class.send :define_method, "find_with_#{model_sym}" do |*args|
          eval <<-CODE
            if args[1] && args[1].is_a?(Hash)
              if args[1].has_key?(:include)
                inc_val = args[1][:include]
                new_val = inc_val.is_a?(Array) ? inc_val.push(:#{:model_sym}) : [inc_val, :#{model_sym}] 
                args[1][:include] = new_val
              else
                args[1].merge!({:include => :#{model_sym}})
              end
            else
              args << {:include => :#{model_sym}}
            end
            find(*args)
          CODE
        end
      end

      def define_accessors(model_sym, attr, options)
        accessor_method_name = attr

        if options[:prefix]
          accessor_method_name = (options[:prefix] == true) ? "#{model_sym}_#{accessor_method_name}" : "#{options[:prefix]}_#{accessor_method_name}" 
        end

        if options[:postfix]
          accessor_method_name = (options[:postfix] == true) ? "#{accessor_method_name}_#{model_sym}" : "#{accessor_method_name}_#{options[:postfix]}" 
        end

        define_method accessor_method_name do
          eval("self.#{model_sym} ? self.#{model_sym}.#{attr} : nil")
        end

        define_method "#{accessor_method_name}=" do |val|
          model_defined = eval("self.#{model_sym}")

          unless model_defined
            klass = options[:class_name] || model_sym.to_s.classify
            eval("self.#{model_sym} = #{klass}.new")
          end

          eval("self.#{model_sym}.#{attr}= val")
        end
      end

      def define_can_be_method_on_requisite_class(model_sym, polymorphic_name)
        klass = model_sym.to_s.classify
        requisite_klass = eval(klass)
        unless requisite_klass.respond_to?(self.name.underscore.to_sym)
          requisite_klass.send :can_be, self.name.underscore, 
                                      :as => polymorphic_name 
        end
      end

      def polymorphic_constraints(polymorphic_name)
        { :foreign_key => "#{polymorphic_name}_id",
          :conditions => "#{polymorphic_name}_type = '#{self.name}'"}
      end
    end # ClassMethods
  end # ClassyInheritance module
end # Stonean module
ActiveRecord::Base.send :include, Stonean::ClassyInheritance

Replies

RE: Validation not working with prefix and postfix options - Added by Andrew Stone 67 days ago

Thanks for helping out! I'll take a look at this over the weekend...had a busy week and am pretty spent right now.

-andy