diff options
Diffstat (limited to 'activemodel/lib')
-rw-r--r-- | activemodel/lib/active_model.rb | 4 | ||||
-rw-r--r-- | activemodel/lib/active_model/attribute_methods.rb | 43 | ||||
-rw-r--r-- | activemodel/lib/active_model/dirty.rb | 126 | ||||
-rw-r--r-- | activemodel/lib/active_model/lint.rb | 96 | ||||
-rw-r--r-- | activemodel/lib/active_model/serialization.rb | 30 | ||||
-rw-r--r-- | activemodel/lib/active_model/serializer.rb | 60 | ||||
-rw-r--r-- | activemodel/lib/active_model/serializers/json.rb | 18 | ||||
-rw-r--r-- | activemodel/lib/active_model/serializers/xml.rb | 76 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/format.rb | 41 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/numericality.rb | 21 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/with.rb | 64 |
11 files changed, 458 insertions, 121 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 9bb4cf8b54..5bb931be7f 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -29,12 +29,14 @@ module ActiveModel autoload :AttributeMethods, 'active_model/attribute_methods' autoload :Conversion, 'active_model/conversion' autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods' + autoload :Dirty, 'active_model/dirty' autoload :Errors, 'active_model/errors' + autoload :Lint, 'active_model/lint' autoload :Name, 'active_model/naming' autoload :Naming, 'active_model/naming' autoload :Observer, 'active_model/observing' autoload :Observing, 'active_model/observing' - autoload :Serializer, 'active_model/serializer' + autoload :Serialization, 'active_model/serialization' autoload :StateMachine, 'active_model/state_machine' autoload :TestCase, 'active_model/test_case' autoload :Validations, 'active_model/validations' diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index de80559036..1091ad3095 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -133,18 +133,31 @@ module ActiveModel undefine_attribute_methods end + def alias_attribute(new_name, old_name) + attribute_method_matchers.each do |matcher| + module_eval <<-STR, __FILE__, __LINE__+1 + def #{matcher.method_name(new_name)}(*args) + send(:#{matcher.method_name(old_name)}, *args) + end + STR + end + end + def define_attribute_methods(attr_names) return if attribute_methods_generated? - attr_names.each do |name| - attribute_method_matchers.each do |method| - method_name = "#{method.prefix}#{name}#{method.suffix}" - unless instance_method_already_implemented?(method_name) - generate_method = "define_method_#{method.prefix}attribute#{method.suffix}" + attr_names.each do |attr_name| + attribute_method_matchers.each do |matcher| + unless instance_method_already_implemented?(matcher.method_name(attr_name)) + generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" if respond_to?(generate_method) - send(generate_method, name) + send(generate_method, attr_name) else - generated_attribute_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__) + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__+1 + def #{matcher.method_name(attr_name)}(*args) + send(:#{matcher.method_missing_target}, '#{attr_name}', *args) + end + STR end end end @@ -180,7 +193,7 @@ module ActiveModel class AttributeMethodMatcher attr_reader :prefix, :suffix - AttributeMethodMatch = Struct.new(:prefix, :base, :suffix) + AttributeMethodMatch = Struct.new(:target, :attr_name) def initialize(options = {}) options.symbolize_keys! @@ -190,11 +203,19 @@ module ActiveModel def match(method_name) if matchdata = @regex.match(method_name) - AttributeMethodMatch.new(matchdata[1], matchdata[2], matchdata[3]) + AttributeMethodMatch.new(method_missing_target, matchdata[2]) else nil end end + + def method_name(attr_name) + "#{prefix}#{attr_name}#{suffix}" + end + + def method_missing_target + :"#{prefix}attribute#{suffix}" + end end def attribute_method_matchers #:nodoc: @@ -214,7 +235,7 @@ module ActiveModel method_name = method_id.to_s if match = match_attribute_method?(method_name) guard_private_attribute_method!(method_name, args) - return __send__("#{match.prefix}attribute#{match.suffix}", match.base, *args, &block) + return __send__(match.target, match.attr_name, *args, &block) end super end @@ -246,7 +267,7 @@ module ActiveModel # The struct's attributes are prefix, base and suffix. def match_attribute_method?(method_name) self.class.send(:attribute_method_matchers).each do |method| - if (match = method.match(method_name)) && attribute_method?(match.base) + if (match = method.match(method_name)) && attribute_method?(match.attr_name) return match end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb new file mode 100644 index 0000000000..735c61df74 --- /dev/null +++ b/activemodel/lib/active_model/dirty.rb @@ -0,0 +1,126 @@ +module ActiveModel + # Track unsaved attribute changes. + # + # A newly instantiated object is unchanged: + # person = Person.find_by_name('Uncle Bob') + # person.changed? # => false + # + # Change the name: + # person.name = 'Bob' + # person.changed? # => true + # person.name_changed? # => true + # person.name_was # => 'Uncle Bob' + # person.name_change # => ['Uncle Bob', 'Bob'] + # person.name = 'Bill' + # person.name_change # => ['Uncle Bob', 'Bill'] + # + # Save the changes: + # person.save + # person.changed? # => false + # person.name_changed? # => false + # + # Assigning the same value leaves the attribute unchanged: + # person.name = 'Bill' + # person.name_changed? # => false + # person.name_change # => nil + # + # Which attributes have changed? + # person.name = 'Bob' + # person.changed # => ['name'] + # person.changes # => { 'name' => ['Bill', 'Bob'] } + # + # Resetting an attribute returns it to its original state: + # person.reset_name! # => 'Bill' + # person.changed? # => false + # person.name_changed? # => false + # person.name # => 'Bill' + # + # Before modifying an attribute in-place: + # person.name_will_change! + # person.name << 'y' + # person.name_change # => ['Bill', 'Billy'] + module Dirty + extend ActiveSupport::Concern + include ActiveModel::AttributeMethods + + included do + attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' + attribute_method_affix :prefix => 'reset_', :suffix => '!' + end + + # Do any attributes have unsaved changes? + # person.changed? # => false + # person.name = 'bob' + # person.changed? # => true + def changed? + !changed_attributes.empty? + end + + # List of attributes with unsaved changes. + # person.changed # => [] + # person.name = 'bob' + # person.changed # => ['name'] + def changed + changed_attributes.keys + end + + # Map of changed attrs => [original value, new value]. + # person.changes # => {} + # person.name = 'bob' + # person.changes # => { 'name' => ['bill', 'bob'] } + def changes + changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h } + end + + # Map of attributes that were changed when the model was saved. + # person.name # => 'bob' + # person.name = 'robert' + # person.save + # person.previous_changes # => {'name' => ['bob, 'robert']} + def previous_changes + previously_changed_attributes + end + + private + # Map of change <tt>attr => original value</tt>. + def changed_attributes + @changed_attributes ||= {} + end + + # Map of fields that were changed when the model was saved + def previously_changed_attributes + @previously_changed || {} + end + + # Handle <tt>*_changed?</tt> for +method_missing+. + def attribute_changed?(attr) + changed_attributes.include?(attr) + end + + # Handle <tt>*_change</tt> for +method_missing+. + def attribute_change(attr) + [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) + end + + # Handle <tt>*_was</tt> for +method_missing+. + def attribute_was(attr) + attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) + end + + # Handle <tt>*_will_change!</tt> for +method_missing+. + def attribute_will_change!(attr) + begin + value = __send__(attr) + value = value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + end + + changed_attributes[attr] = value + end + + # Handle <tt>reset_*!</tt> for +method_missing+. + def reset_attribute!(attr) + __send__("#{attr}=", changed_attributes[attr]) if attribute_changed?(attr) + end + end +end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb new file mode 100644 index 0000000000..46af8ca9de --- /dev/null +++ b/activemodel/lib/active_model/lint.rb @@ -0,0 +1,96 @@ +require "test/unit" +require "test/unit/ui/console/testrunner" + +# You can test whether an object is compliant with the ActiveModel API by +# calling ActiveModel::Compliance.test(object). It will emit a Test::Unit +# output that tells you whether your object is fully compliant, or if not, +# which aspects of the API are not implemented. +# +# These tests do not attempt to determine the semantic correctness of the +# returned values. For instance, you could implement valid? to always +# return true, and the tests would pass. It is up to you to ensure that +# the values are semantically meaningful. +# +# Objects you pass in are expected to return a compliant object from a +# call to to_model. It is perfectly fine for to_model to return self. + +module ActiveModel + module Lint + def self.test(object, verbosity = 2, output = STDOUT) + test_class = Class.new(::Test::Unit::TestCase) do + include Test + + define_method(:setup) do + assert object.respond_to?(:to_model), "The object should respond_to :to_model" + @object = object.to_model + super + end + end + + ::Test::Unit::UI::Console::TestRunner.new(test_class, verbosity, output).start + end + + module Test + def assert_boolean(name, result) + assert result == true || result == false, "#{name} should be a boolean" + end + + # valid? + # ------ + # + # Returns a boolean that specifies whether the object is in a valid or invalid + # state. + def test_valid? + assert @object.respond_to?(:valid?), "The model should respond to valid?" + assert_boolean "valid?", @object.valid? + end + + # new_record? + # ----------- + # + # Returns a boolean that specifies whether the object has been persisted yet. + # This is used when calculating the URL for an object. If the object is + # not persisted, a form for that object, for instance, will be POSTed to the + # collection. If it is persisted, a form for the object will put PUTed to the + # URL for the object. + def test_new_record? + assert @object.respond_to?(:new_record?), "The model should respond to new_record?" + assert_boolean "new_record?", @object.new_record? + end + + def test_destroyed? + assert @object.respond_to?(:new_record?), "The model should respond to destroyed?" + assert_boolean "destroyed?", @object.destroyed? + end + + # errors + # ------ + # + # Returns an object that has :[] and :full_messages defined on it. See below + # for more details. + def setup + assert @object.respond_to?(:errors), "The model should respond to errors" + @errors = @object.errors + end + + # This module tests the #errors object + module Errors + # Returns an Array of Strings that are the errors for the attribute in + # question. If localization is used, the Strings should be localized + # for the current locale. If no error is present, this method should + # return an empty Array. + def test_errors_aref + assert @errors[:hello].is_a?(Array), "errors#[] should return an Array" + end + + # Returns an Array of all error messages for the object. Each message + # should contain information about the field, if applicable. + def test_errors_full_messages + assert @errors.full_messages.is_a?(Array), "errors#full_messages should return an Array" + end + end + + include Errors + end + end +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb new file mode 100644 index 0000000000..4c0073f687 --- /dev/null +++ b/activemodel/lib/active_model/serialization.rb @@ -0,0 +1,30 @@ +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' + +module ActiveModel + module Serialization + def serializable_hash(options = nil) + options ||= {} + + options[:only] = Array.wrap(options[:only]).map { |n| n.to_s } + options[:except] = Array.wrap(options[:except]).map { |n| n.to_s } + + attribute_names = attributes.keys.sort + if options[:only].any? + attribute_names &= options[:only] + elsif options[:except].any? + attribute_names -= options[:except] + end + + method_names = Array.wrap(options[:methods]).inject([]) do |methods, name| + methods << name if respond_to?(name.to_s) + methods + end + + (attribute_names + method_names).inject({}) { |hash, name| + hash[name] = send(name) + hash + } + end + end +end diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb deleted file mode 100644 index 5b603bdbd7..0000000000 --- a/activemodel/lib/active_model/serializer.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/slice' - -module ActiveModel - class Serializer - attr_reader :options - - def initialize(serializable, options = nil) - @serializable = serializable - @options = options ? options.dup : {} - - @options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s } - @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s } - end - - def serialize - raise NotImplemented - end - - def to_s(&block) - serialize(&block) - end - - # To replicate the behavior in ActiveRecord#attributes, - # <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set - # for a N level model but is set for the N+1 level models, - # then because <tt>:except</tt> is set to a default value, the second - # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if - # <tt>:only</tt> is set, always delete <tt>:except</tt>. - def serializable_attribute_names - attribute_names = @serializable.attributes.keys.sort - - if options[:only].any? - attribute_names &= options[:only] - elsif options[:except].any? - attribute_names -= options[:except] - end - - attribute_names - end - - def serializable_method_names - Array.wrap(options[:methods]).inject([]) do |methods, name| - methods << name if @serializable.respond_to?(name.to_s) - methods - end - end - - def serializable_names - serializable_attribute_names + serializable_method_names - end - - def serializable_hash - serializable_names.inject({}) { |hash, name| - hash[name] = @serializable.send(name) - hash - } - end - end -end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index e94512fd64..ee6d48bfc6 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -5,6 +5,7 @@ module ActiveModel module Serializers module JSON extend ActiveSupport::Concern + include ActiveModel::Serialization included do extend ActiveModel::Naming @@ -12,19 +13,6 @@ module ActiveModel cattr_accessor :include_root_in_json, :instance_writer => false end - class Serializer < ActiveModel::Serializer - def serializable_hash - model = super - @serializable.include_root_in_json ? - { @serializable.class.model_name.element => model } : - model - end - - def serialize - ActiveSupport::JSON.encode(serializable_hash) - end - end - # Returns a JSON string representing the model. Some configuration is # available through +options+. # @@ -92,7 +80,9 @@ module ActiveModel # {"comments": [{"body": "Don't think too hard"}], # "title": "So I was thinking"}]} def encode_json(encoder) - Serializer.new(self, encoder.options).to_s + hash = serializable_hash(encoder.options) + hash = { self.class.model_name.element => hash } if include_root_in_json + ActiveSupport::JSON.encode(hash) end def as_json(options = nil) diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 4508a39347..86149f1e5f 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -5,8 +5,9 @@ module ActiveModel module Serializers module Xml extend ActiveSupport::Concern + include ActiveModel::Serialization - class Serializer < ActiveModel::Serializer #:nodoc: + class Serializer #:nodoc: class Attribute #:nodoc: attr_reader :name, :value, :type @@ -74,32 +75,32 @@ module ActiveModel end end - def builder - @builder ||= begin - require 'builder' unless defined? ::Builder - options[:indent] ||= 2 - builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) + attr_reader :options - unless options[:skip_instruct] - builder.instruct! - options[:skip_instruct] = true - end + def initialize(serializable, options = nil) + @serializable = serializable + @options = options ? options.dup : {} - builder - end + @options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s } + @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s } end - def root - root = (options[:root] || @serializable.class.model_name.singular).to_s - reformat_name(root) - end - - def dasherize? - !options.has_key?(:dasherize) || options[:dasherize] - end + # To replicate the behavior in ActiveRecord#attributes, + # <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set + # for a N level model but is set for the N+1 level models, + # then because <tt>:except</tt> is set to a default value, the second + # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if + # <tt>:only</tt> is set, always delete <tt>:except</tt>. + def serializable_attribute_names + attribute_names = @serializable.attributes.keys.sort + + if options[:only].any? + attribute_names &= options[:only] + elsif options[:except].any? + attribute_names -= options[:except] + end - def camelize? - options.has_key?(:camelize) && options[:camelize] + attribute_names end def serializable_attributes @@ -134,6 +135,34 @@ module ActiveModel end private + def builder + @builder ||= begin + require 'builder' unless defined? ::Builder + options[:indent] ||= 2 + builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) + + unless options[:skip_instruct] + builder.instruct! + options[:skip_instruct] = true + end + + builder + end + end + + def root + root = (options[:root] || @serializable.class.model_name.singular).to_s + reformat_name(root) + end + + def dasherize? + !options.has_key?(:dasherize) || options[:dasherize] + end + + def camelize? + options.has_key?(:camelize) && options[:camelize] + end + def reformat_name(name) name = name.camelize if camelize? dasherize? ? name.dasherize : name @@ -163,8 +192,7 @@ module ActiveModel end def to_xml(options = {}, &block) - serializer = Serializer.new(self, options) - block_given? ? serializer.to_s(&block) : serializer.to_s + Serializer.new(self, options).serialize(&block) end def from_xml(xml) diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 6f3b668bf0..c670dafc7c 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -1,22 +1,30 @@ module ActiveModel module Validations module ClassMethods - # Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression - # provided. + # Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided. + # You can require that the attribute matches the regular expression: # # class Person < ActiveRecord::Base # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create # end # + # Alternatively, you can require that the specified attribute does _not_ match the regular expression: + # + # class Person < ActiveRecord::Base + # validates_format_of :email, :without => /NOSPAM/ + # end + # # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. # - # A regular expression must be provided or else an exception will be raised. + # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression, + # or else an exception will be raised. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!). + # * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation. + # * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation. # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The @@ -25,14 +33,27 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) - configuration = { :with => nil } - configuration.update(attr_names.extract_options!) + configuration = attr_names.extract_options! + + unless configuration.include?(:with) ^ configuration.include?(:without) # ^ == xor, or "exclusive or" + raise ArgumentError, "Either :with or :without must be supplied (but not both)" + end - raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) + if configuration[:with] && !configuration[:with].is_a?(Regexp) + raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash" + end - validates_each(attr_names, configuration) do |record, attr_name, value| - unless value.to_s =~ configuration[:with] - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) + if configuration[:without] && !configuration[:without].is_a?(Regexp) + raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash" + end + + if configuration[:with] + validates_each(attr_names, configuration) do |record, attr_name, value| + record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s !~ configuration[:with] + end + elsif configuration[:without] + validates_each(attr_names, configuration) do |record, attr_name, value| + record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s =~ configuration[:without] end end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index ada6e28594..32dbcd82d0 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -31,6 +31,21 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. + # + # The following checks can also be supplied with a proc or a symbol which corresponds to a method: + # * <tt>:greater_than</tt> + # * <tt>:greater_than_or_equal_to</tt> + # * <tt>:equal_to</tt> + # * <tt>:less_than</tt> + # * <tt>:less_than_or_equal_to</tt> + # + # class Person < ActiveRecord::Base + # validates_numericality_of :width, :less_than => Proc.new { |person| person.height } + # validates_numericality_of :width, :greater_than => :minimum_weight + # end + # + # + def validates_numericality_of(*attr_names) configuration = { :only_integer => false, :allow_nil => false } configuration.update(attr_names.extract_options!) @@ -38,7 +53,8 @@ module ActiveModel numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys (numericality_options - [ :odd, :even ]).each do |option| - raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric) + value = configuration[option] + raise ArgumentError, ":#{option} must be a number, a symbol or a proc" unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol) end validates_each(attr_names,configuration) do |record, attr_name, value| @@ -74,6 +90,9 @@ module ActiveModel record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) end else + configuration[option] = configuration[option].call(record) if configuration[option].is_a? Proc + configuration[option] = record.method(configuration[option]).call if configuration[option].is_a? Symbol + unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb new file mode 100644 index 0000000000..851cdfebf0 --- /dev/null +++ b/activemodel/lib/active_model/validations/with.rb @@ -0,0 +1,64 @@ +module ActiveModel + module Validations + module ClassMethods + + # Passes the record off to the class or classes specified and allows them to add errors based on more complex conditions. + # + # class Person < ActiveRecord::Base + # validates_with MyValidator + # end + # + # class MyValidator < ActiveRecord::Validator + # def validate + # if some_complex_logic + # record.errors[:base] << "This record is invalid" + # end + # end + # + # private + # def some_complex_logic + # # ... + # end + # end + # + # You may also pass it multiple classes, like so: + # + # class Person < ActiveRecord::Base + # validates_with MyValidator, MyOtherValidator, :on => :create + # end + # + # Configuration options: + # * <tt>on</tt> - Specifies when this validation is active (<tt>:create</tt> or <tt>:update</tt> + # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). + # The method, proc or string should return or evaluate to a true or false value. + # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). + # The method, proc or string should return or evaluate to a true or false value. + # + # If you pass any additional configuration options, they will be passed to the class and available as <tt>options</tt>: + # + # class Person < ActiveRecord::Base + # validates_with MyValidator, :my_custom_key => "my custom value" + # end + # + # class MyValidator < ActiveRecord::Validator + # def validate + # options[:my_custom_key] # => "my custom value" + # end + # end + # + def validates_with(*args) + configuration = args.extract_options! + + send(validation_method(configuration[:on]), configuration) do |record| + args.each do |klass| + klass.new(record, configuration.except(:on, :if, :unless)).validate + end + end + end + end + end +end + + |