diff options
Diffstat (limited to 'activemodel/lib/active_model')
21 files changed, 188 insertions, 140 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 98cde8ba59..8d6d405e96 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,4 +1,5 @@ require 'thread_safe' +require 'mutex_m' module ActiveModel # Raised when an attribute is not defined. @@ -218,6 +219,16 @@ module ActiveModel end end + # Is +new_name+ an alias? + def attribute_alias?(new_name) + attribute_aliases.key? new_name.to_s + end + + # Returns the original name for the alias +name+ + def attribute_alias(name) + attribute_aliases[name.to_s] + end + # Declares the attributes that should be prefixed and suffixed by # ActiveModel::AttributeMethods. # @@ -322,9 +333,10 @@ module ActiveModel attribute_method_matchers_cache.clear end - # Returns true if the attribute methods defined have been generated. def generated_attribute_methods #:nodoc: - @generated_attribute_methods ||= Module.new.tap { |mod| include mod } + @generated_attribute_methods ||= Module.new { + extend Mutex_m + }.tap { |mod| include mod } end protected @@ -337,7 +349,7 @@ module ActiveModel # invoked often in a typical rails, both of which invoke the method # +match_attribute_method?+. The latter method iterates through an # array doing regular expression matches, which results in a lot of - # object creations. Most of the times it returns a +nil+ match. As the + # object creations. Most of the time it returns a +nil+ match. As the # match result is always the same given a +method_name+, this cache is # used to alleviate the GC, which ultimately also speeds up the app # significantly (in our case our test suite finishes 10% faster with @@ -388,14 +400,6 @@ module ActiveModel AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) def initialize(options = {}) - if options[:prefix] == '' || options[:suffix] == '' - message = "Specifying an empty prefix/suffix for an attribute method is no longer " \ - "necessary. If the un-prefixed/suffixed version of the method has not been " \ - "defined when `define_attribute_methods` is called, it will be defined " \ - "automatically." - ActiveSupport::Deprecation.warn message - end - @prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '') @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/ @method_missing_target = "#{@prefix}attribute#{@suffix}" diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 8b09f8b203..377aa6ee27 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -135,7 +135,10 @@ module ActiveModel klass.define_singleton_method("after_#{callback}") do |*args, &block| options = args.extract_options! options[:prepend] = true - options[:if] = Array(options[:if]) << "value != false" + conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v| + v != false + } + options[:if] = Array(options[:if]) << conditional set_callback(:"#{callback}", :after, *(args << options), &block) end end diff --git a/activemodel/lib/active_model/deprecated_mass_assignment_security.rb b/activemodel/lib/active_model/deprecated_mass_assignment_security.rb deleted file mode 100644 index 1f409c87b9..0000000000 --- a/activemodel/lib/active_model/deprecated_mass_assignment_security.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActiveModel - module DeprecatedMassAssignmentSecurity # :nodoc: - extend ActiveSupport::Concern - - module ClassMethods # :nodoc: - def attr_protected(*args) - raise "`attr_protected` is extracted out of Rails into a gem. " \ - "Please use new recommended protection model for params" \ - "(strong_parameters) or add `protected_attributes` to your " \ - "Gemfile to use old one." - end - - def attr_accessible(*args) - raise "`attr_accessible` is extracted out of Rails into a gem. " \ - "Please use new recommended protection model for params" \ - "(strong_parameters) or add `protected_attributes` to your " \ - "Gemfile to use old one." - end - end - end -end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index cafdb946c0..c5f1b3f11a 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -14,13 +14,9 @@ module ActiveModel # track. # * Call <tt>attr_name_will_change!</tt> before each change to the tracked # attribute. - # - # If you wish to also track previous changes on save or update, you need to - # add: - # - # @previously_changed = changes - # - # inside of your save or update method. + # * Call <tt>changes_applied</tt> after the changes are persisted. + # * Call <tt>reset_changes</tt> when you want to reset the changes + # information. # # A minimal implementation could be: # @@ -39,8 +35,12 @@ module ActiveModel # end # # def save - # @previously_changed = changes - # @changed_attributes.clear + # # do persistence work + # changes_applied + # end + # + # def reload! + # reset_changes # end # end # @@ -65,6 +65,12 @@ module ActiveModel # person.changed? # => false # person.name_changed? # => false # + # Reset the changes: + # + # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.reload! + # person.previous_changes # => {} + # # Assigning the same value leaves the attribute unchanged: # # person.name = 'Bill' @@ -129,7 +135,7 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed + @previously_changed ||= {} end # Returns a hash of the attributes with unsaved changes indicating their original @@ -139,14 +145,31 @@ module ActiveModel # person.name = 'robert' # person.changed_attributes # => {"name" => "bob"} def changed_attributes - @changed_attributes ||= {} + @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new + end + + # Handle <tt>*_changed?</tt> for +method_missing+. + def attribute_changed?(attr) + changed_attributes.include?(attr) + end + + # Handle <tt>*_was</tt> for +method_missing+. + def attribute_was(attr) + attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end private - # Handle <tt>*_changed?</tt> for +method_missing+. - def attribute_changed?(attr) - changed_attributes.include?(attr) + # Removes current changes and makes them accessible through +previous_changes+. + def changes_applied + @previously_changed = changes + @changed_attributes = {} + end + + # Removes all dirty data: current changes and previous changes + def reset_changes + @previously_changed = {} + @changed_attributes = {} end # Handle <tt>*_change</tt> for +method_missing+. @@ -154,11 +177,6 @@ module ActiveModel [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) return if attribute_changed?(attr) diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 0d7efab04b..cf7551e4f4 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -50,7 +50,7 @@ module ActiveModel # # The above allows you to do: # - # p = Person.new + # person = Person.new # person.validate! # => ["can not be nil"] # person.errors.full_messages # => ["name can not be nil"] # # etc.. @@ -238,8 +238,8 @@ module ActiveModel # object. You can pass the <tt>:full_messages</tt> option. This determines # if the json object should contain full messages or not (false by default). # - # person.as_json # => {:name=>["can not be nil"]} - # person.as_json(full_messages: true) # => {:name=>["name can not be nil"]} + # person.errors.as_json # => {:name=>["can not be nil"]} + # person.errors.as_json(full_messages: true) # => {:name=>["name can not be nil"]} def as_json(options=nil) to_hash(options && options[:full_messages]) end @@ -247,8 +247,8 @@ module ActiveModel # Returns a Hash of attributes with their error messages. If +full_messages+ # is +true+, it will contain full messages (see +full_message+). # - # person.to_hash # => {:name=>["can not be nil"]} - # person.to_hash(true) # => {:name=>["name can not be nil"]} + # person.errors.to_hash # => {:name=>["can not be nil"]} + # person.errors.to_hash(true) # => {:name=>["name can not be nil"]} def to_hash(full_messages = false) if full_messages messages = {} @@ -289,7 +289,7 @@ module ActiveModel # # => NameIsInvalid: name is invalid # # person.errors.messages # => {} - def add(attribute, message = nil, options = {}) + def add(attribute, message = :invalid, options = {}) message = normalize_message(attribute, message, options) if exception = options[:strict] exception = ActiveModel::StrictValidationFailed if exception == true @@ -331,7 +331,7 @@ module ActiveModel # # person.errors.add :name, :blank # person.errors.added? :name, :blank # => true - def added?(attribute, message = nil, options = {}) + def added?(attribute, message = :invalid, options = {}) message = normalize_message(attribute, message, options) self[attribute].include? message end @@ -437,8 +437,6 @@ module ActiveModel private def normalize_message(attribute, message, options) - message ||= :invalid - case message when Symbol generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index 62383a03e8..f048dda5c6 100644 --- a/activemodel/lib/active_model/model.rb +++ b/activemodel/lib/active_model/model.rb @@ -79,6 +79,8 @@ module ActiveModel params.each do |attr, value| self.public_send("#{attr}=", value) end if params + + super() end # Indicates if the model is persisted. Default is +false+. diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index bc9edf4a56..11ebfe6cc0 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -262,10 +262,10 @@ module ActiveModel # namespaced models regarding whether it's inside isolated engine. # # # For isolated engine: - # ActiveModel::Naming.singular_route_key(Blog::Post) #=> post + # ActiveModel::Naming.singular_route_key(Blog::Post) # => "post" # # # For shared engine: - # ActiveModel::Naming.singular_route_key(Blog::Post) #=> blog_post + # ActiveModel::Naming.singular_route_key(Blog::Post) # => "blog_post" def self.singular_route_key(record_or_class) model_name_from_record_or_class(record_or_class).singular_route_key end @@ -274,10 +274,10 @@ module ActiveModel # namespaced models regarding whether it's inside isolated engine. # # # For isolated engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> posts + # ActiveModel::Naming.route_key(Blog::Post) # => "posts" # # # For shared engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts + # ActiveModel::Naming.route_key(Blog::Post) # => "blog_posts" # # The route key also considers if the noun is uncountable and, in # such cases, automatically appends _index. @@ -289,10 +289,10 @@ module ActiveModel # namespaced models regarding whether it's inside isolated engine. # # # For isolated engine: - # ActiveModel::Naming.param_key(Blog::Post) #=> post + # ActiveModel::Naming.param_key(Blog::Post) # => "post" # # # For shared engine: - # ActiveModel::Naming.param_key(Blog::Post) #=> blog_post + # ActiveModel::Naming.param_key(Blog::Post) # => "blog_post" def self.param_key(record_or_class) model_name_from_record_or_class(record_or_class).param_key end diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 750fd723a0..7e694b5c50 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -2,7 +2,9 @@ module ActiveModel module SecurePassword extend ActiveSupport::Concern - class << self; attr_accessor :min_cost; end + class << self + attr_accessor :min_cost # :nodoc: + end self.min_cost = false module ClassMethods @@ -15,12 +17,12 @@ module ActiveModel # argument. You can add more validations by hand if need be. # # If you don't need the confirmation validation, just don't set any - # value to the password_confirmation attribute and the the validation + # value to the password_confirmation attribute and the validation # will not be triggered. # - # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password: + # You need to add bcrypt-ruby (~> 3.1.2) to Gemfile to use #has_secure_password: # - # gem 'bcrypt-ruby', '~> 3.0.0' + # gem 'bcrypt-ruby', '~> 3.1.2' # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # @@ -44,7 +46,6 @@ module ActiveModel # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. begin - gem 'bcrypt-ruby', '~> 3.0.0' require 'bcrypt' rescue LoadError $stderr.puts "You don't have bcrypt-ruby installed in your application. Please add it to your Gemfile and run bundle install" @@ -56,8 +57,9 @@ module ActiveModel include InstanceMethodsOnActivation if options.fetch(:validations, true) - validates_confirmation_of :password + validates_confirmation_of :password, if: :should_confirm_password? validates_presence_of :password, on: :create + validates_presence_of :password_confirmation, if: :should_confirm_password? before_create { raise "Password digest missing on new record" if password_digest.blank? } end @@ -100,15 +102,19 @@ module ActiveModel def password=(unencrypted_password) unless unencrypted_password.blank? @password = unencrypted_password - cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST + cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) end end def password_confirmation=(unencrypted_password) - unless unencrypted_password.blank? - @password_confirmation = unencrypted_password - end + @password_confirmation = unencrypted_password + end + + private + + def should_confirm_password? + password_confirmation && password.present? end end end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 9d984b7a18..05e2e089e5 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -109,7 +109,7 @@ module ActiveModel # # def attributes=(hash) # hash.each do |key, value| - # instance_variable_set("@#{key}", value) + # send("#{key}=", value) # end # end # diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 2803f69b6f..7f99536dbb 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/slice' @@ -205,7 +205,7 @@ module ActiveModel Serializer.new(self, options).serialize(&block) end - # Sets the model +attributes+ from a JSON string. Returns +self+. + # Sets the model +attributes+ from an XML string. Returns +self+. # # class Person # include ActiveModel::Serializers::Xml diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 92206450d2..31c2245265 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -142,7 +142,9 @@ module ActiveModel if options.key?(:on) options = options.dup options[:if] = Array(options[:if]) - options[:if].unshift("validation_context == :#{options[:on]}") + options[:if].unshift lambda { |o| + o.validation_context == options[:on] + } end args << options set_callback(:validate, *args, &block) @@ -226,7 +228,6 @@ module ActiveModel # Person.validators_on(:name) # # => [ # # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>, - # # #<ActiveModel::Validations::InclusionValidator:0x007fe603bb8780 @attributes=[:age], @options={in:0..99}> # # ] def validators_on(*attributes) attributes.flat_map do |attribute| diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 78e6f67a47..139de16326 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -4,6 +4,7 @@ module ActiveModel class AcceptanceValidator < EachValidator # :nodoc: def initialize(options) super({ allow_nil: true, accept: "1" }.merge!(options)) + setup!(options[:class]) end def validate_each(record, attribute, value) @@ -12,7 +13,8 @@ module ActiveModel end end - def setup(klass) + private + def setup!(klass) attr_readers = attributes.reject { |name| klass.attribute_method?(name) } attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } klass.send(:attr_reader, *attr_readers) diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index cabb9482f2..fde53b9f89 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -58,7 +58,9 @@ module ActiveModel if options.is_a?(Hash) && options[:on] options[:if] = Array(options[:if]) options[:on] = Array(options[:on]) - options[:if].unshift("#{options[:on]}.include? self.validation_context") + options[:if].unshift lambda { |o| + options[:on].include? o.validation_context + } end set_callback(:validation, :before, *args, &block) end diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb index 49df98d6c1..bad9e4f9a9 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -15,26 +15,36 @@ module ActiveModel private def include?(record, value) - exclusions = if delimiter.respond_to?(:call) - delimiter.call(record) - elsif delimiter.respond_to?(:to_sym) - record.send(delimiter) - else - delimiter - end + members = if delimiter.respond_to?(:call) + delimiter.call(record) + elsif delimiter.respond_to?(:to_sym) + record.send(delimiter) + else + delimiter + end - exclusions.send(inclusion_method(exclusions), value) + members.send(inclusion_method(members), value) end def delimiter @delimiter ||= options[:in] || options[:within] end - # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the - # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> - # uses the previous logic of comparing a value with the range endpoints. + # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all + # possible values in the range for equality, which is slower but more accurate. + # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range + # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges. def inclusion_method(enumerable) - enumerable.is_a?(Range) ? :cover? : :include? + if enumerable.is_a? Range + case enumerable.first + when Numeric, Time, DateTime + :cover? + else + :include? + end + else + :include? + end end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 1d85378892..b0542661af 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -2,6 +2,11 @@ module ActiveModel module Validations class ConfirmationValidator < EachValidator # :nodoc: + def initialize(options) + super + setup!(options[:class]) + end + def validate_each(record, attribute, value) if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) human_attribute_name = record.class.human_attribute_name(attribute) @@ -9,7 +14,8 @@ module ActiveModel end end - def setup(klass) + private + def setup!(klass) klass.send(:attr_reader, *attributes.map do |attribute| :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation") end.compact) diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index be7cae588f..f0fe22438f 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -17,8 +17,8 @@ module ActiveModel raise ArgumentError, "Either :with or :without must be supplied (but not both)" end - check_options_validity(options, :with) - check_options_validity(options, :without) + check_options_validity :with + check_options_validity :without end private @@ -32,21 +32,23 @@ module ActiveModel record.errors.add(attribute, :invalid, options.except(name).merge!(value: value)) end - def regexp_using_multiline_anchors?(regexp) - regexp.source.start_with?("^") || - (regexp.source.end_with?("$") && !regexp.source.end_with?("\\$")) + def check_options_validity(name) + if option = options[name] + if option.is_a?(Regexp) + if options[:multiline] != true && regexp_using_multiline_anchors?(option) + raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \ + "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \ + ":multiline => true option?" + end + elsif !option.respond_to?(:call) + raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" + end + end end - def check_options_validity(options, name) - option = options[name] - if option && !option.is_a?(Regexp) && !option.respond_to?(:call) - raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" - elsif option && option.is_a?(Regexp) && - regexp_using_multiline_anchors?(option) && options[:multiline] != true - raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \ - "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \ - ":multiline => true option?" - end + def regexp_using_multiline_anchors?(regexp) + source = regexp.source + source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$")) end end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 1cfd86efee..24337614c5 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -28,7 +28,7 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of available items. This can be # supplied as a proc, lambda or symbol which returns an enumerable. If the - # enumerable is a range the test is performed with <tt>Range#cover?</tt>, + # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>, # otherwise with <tt>include?</tt>. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # * <tt>:message</tt> - Specifies a custom error message (default is: "is diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index c6abe45f4a..c8d3236463 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -11,8 +11,9 @@ module ActiveModel def check_validity! keys = CHECKS.keys - [:odd, :even] options.slice(*keys).each do |option, value| - next if value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol) - 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) + raise ArgumentError, ":#{option} must be a number, a symbol or a proc" + end end end @@ -43,11 +44,15 @@ module ActiveModel record.errors.add(attr_name, option, filtered_options(value)) end else - option_value = option_value.call(record) if option_value.is_a?(Proc) - option_value = record.send(option_value) if option_value.is_a?(Symbol) + case option_value + when Proc + option_value = option_value.call(record) + when Symbol + option_value = record.send(option_value) + end unless value.send(CHECKS[option], option_value) - record.errors.add(attr_name, option, filtered_options(value).merge(count: option_value)) + record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value)) end end end @@ -56,16 +61,9 @@ module ActiveModel protected def parse_raw_value_as_a_number(raw_value) - case raw_value - when /\A0[xX]/ - nil - else - begin - Kernel.Float(raw_value) - rescue ArgumentError, TypeError - nil - end - end + Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/ + rescue ArgumentError, TypeError + nil end def parse_raw_value_as_an_integer(raw_value) @@ -73,7 +71,9 @@ module ActiveModel end def filtered_options(value) - options.except(*RESERVED_OPTIONS).merge!(value: value) + filtered = options.except(*RESERVED_OPTIONS) + filtered[:value] = value + filtered end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 9a1ff2ad39..bf588b7bd0 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -13,7 +13,7 @@ module ActiveModel # validates :terms, acceptance: true # validates :password, confirmation: true # validates :username, exclusion: { in: %w(admin superuser) } - # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, on: :create } + # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create } # validates :age, inclusion: { in: 0..9 } # validates :first_name, length: { maximum: 30 } # validates :age, numericality: true diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 2ae335d0f4..16bd6670d1 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -83,9 +83,10 @@ module ActiveModel # end def validates_with(*args, &block) options = args.extract_options! + options[:class] = self + args.each do |klass| validator = klass.new(options, &block) - validator.setup(self) if validator.respond_to?(:setup) if validator.respond_to?(:attributes) && !validator.attributes.empty? validator.attributes.each do |attribute| diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 037650e5ac..690856aee1 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -82,18 +82,16 @@ module ActiveModel # validates :title, presence: true # end # - # Validator may also define a +setup+ instance method which will get called - # with the class that using that validator as its argument. This can be - # useful when there are prerequisites such as an +attr_accessor+ being present. + # It can be useful to access the class that is using that validator when there are prerequisites such + # as an +attr_accessor+ being present. This class is accessable via +options[:class]+ in the constructor. + # To setup your validator override the constructor. # # class MyValidator < ActiveModel::Validator - # def setup(klass) - # klass.send :attr_accessor, :custom_attribute + # def initialize(options={}) + # super + # options[:class].send :attr_accessor, :custom_attribute # end # end - # - # This setup method is only called when used with validation macros or the - # class level <tt>validates_with</tt> method. class Validator attr_reader :options @@ -107,7 +105,8 @@ module ActiveModel # Accepts options that will be made available through the +options+ reader. def initialize(options = {}) - @options = options.freeze + @options = options.except(:class).freeze + deprecated_setup(options) end # Return the kind for this validator. @@ -123,6 +122,21 @@ module ActiveModel def validate(record) raise NotImplementedError, "Subclasses must implement a validate(record) method." end + + private + def deprecated_setup(options) # TODO: remove me in 4.2. + return unless respond_to?(:setup) + ActiveSupport::Deprecation.warn "The `Validator#setup` instance method is deprecated and will be removed on Rails 4.2. Do your setup in the constructor instead: + +class MyValidator < ActiveModel::Validator + def initialize(options={}) + super + options[:class].send :attr_accessor, :custom_attribute + end +end +" + setup(options[:class]) + end end # +EachValidator+ is a validator which iterates through the attributes given |