diff options
Diffstat (limited to 'activemodel/lib')
34 files changed, 671 insertions, 409 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 3bd5531356..46d60db756 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -37,7 +37,6 @@ module ActiveModel autoload :ForbiddenAttributesProtection autoload :Lint autoload :Model - autoload :DeprecatedMassAssignmentSecurity autoload :Name, 'active_model/naming' autoload :Naming autoload :SecurePassword @@ -49,6 +48,7 @@ module ActiveModel eager_autoload do autoload :Errors + autoload :StrictValidationFailed, 'active_model/errors' end module Serializers @@ -60,9 +60,9 @@ module ActiveModel end end - def eager_load! + def self.eager_load! super - ActiveModel::Serializer.eager_load! + ActiveModel::Serializers.eager_load! end end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 6d11c0fbdc..96be551264 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. @@ -12,19 +13,23 @@ module ActiveModel # # => ActiveModel::MissingAttributeError: missing attribute: user_id class MissingAttributeError < NoMethodError end - # == Active \Model Attribute Methods + + # == Active \Model \Attribute \Methods # - # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and - # suffixes to your methods as well as handling the creation of Active Record - # like class methods such as +table_name+. + # Provides a way to add prefixes and suffixes to your methods as + # well as handling the creation of <tt>ActiveRecord::Base</tt>-like + # class methods such as +table_name+. # - # The requirements to implement ActiveModel::AttributeMethods are to: + # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to: # - # * <tt>include ActiveModel::AttributeMethods</tt> in your object. - # * Call each Attribute Method module method you want to add, such as - # +attribute_method_suffix+ or +attribute_method_prefix+. + # * <tt>include ActiveModel::AttributeMethods</tt> in your class. + # * Call each of its method you want to add, such as +attribute_method_suffix+ + # or +attribute_method_prefix+. # * Call +define_attribute_methods+ after the other methods are called. # * Define the various generic +_attribute+ methods that you have declared. + # * Define an +attributes+ method which returns a hash with each + # attribute name in your model as hash key and the attribute value as hash value. + # Hash keys must be strings. # # A minimal implementation could be: # @@ -38,6 +43,10 @@ module ActiveModel # # attr_accessor :name # + # def attributes + # { 'name' => @name } + # end + # # private # # def attribute_contrived?(attr) @@ -52,13 +61,6 @@ module ActiveModel # send("#{attr}=", 'Default Name') # end # end - # - # Note that whenever you include ActiveModel::AttributeMethods in your class, - # it requires you to implement an +attributes+ method which returns a hash - # with each attribute name in your model as hash key and the attribute value as - # hash value. - # - # Hash keys must be strings. module AttributeMethods extend ActiveSupport::Concern @@ -166,20 +168,19 @@ module ActiveModel # private # # def reset_attribute_to_default!(attr) - # ... + # send("#{attr}=", 'Default Name') # end # end # # person = Person.new # person.name # => 'Gem' # person.reset_name_to_default! - # person.name # => 'Gemma' + # person.name # => 'Default Name' def attribute_method_affix(*affixes) self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] } undefine_attribute_methods end - # Allows you to make aliases for attributes. # # class Person @@ -213,6 +214,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. # @@ -234,7 +245,7 @@ module ActiveModel # private # # def clear_attribute(attr) - # ... + # send("#{attr}=", nil) # end # end def define_attribute_methods(*attr_names) @@ -317,9 +328,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 @@ -332,23 +344,21 @@ 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 # this cache). def attribute_method_matchers_cache #:nodoc: - @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(:initial_capacity => 4) + @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4) end - def attribute_method_matcher(method_name) #:nodoc: + def attribute_method_matchers_matching(method_name) #:nodoc: attribute_method_matchers_cache.compute_if_absent(method_name) do # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix # will match every time. matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1) - match = nil - matchers.detect { |method| match = method.match(method_name) } - match + matchers.map { |method| method.match(method_name) }.compact end end @@ -383,14 +393,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}" @@ -413,17 +415,16 @@ module ActiveModel end end - # Allows access to the object attributes, which are held in the - # <tt>@attributes</tt> hash, as though they were first-class methods. So a - # Person class with a name attribute can use Person#name and Person#name= - # and never directly use the attributes hash -- except for multiple assigns - # with ActiveRecord#attributes=. A Milestone class can also ask - # Milestone#completed? to test that the completed attribute is not +nil+ - # or 0. + # Allows access to the object attributes, which are held in the hash + # returned by <tt>attributes</tt>, as though they were first-class + # methods. So a +Person+ class with a +name+ attribute can for example use + # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use + # the attributes hash -- except for multiple assigns with + # <tt>ActiveRecord::Base#attributes=</tt>. # - # It's also possible to instantiate related objects, so a Client class - # belonging to the clients table with a +master_id+ foreign key can - # instantiate master through Client#master. + # It's also possible to instantiate related objects, so a <tt>Client</tt> + # class belonging to the +clients+ table with a +master_id+ foreign key + # can instantiate master through <tt>Client#master</tt>. def method_missing(method, *args, &block) if respond_to_without_attributes?(method, true) super @@ -433,17 +434,17 @@ module ActiveModel end end - # attribute_missing is like method_missing, but for attributes. When method_missing is - # called we check to see if there is a matching attribute method. If so, we call - # attribute_missing to dispatch the attribute. This method can be overloaded to - # customize the behavior. + # +attribute_missing+ is like +method_missing+, but for attributes. When + # +method_missing+ is called we check to see if there is a matching + # attribute method. If so, we tell +attribute_missing+ to dispatch the + # attribute. This method can be overloaded to customize the behavior. def attribute_missing(match, *args, &block) __send__(match.target, match.attr_name, *args, &block) end - # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, - # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> - # which will all return +true+. + # A +Person+ instance with a +name+ attribute can ask + # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>, + # and <tt>person.respond_to?(:name?)</tt> which will all return +true+. alias :respond_to_without_attributes? :respond_to? def respond_to?(method, include_private_methods = false) if super @@ -466,8 +467,8 @@ module ActiveModel # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. def match_attribute_method?(method_name) - match = self.class.send(:attribute_method_matcher, method_name) - match if match && attribute_method?(match.attr_name) + matches = self.class.send(:attribute_method_matchers_matching, method_name) + matches.detect { |match| attribute_method?(match.attr_name) } end def missing_attribute(attr_name, stack) diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index c52e4947ae..b3d70dc515 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/extract_options' + module ActiveModel # == Active \Model \Callbacks # @@ -28,7 +30,7 @@ module ActiveModel # end # # Then in your class, you can use the +before_create+, +after_create+ and - # +around_create+ methods, just as you would in an Active Record module. + # +around_create+ methods, just as you would in an Active Record model. # # before_create :action_before_create # @@ -95,13 +97,16 @@ module ActiveModel # # obj is the MyModel instance that the callback is being called on # end # end + # + # NOTE: +method_name+ passed to `define_model_callbacks` must not end with + # `!`, `?` or `=`. def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { - :terminator => "result == false", - :skip_after_callbacks_if_terminated => true, - :scope => [:kind, :name], - :only => [:before, :around, :after] + terminator: ->(_,result) { result == false }, + skip_after_callbacks_if_terminated: true, + scope: [:kind, :name], + only: [:before, :around, :after] }.merge!(options) types = Array(options.delete(:only)) @@ -118,30 +123,27 @@ module ActiveModel private def _define_before_model_callback(klass, callback) #:nodoc: - klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1 - def self.before_#{callback}(*args, &block) - set_callback(:#{callback}, :before, *args, &block) - end - CALLBACK + klass.define_singleton_method("before_#{callback}") do |*args, &block| + set_callback(:"#{callback}", :before, *args, &block) + end end def _define_around_model_callback(klass, callback) #:nodoc: - klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1 - def self.around_#{callback}(*args, &block) - set_callback(:#{callback}, :around, *args, &block) - end - CALLBACK + klass.define_singleton_method("around_#{callback}") do |*args, &block| + set_callback(:"#{callback}", :around, *args, &block) + end end def _define_after_model_callback(klass, callback) #:nodoc: - klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1 - def self.after_#{callback}(*args, &block) - options = args.extract_options! - options[:prepend] = true - options[:if] = Array(options[:if]) << "value != false" - set_callback(:#{callback}, :after, *(args << options), &block) - end - CALLBACK + klass.define_singleton_method("after_#{callback}") do |*args, &block| + options = args.extract_options! + options[:prepend] = true + conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v| + v != false + } + options[:if] = Array(options[:if]) << conditional + set_callback(:"#{callback}", :after, *(args << options), &block) + end end end end diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 1f5d23dd8e..9c9b6f4a77 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,5 +1,5 @@ module ActiveModel - # == Active \Model Conversions + # == Active \Model \Conversion # # Handles default conversions: to_model, to_key, to_param, and to_partial_path. # @@ -40,13 +40,15 @@ module ActiveModel self end - # Returns an Enumerable of all key attributes if any is set, regardless if - # the object is persisted or not. If there no key attributes, returns +nil+. + # Returns an Array of all key attributes if any is set, regardless if + # the object is persisted or not. Returns +nil+ if there are no key attributes. # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Conversion + # attr_accessor :id # end # - # person = Person.create + # person = Person.create(id: 1) # person.to_key # => [1] def to_key key = respond_to?(:id) && id @@ -56,13 +58,18 @@ module ActiveModel # Returns a +string+ representing the object's key suitable for use in URLs, # or +nil+ if <tt>persisted?</tt> is +false+. # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Conversion + # attr_accessor :id + # def persisted? + # true + # end # end # - # person = Person.create + # person = Person.create(id: 1) # person.to_param # => "1" def to_param - persisted? ? to_key.join('-') : nil + (persisted? && key = to_key) ? key.join('-') : nil end # Returns a +string+ identifying the path associated with the object. @@ -83,8 +90,8 @@ module ActiveModel # internal method and should not be accessed directly. def _to_partial_path #:nodoc: @_to_partial_path ||= begin - element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)) - collection = ActiveSupport::Inflector.tableize(self) + element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name)) + collection = ActiveSupport::Inflector.tableize(name) "#{collection}/#{element}".freeze 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 6e67cd2285..337b61c55c 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,5 +1,6 @@ require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' +require 'active_support/core_ext/string/filters' module ActiveModel # == Active \Model \Dirty @@ -12,15 +13,12 @@ module ActiveModel # * <tt>include ActiveModel::Dirty</tt> in your object. # * Call <tt>define_attribute_methods</tt> passing each method you want to # track. - # * Call <tt>attr_name_will_change!</tt> before each change to the tracked + # * 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>clear_changes_information</tt> when you want to reset the changes + # information. + # * Call <tt>restore_attributes</tt> when you want to restore previous data. # # A minimal implementation could be: # @@ -39,14 +37,25 @@ module ActiveModel # end # # def save - # @previously_changed = changes - # @changed_attributes.clear + # # do persistence work + # + # changes_applied + # end + # + # def reload! + # # get the values from the persistence layer + # + # clear_changes_information + # end + # + # def rollback! + # restore_attributes # end # end # # A newly instantiated object is unchanged: # - # person = Person.find_by_name('Uncle Bob') + # person = Person.find_by(name: 'Uncle Bob') # person.changed? # => false # # Change the name: @@ -54,6 +63,7 @@ module ActiveModel # person.name = 'Bob' # person.changed? # => true # person.name_changed? # => true + # person.name_changed?(from: "Uncle Bob", to: "Bob") # => true # person.name_was # => "Uncle Bob" # person.name_change # => ["Uncle Bob", "Bob"] # person.name = 'Bill' @@ -65,6 +75,19 @@ module ActiveModel # person.changed? # => false # person.name_changed? # => false # + # Reset the changes: + # + # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.reload! + # person.previous_changes # => {} + # + # Rollback the changes: + # + # person.name = "Uncle Bob" + # person.rollback! + # person.name # => "Bill" + # person.name_changed? # => false + # # Assigning the same value leaves the attribute unchanged: # # person.name = 'Bill' @@ -77,9 +100,11 @@ module ActiveModel # person.changed # => ["name"] # person.changes # => {"name" => ["Bill", "Bob"]} # - # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> - # to mark that the attribute is changing. Otherwise ActiveModel can't track - # changes to in-place attributes. + # If an attribute is modified in-place then make use of + # +[attribute_name]_will_change!+ to mark that the attribute is changing. + # Otherwise Active Model can't track changes to in-place attributes. Note + # that Active Record can detect in-place modifications automatically. You do + # not need to call +[attribute_name]_will_change!+ on Active Record models. # # person.name_will_change! # person.name_change # => ["Bill", "Bill"] @@ -91,7 +116,8 @@ module ActiveModel included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' - attribute_method_affix :prefix => 'reset_', :suffix => '!' + attribute_method_affix prefix: 'reset_', suffix: '!' + attribute_method_affix prefix: 'restore_', suffix: '!' end # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. @@ -129,7 +155,7 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed + @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new end # Returns a hash of the attributes with unsaved changes indicating their original @@ -139,14 +165,52 @@ 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, options = {}) #:nodoc: + result = changes_include?(attr) + result &&= options[:to] == __send__(attr) if options.key?(:to) + result &&= options[:from] == changed_attributes[attr] if options.key?(:from) + result + end + + # Handle <tt>*_was</tt> for +method_missing+. + def attribute_was(attr) # :nodoc: + attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) + end + + # Restore all previous data of the provided attributes. + def restore_attributes(attributes = changed) + attributes.each { |attr| restore_attribute! attr } end private - # Handle <tt>*_changed?</tt> for +method_missing+. - def attribute_changed?(attr) - changed_attributes.include?(attr) + def changes_include?(attr_name) + attributes_changed_by_setter.include?(attr_name) + end + + # Removes current changes and makes them accessible through +previous_changes+. + def changes_applied # :doc: + @previously_changed = changes + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new + end + + # Clear all dirty data: current changes and previous changes. + def clear_changes_information # :doc: + @previously_changed = ActiveSupport::HashWithIndifferentAccess.new + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new + end + + def reset_changes + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `#reset_changes` is deprecated and will be removed on Rails 5. + Please use `#clear_changes_information` instead. + MSG + + clear_changes_information end # Handle <tt>*_change</tt> for +method_missing+. @@ -154,11 +218,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) @@ -169,15 +228,39 @@ module ActiveModel rescue TypeError, NoMethodError end - changed_attributes[attr] = value + set_attribute_was(attr, value) end # Handle <tt>reset_*!</tt> for +method_missing+. def reset_attribute!(attr) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `#reset_#{attr}!` is deprecated and will be removed on Rails 5. + Please use `#restore_#{attr}!` instead. + MSG + + restore_attribute!(attr) + end + + # Handle <tt>restore_*!</tt> for +method_missing+. + def restore_attribute!(attr) if attribute_changed?(attr) __send__("#{attr}=", changed_attributes[attr]) - changed_attributes.delete(attr) + clear_attribute_changes([attr]) end end + + # This is necessary because `changed_attributes` might be overridden in + # other implemntations (e.g. in `ActiveRecord`) + alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc: + + # Force an attribute to have a particular "before" value + def set_attribute_was(attr, old_value) + attributes_changed_by_setter[attr] = old_value + end + + # Remove changes information for the provided attributes. + def clear_attribute_changes(attributes) # :doc: + attributes_changed_by_setter.except!(*attributes) + end end end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 963e52bff3..55687cb3c7 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -7,12 +7,11 @@ module ActiveModel # == Active \Model \Errors # # Provides a modified +Hash+ that you can include in your object - # for handling error messages and interacting with Action Pack helpers. + # for handling error messages and interacting with Action View helpers. # # A minimal implementation could be: # # class Person - # # # Required dependency for ActiveModel::Errors # extend ActiveModel::Naming # @@ -24,7 +23,7 @@ module ActiveModel # attr_reader :errors # # def validate! - # errors.add(:name, "can not be nil") if name == nil + # errors.add(:name, "cannot be nil") if name.nil? # end # # # The following methods are needed to be minimally implemented @@ -40,7 +39,6 @@ module ActiveModel # def Person.lookup_ancestors # [self] # end - # # end # # The last three methods are required in your object for Errors to be @@ -52,9 +50,9 @@ module ActiveModel # # The above allows you to do: # - # p = Person.new - # person.validate! # => ["can not be nil"] - # person.errors.full_messages # => ["name can not be nil"] + # person = Person.new + # person.validate! # => ["cannot be nil"] + # person.errors.full_messages # => ["name cannot be nil"] # # etc.. class Errors include Enumerable @@ -82,7 +80,7 @@ module ActiveModel # Clear the error messages. # - # person.errors.full_messages # => ["name can not be nil"] + # person.errors.full_messages # => ["name cannot be nil"] # person.errors.clear # person.errors.full_messages # => [] def clear @@ -92,19 +90,21 @@ module ActiveModel # Returns +true+ if the error messages include an error for the given key # +attribute+, +false+ otherwise. # - # person.errors.messages # => {:name=>["can not be nil"]} + # person.errors.messages # => {:name=>["cannot be nil"]} # person.errors.include?(:name) # => true # person.errors.include?(:age) # => false def include?(attribute) - (v = messages[attribute]) && v.any? + messages[attribute].present? end # aliases include? alias :has_key? :include? + # aliases include? + alias :key? :include? # Get messages for +key+. # - # person.errors.messages # => {:name=>["can not be nil"]} - # person.errors.get(:name) # => ["can not be nil"] + # person.errors.messages # => {:name=>["cannot be nil"]} + # person.errors.get(:name) # => ["cannot be nil"] # person.errors.get(:age) # => nil def get(key) messages[key] @@ -112,7 +112,7 @@ module ActiveModel # Set messages for +key+ to +value+. # - # person.errors.get(:name) # => ["can not be nil"] + # person.errors.get(:name) # => ["cannot be nil"] # person.errors.set(:name, ["can't be nil"]) # person.errors.get(:name) # => ["can't be nil"] def set(key, value) @@ -121,8 +121,8 @@ module ActiveModel # Delete messages for +key+. Returns the deleted messages. # - # person.errors.get(:name) # => ["can not be nil"] - # person.errors.delete(:name) # => ["can not be nil"] + # person.errors.get(:name) # => ["cannot be nil"] + # person.errors.delete(:name) # => ["cannot be nil"] # person.errors.get(:name) # => nil def delete(key) messages.delete(key) @@ -131,8 +131,8 @@ module ActiveModel # When passed a symbol or a name of a method, returns an array of errors # for the method. # - # person.errors[:name] # => ["can not be nil"] - # person.errors['name'] # => ["can not be nil"] + # person.errors[:name] # => ["cannot be nil"] + # person.errors['name'] # => ["cannot be nil"] def [](attribute) get(attribute.to_sym) || set(attribute.to_sym, []) end @@ -177,15 +177,15 @@ module ActiveModel # Returns all message values. # - # person.errors.messages # => {:name=>["can not be nil", "must be specified"]} - # person.errors.values # => [["can not be nil", "must be specified"]] + # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} + # person.errors.values # => [["cannot be nil", "must be specified"]] def values messages.values end # Returns all message keys. # - # person.errors.messages # => {:name=>["can not be nil", "must be specified"]} + # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} # person.errors.keys # => [:name] def keys messages.keys @@ -213,7 +213,7 @@ module ActiveModel # Returns +true+ if no errors are found, +false+ otherwise. # If the error message is a string it can be empty. # - # person.errors.full_messages # => ["name can not be nil"] + # person.errors.full_messages # => ["name cannot be nil"] # person.errors.empty? # => false def empty? all? { |k, v| v && v.empty? && !v.is_a?(String) } @@ -233,15 +233,15 @@ module ActiveModel # # <error>name must be specified</error> # # </errors> def to_xml(options={}) - to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options)) + to_a.to_xml({ root: "errors", skip_types: true }.merge!(options)) end # Returns a Hash that can be used as the JSON representation for this # 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=>["cannot be nil"]} + # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]} def as_json(options=nil) to_hash(options && options[:full_messages]) end @@ -249,15 +249,13 @@ 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=>["cannot be nil"]} + # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]} def to_hash(full_messages = false) if full_messages - messages = {} - self.messages.each do |attribute, array| + self.messages.each_with_object({}) do |(attribute, array), messages| messages[attribute] = array.map { |message| full_message(attribute, message) } end - messages else self.messages.dup end @@ -281,17 +279,24 @@ module ActiveModel # If +message+ is a proc, it will be called, allowing for things like # <tt>Time.now</tt> to be used within an error. # - # If the <tt>:strict</tt> option is set to true will raise + # If the <tt>:strict</tt> option is set to +true+, it will raise # ActiveModel::StrictValidationFailed instead of adding the error. # <tt>:strict</tt> option can also be set to any other exception. # - # person.errors.add(:name, nil, strict: true) + # person.errors.add(:name, :invalid, strict: true) # # => ActiveModel::StrictValidationFailed: name is invalid - # person.errors.add(:name, nil, strict: NameIsInvalid) + # person.errors.add(:name, :invalid, strict: NameIsInvalid) # # => NameIsInvalid: name is invalid # # person.errors.messages # => {} - def add(attribute, message = nil, options = {}) + # + # +attribute+ should be set to <tt>:base</tt> if the error is not + # directly associated with a single attribute. + # + # person.errors.add(:base, "either name or email must be present") + # person.errors.messages + # # => {:base=>["either name or email must be present"]} + def add(attribute, message = :invalid, options = {}) message = normalize_message(attribute, message, options) if exception = options[:strict] exception = ActiveModel::StrictValidationFailed if exception == true @@ -333,7 +338,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 @@ -352,17 +357,31 @@ module ActiveModel map { |attribute, message| full_message(attribute, message) } end + # Returns all the full error messages for a given attribute in an array. + # + # class Person + # validates_presence_of :name, :email + # validates_length_of :name, in: 5..30 + # end + # + # person = Person.create() + # person.errors.full_messages_for(:name) + # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"] + def full_messages_for(attribute) + (get(attribute) || []).map { |message| full_message(attribute, message) } + end + # Returns a full message for a given attribute. # # person.errors.full_message(:name, 'is invalid') # => "Name is invalid" def full_message(attribute, message) return message if attribute == :base attr_name = attribute.to_s.tr('.', '_').humanize - attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) + attr_name = @base.class.human_attribute_name(attribute, default: attr_name) I18n.t(:"errors.format", { - :default => "%{attribute} %{message}", - :attribute => attr_name, - :message => message + default: "%{attribute} %{message}", + attribute: attr_name, + message: message }) end @@ -414,10 +433,10 @@ module ActiveModel value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil) options = { - :default => defaults, - :model => @base.class.model_name.human, - :attribute => @base.class.human_attribute_name(attribute), - :value => value + default: defaults, + model: @base.model_name.human, + attribute: @base.class.human_attribute_name(attribute), + value: value }.merge!(options) I18n.translate(key, options) @@ -425,8 +444,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/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb index 7468f95548..b4fa378601 100644 --- a/activemodel/lib/active_model/forbidden_attributes_protection.rb +++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb @@ -23,5 +23,6 @@ module ActiveModel attributes end end + alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment end end diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb new file mode 100644 index 0000000000..2403242ce6 --- /dev/null +++ b/activemodel/lib/active_model/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveModel + # Returns the version of the currently loaded Active Model as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 5 + MINOR = 0 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 1be2913f0b..38087521a2 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -13,7 +13,7 @@ module ActiveModel # # These tests do not attempt to determine the semantic correctness of the # returned values. For instance, you could implement <tt>valid?</tt> to - # always return true, and the tests would pass. It is up to you to ensure + # 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 @@ -73,16 +73,19 @@ module ActiveModel # == \Naming # - # Model.model_name must return a string with some convenience methods: - # <tt>:human</tt>, <tt>:singular</tt> and <tt>:plural</tt>. Check - # ActiveModel::Naming for more information. + # Model.model_name and Model#model_name must return a string with some + # convenience methods: # <tt>:human</tt>, <tt>:singular</tt> and + # <tt>:plural</tt>. Check ActiveModel::Naming for more information. def test_model_naming - assert model.class.respond_to?(:model_name), "The model should respond to model_name" + assert model.class.respond_to?(:model_name), "The model class should respond to model_name" model_name = model.class.model_name assert model_name.respond_to?(:to_str) assert model_name.human.respond_to?(:to_str) assert model_name.singular.respond_to?(:to_str) assert model_name.plural.respond_to?(:to_str) + + assert model.respond_to?(:model_name), "The model instance should respond to model_name" + assert_equal model.model_name, model.class.model_name end # == \Errors Testing @@ -98,7 +101,7 @@ module ActiveModel private def model - assert @model.respond_to?(:to_model), "The object should respond_to to_model" + assert @model.respond_to?(:to_model), "The object should respond to to_model" @model.to_model end diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index 540e8132d3..bf07945fe1 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -14,9 +14,15 @@ en: empty: "can't be empty" blank: "can't be blank" present: "must be blank" - too_long: "is too long (maximum is %{count} characters)" - too_short: "is too short (minimum is %{count} characters)" - wrong_length: "is the wrong length (should be %{count} characters)" + too_long: + one: "is too long (maximum is 1 character)" + other: "is too long (maximum is %{count} characters)" + too_short: + one: "is too short (minimum is 1 character)" + other: "is too short (minimum is %{count} characters)" + wrong_length: + one: "is the wrong length (should be 1 character)" + other: "is the wrong length (should be %{count} characters)" not_a_number: "is not a number" not_an_integer: "must be an integer" greater_than: "must be greater than %{count}" diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index 62383a03e8..d51d6ddcc9 100644 --- a/activemodel/lib/active_model/model.rb +++ b/activemodel/lib/active_model/model.rb @@ -1,6 +1,6 @@ module ActiveModel - # == Active \Model Basic \Model + # == Active \Model \Basic \Model # # Includes the required interface for an object to interact with # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules. @@ -16,8 +16,8 @@ module ActiveModel # end # # person = Person.new(name: 'bob', age: '18') - # person.name # => 'bob' - # person.age # => 18 + # person.name # => "bob" + # person.age # => "18" # # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt> # to return +false+, which is the most common case. You may want to override @@ -56,13 +56,13 @@ module ActiveModel # refer to the specific modules included in <tt>ActiveModel::Model</tt> # (see below). module Model - def self.included(base) #:nodoc: - base.class_eval do - extend ActiveModel::Naming - extend ActiveModel::Translation - include ActiveModel::Validations - include ActiveModel::Conversion - end + extend ActiveSupport::Concern + include ActiveModel::Validations + include ActiveModel::Conversion + + included do + extend ActiveModel::Naming + extend ActiveModel::Translation end # Initializes a new model with the given +params+. @@ -74,11 +74,13 @@ module ActiveModel # # person = Person.new(name: 'bob', age: '18') # person.name # => "bob" - # person.age # => 18 + # person.age # => "18" def initialize(params={}) 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 6887f6d781..4e6b02c246 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -129,7 +129,7 @@ module ActiveModel # # Equivalent to +to_s+. delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, - :to_str, :to => :name + :to_str, to: :name # Returns a new ActiveModel::Name instance. By default, the +namespace+ # and +name+ option will take the namespace and name of the given class @@ -183,7 +183,7 @@ module ActiveModel defaults << options[:default] if options[:default] defaults << @human - options = { :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults }.merge!(options.except(:default)) + options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default)) I18n.translate(defaults.shift, options) end @@ -204,7 +204,7 @@ module ActiveModel # extend ActiveModel::Naming # end # - # BookCover.model_name # => "BookCover" + # BookCover.model_name.name # => "BookCover" # BookCover.model_name.human # => "Book cover" # # BookCover.model_name.i18n_key # => :book_cover @@ -214,14 +214,20 @@ module ActiveModel # is required to pass the Active Model Lint test. So either extending the # provided method below, or rolling your own is required. module Naming + def self.extended(base) #:nodoc: + base.remove_possible_method :model_name + base.delegate :model_name, to: :class + end + # Returns an ActiveModel::Name object for module. It can be # used to retrieve all kinds of naming-related information # (See ActiveModel::Name for more information). # - # class Person < ActiveModel::Model + # class Person + # include ActiveModel::Model # end # - # Person.model_name # => Person + # Person.model_name.name # => "Person" # Person.model_name.class # => ActiveModel::Name # Person.model_name.singular # => "person" # Person.model_name.plural # => "people" @@ -262,10 +268,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 +280,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,21 +295,19 @@ 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 def self.model_name_from_record_or_class(record_or_class) #:nodoc: - if record_or_class.respond_to?(:model_name) - record_or_class.model_name - elsif record_or_class.respond_to?(:to_model) - record_or_class.to_model.class.model_name + if record_or_class.respond_to?(:to_model) + record_or_class.to_model.model_name else - record_or_class.class.model_name + record_or_class.model_name end end private_class_method :model_name_from_record_or_class diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 6644b60609..96e88f1b6c 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -2,25 +2,36 @@ module ActiveModel module SecurePassword extend ActiveSupport::Concern - class << self; attr_accessor :min_cost; end + # BCrypt hash function can handle maximum 72 characters, and if we pass + # password of length more than 72 characters it ignores extra characters. + # Hence need to put a restriction on password length. + MAX_PASSWORD_LENGTH_ALLOWED = 72 + + class << self + attr_accessor :min_cost # :nodoc: + end self.min_cost = false module ClassMethods # Adds methods to set and authenticate against a BCrypt password. - # This mechanism requires you to have a password_digest attribute. + # This mechanism requires you to have a +password_digest+ attribute. + # + # The following validations are added automatically: + # * Password must be present on creation + # * Password length should be less than or equal to 72 characters + # * Confirmation of password (using a +password_confirmation+ attribute) # - # Validations for presence of password on create, confirmation of password - # (using a +password_confirmation+ attribute) are automatically added. If - # you wish to turn off validations, pass <tt>validations: false</tt> as an - # argument. You can add more validations by hand if need be. + # If password confirmation validation is not needed, simply leave out the + # value for +password_confirmation+ (i.e. don't provide a form field for + # it). When this attribute has a +nil+ value, the validation will not be + # triggered. # - # If you don't need the confirmation validation, just don't set any - # value to the password_confirmation attribute and the the validation - # will not be triggered. + # For further customizability, it is possible to supress the default + # validations by passing <tt>validations: false</tt> as an argument. # - # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password: + # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password: # - # gem 'bcrypt-ruby', '~> 3.0.0' + # gem 'bcrypt', '~> 3.1.7' # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # @@ -30,33 +41,44 @@ module ActiveModel # end # # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch') - # user.save # => false, password required + # user.save # => false, password required # user.password = 'mUc3m00RsqyRe' - # user.save # => false, confirmation doesn't match + # user.save # => false, confirmation doesn't match # user.password_confirmation = 'mUc3m00RsqyRe' - # user.save # => true - # user.authenticate('notright') # => false - # user.authenticate('mUc3m00RsqyRe') # => user - # User.find_by_name('david').try(:authenticate, 'notright') # => false - # User.find_by_name('david').try(:authenticate, 'mUc3m00RsqyRe') # => user + # user.save # => true + # user.authenticate('notright') # => false + # user.authenticate('mUc3m00RsqyRe') # => user + # User.find_by(name: 'david').try(:authenticate, 'notright') # => false + # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user def has_secure_password(options = {}) - # Load bcrypt-ruby only when has_secure_password is used. + # Load bcrypt gem only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. - gem 'bcrypt-ruby', '~> 3.0.0' - require 'bcrypt' + begin + require 'bcrypt' + rescue LoadError + $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install" + raise + end - attr_reader :password + include InstanceMethodsOnActivation if options.fetch(:validations, true) - validates_confirmation_of :password - validates_presence_of :password, :on => :create + include ActiveModel::Validations - before_create { raise "Password digest missing on new record" if password_digest.blank? } - end + # This ensures the model has a password by checking whether the password_digest + # is present, so that this works with both new and existing records. However, + # when there is an error, the message is added to the password attribute instead + # so that the error message will make sense to the end-user. + validate do |record| + record.errors.add(:password, :blank) unless record.password_digest.present? + end - include InstanceMethodsOnActivation + validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED + validates_confirmation_of :password, allow_blank: true + end + # This code is necessary as long as the protected_attributes gem is supported. if respond_to?(:attributes_protected_by_default) def self.attributes_protected_by_default #:nodoc: super + ['password_digest'] @@ -77,11 +99,13 @@ module ActiveModel # user.authenticate('notright') # => false # user.authenticate('mUc3m00RsqyRe') # => user def authenticate(unencrypted_password) - BCrypt::Password.new(password_digest) == unencrypted_password && self + BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self end + attr_reader :password + # Encrypts the password into the +password_digest+ attribute, only if the - # new password is not blank. + # new password is not empty. # # class User < ActiveRecord::Base # has_secure_password validations: false @@ -93,12 +117,18 @@ module ActiveModel # user.password = 'mUc3m00RsqyRe' # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4." def password=(unencrypted_password) - unless unencrypted_password.blank? + if unencrypted_password.nil? + self.password_digest = nil + elsif !unencrypted_password.empty? @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) + @password_confirmation = unencrypted_password + end end end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index fdb06aebb9..976f50b13e 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -4,7 +4,7 @@ require 'active_support/core_ext/hash/slice' module ActiveModel # == Active \Model \Serialization # - # Provides a basic serialization to a serializable_hash for your object. + # Provides a basic serialization to a serializable_hash for your objects. # # A minimal implementation could be: # @@ -25,14 +25,14 @@ module ActiveModel # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # - # You need to declare an attributes hash which contains the attributes you - # want to serialize. Attributes must be strings, not symbols. When called, - # serializable hash will use instance methods that match the name of the - # attributes hash's keys. In order to override this behavior, take a look at - # the private method +read_attribute_for_serialization+. + # An +attributes+ hash must be defined and should contain any attributes you + # need to be serialized. Attributes must be strings, not symbols. + # When called, serializable hash will use instance methods that match the name + # of the attributes hash's keys. In order to override this behavior, take a look + # at the private method +read_attribute_for_serialization+. # - # Most of the time though, you will want to include the JSON or XML - # serializations. Both of these modules automatically include the + # Most of the time though, either the JSON or XML serializations are needed. + # Both of these modules automatically include the # <tt>ActiveModel::Serialization</tt> module, so there is no need to # explicitly include it. # @@ -128,7 +128,7 @@ module ActiveModel # retrieve the value for a given attribute differently: # # class MyClass - # include ActiveModel::Validations + # include ActiveModel::Serialization # # def initialize(data = {}) # @data = data diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 9d984b7a18..b66dbf1afe 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -2,7 +2,7 @@ require 'active_support/json' module ActiveModel module Serializers - # == Active Model JSON Serializer + # == Active \Model \JSON \Serializer module JSON extend ActiveSupport::Concern include ActiveModel::Serialization @@ -93,7 +93,7 @@ module ActiveModel end if root - root = self.class.model_name.element if root == true + root = model_name.element if root == true { root => serializable_hash(options) } else serializable_hash(options) @@ -109,7 +109,7 @@ module ActiveModel # # def attributes=(hash) # hash.each do |key, value| - # instance_variable_set("@#{key}", value) + # send("#{key}=", value) # end # end # @@ -130,10 +130,10 @@ module ActiveModel # # json = { person: { name: 'bob', age: 22, awesome:true } }.to_json # person = Person.new - # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob"> - # person.name # => "bob" - # person.age # => 22 - # person.awesome # => true + # person.from_json(json, true) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob"> + # person.name # => "bob" + # person.age # => 22 + # person.awesome # => true def from_json(json, include_root=include_root_in_json) hash = ActiveSupport::JSON.decode(json) hash = hash.values.first if include_root diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 648ae7ce3d..3ad3bf30ad 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' @@ -79,17 +79,17 @@ module ActiveModel require 'builder' unless defined? ::Builder options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) + options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) @builder = options[:builder] @builder.instruct! unless options[:skip_instruct] - root = (options[:root] || @serializable.class.model_name.element).to_s + root = (options[:root] || @serializable.model_name.element).to_s root = ActiveSupport::XmlMini.rename_key(root, options) args = [root] - args << {:xmlns => options[:namespace]} if options[:namespace] - args << {:type => options[:type]} if options[:type] && !options[:skip_types] + args << { xmlns: options[:namespace] } if options[:namespace] + args << { type: options[:type] } if options[:type] && !options[:skip_types] @builder.tag!(*args) do add_attributes_and_methods @@ -132,7 +132,7 @@ module ActiveModel records = records.to_ary tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) - type = options[:skip_types] ? { } : {:type => "array"} + type = options[:skip_types] ? { } : { type: "array" } association_name = association.to_s.singularize merged_options[:root] = association_name @@ -145,7 +145,7 @@ module ActiveModel record_type = {} else record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name - record_type = {:type => record_class} + record_type = { type: record_class } end record.to_xml merged_options.merge(record_type) @@ -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/translation.rb b/activemodel/lib/active_model/translation.rb index 0d098ba93d..8470915abb 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -41,7 +41,7 @@ module ActiveModel # # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) - options = { :count => 1 }.merge!(options) + options = { count: 1 }.merge!(options) parts = attribute.to_s.split(".") attribute = parts.pop namespace = parts.join("/") unless parts.empty? diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 2db4a25f61..6a2668b8f7 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -4,7 +4,7 @@ require 'active_support/core_ext/hash/except' module ActiveModel - # == Active \Model Validations + # == Active \Model \Validations # # Provides a full validation framework to your objects. # @@ -39,6 +39,7 @@ module ActiveModel extend ActiveSupport::Concern included do + extend ActiveModel::Naming extend ActiveModel::Callbacks extend ActiveModel::Translation @@ -46,7 +47,7 @@ module ActiveModel include HelperMethods attr_accessor :validation_context - define_callbacks :validate, :scope => :name + define_callbacks :validate, scope: :name class_attribute :_validators self._validators = Hash.new { |h,k| h[k] = [] } @@ -66,8 +67,11 @@ module ActiveModel # end # # Options: - # * <tt>:on</tt> - Specifies the context where this validation is active - # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>) + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine @@ -83,6 +87,8 @@ module ActiveModel validates_with BlockValidator, _merge_attributes(attr_names), &block end + VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze + # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldy and # you're looking for more descriptive declaration of your validations. @@ -124,10 +130,11 @@ module ActiveModel # end # # Options: - # * <tt>:on</tt> - Specifies the context where this validation is active - # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>) - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. - # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</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, @@ -139,11 +146,23 @@ module ActiveModel # value. def validate(*args, &block) options = args.extract_options! + + if args.all? { |arg| arg.is_a?(Symbol) } + options.each_key do |k| + unless VALID_OPTIONS_FOR_VALIDATE.include?(k) + raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{VALID_OPTIONS_FOR_VALIDATE.map(&:inspect).join(', ')}. Perhaps you meant to call `validates` instead of `validate`?") + end + end + end + if options.key?(:on) options = options.dup options[:if] = Array(options[:if]) - options[:if].unshift("validation_context == :#{options[:on]}") + options[:if].unshift ->(o) { + Array(options[:on]).include?(o.validation_context) + } end + args << options set_callback(:validate, *args, &block) end @@ -169,6 +188,49 @@ module ActiveModel _validators.values.flatten.uniq end + # Clears all of the validators and validations. + # + # Note that this will clear anything that is being used to validate + # the model for both the +validates_with+ and +validate+ methods. + # It clears the validators that are created with an invocation of + # +validates_with+ and the callbacks that are set by an invocation + # of +validate+. + # + # class Person + # include ActiveModel::Validations + # + # validates_with MyValidator + # validates_with OtherValidator, on: :create + # validates_with StrictValidator, strict: true + # validate :cannot_be_robot + # + # def cannot_be_robot + # errors.add(:base, 'A person cannot be a robot') if person_is_robot + # end + # end + # + # Person.validators + # # => [ + # # #<MyValidator:0x007fbff403e808 @options={}>, + # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>, + # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}> + # # ] + # + # If one runs <tt>Person.clear_validators!</tt> and then checks to see what + # validators this class has, you would obtain: + # + # Person.validators # => [] + # + # Also, the callback set by <tt>validate :cannot_be_robot</tt> will be erased + # so that: + # + # Person._validate_callbacks.empty? # => true + # + def clear_validators! + reset_callbacks(:validate) + _validators.clear + end + # List all validators that are being used to validate a specific attribute. # # class Person @@ -183,7 +245,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| @@ -239,6 +300,8 @@ module ActiveModel # Runs all the specified validations and returns +true+ if no errors were # added otherwise +false+. # + # Aliased as validate. + # # class Person # include ActiveModel::Validations # @@ -273,6 +336,8 @@ module ActiveModel self.validation_context = current_context end + alias_method :validate, :valid? + # Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were # added, +false+ otherwise. # @@ -327,13 +392,10 @@ module ActiveModel protected def run_validations! #:nodoc: - run_callbacks :validate + _run_validate_callbacks errors.empty? end end end -Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path| - filename = File.basename(path) - require "active_model/validations/#{filename}" -end +Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file } diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb index 1a1863370b..9b5416fb1d 100644 --- a/activemodel/lib/active_model/validations/absence.rb +++ b/activemodel/lib/active_model/validations/absence.rb @@ -21,7 +21,7 @@ module ActiveModel # * <tt>:message</tt> - A custom error message (default is: "must be blank"). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_absence_of(*attr_names) validates_with AbsenceValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 0935ad0d2a..ac5e79859b 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -3,7 +3,8 @@ module ActiveModel module Validations class AcceptanceValidator < EachValidator # :nodoc: def initialize(options) - super({ :allow_nil => true, :accept => "1" }.merge!(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) @@ -36,8 +38,6 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default - # is +true+). # * <tt>:accept</tt> - Specifies value that is considered accepted. # The default value is a string "1", which makes it easy to relate to # an HTML checkbox. This should be set to +true+ if you are validating @@ -45,8 +45,8 @@ module ActiveModel # before validation. # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index e28ad2841b..25ccabd66b 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -1,6 +1,6 @@ module ActiveModel module Validations - # == Active \Model Validation Callbacks + # == Active \Model \Validation \Callbacks # # Provides an interface for any class to have +before_validation+ and # +after_validation+ callbacks. @@ -22,7 +22,10 @@ module ActiveModel included do include ActiveSupport::Callbacks - define_callbacks :validation, :terminator => "result == false", :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name] + define_callbacks :validation, + terminator: ->(_,result) { result == false }, + skip_after_callbacks_if_terminated: true, + scope: [:kind, :name] end module ClassMethods @@ -55,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 ->(o) { + options[:on].include? o.validation_context + } end set_callback(:validation, :before, *args, &block) end @@ -93,7 +98,9 @@ module ActiveModel options[:if] = Array(options[:if]) if options[:on] options[:on] = Array(options[:on]) - options[:if].unshift("#{options[:on]}.include? self.validation_context") + options[:if].unshift ->(o) { + options[:on].include? o.validation_context + } end set_callback(:validation, :after, *(args << options), &block) end @@ -103,7 +110,7 @@ module ActiveModel # Overwrite run validations to include callbacks. def run_validations! #:nodoc: - run_callbacks(:validation) { super } + _run_validation_callbacks { super } end end 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 3a3abce364..1b11c28087 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -2,17 +2,27 @@ 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) - record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(:attribute => human_attribute_name)) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name)) end end - def setup(klass) - klass.send(:attr_accessor, *attributes.map do |attribute| + private + def setup!(klass) + klass.send(:attr_reader, *attributes.map do |attribute| :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation") end.compact) + + klass.send(:attr_writer, *attributes.map do |attribute| + :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=") + end.compact) end end @@ -44,10 +54,10 @@ module ActiveModel # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "doesn't match - # confirmation"). + # <tt>%{translated_attribute_name}</tt>"). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index b7f38e48f5..f342d27275 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -8,7 +8,7 @@ module ActiveModel def validate_each(record, attribute, value) if include?(record, value) - record.errors.add(attribute, :exclusion, options.except(:in, :within).merge!(:value => value)) + record.errors.add(attribute, :exclusion, options.except(:in, :within).merge!(value: value)) end end end @@ -34,13 +34,9 @@ module ActiveModel # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is # reserved"). - # * <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+). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 9398b7e66e..02478dd5b6 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 @@ -29,30 +29,32 @@ module ActiveModel end def record_error(record, attribute, name, value) - record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value)) + 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 module HelperMethods # Validates whether the value of the specified attribute is of the correct - # form, going by the regular expression provided.You can require that the + # form, going by the regular expression provided. You can require that the # attribute matches the regular expression: # # class Person < ActiveRecord::Base @@ -89,10 +91,6 @@ module ActiveModel # # 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> - Regular expression that if the attribute matches will # result in a successful validation. This can be provided as a proc or # lambda returning regular expression which will be called at runtime. @@ -105,7 +103,7 @@ module ActiveModel # beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>. # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 5e45a04c2c..c84025f083 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -8,7 +8,7 @@ module ActiveModel def validate_each(record, attribute, value) unless include?(record, value) - record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(:value => value)) + record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(value: value)) end end end @@ -28,18 +28,15 @@ 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>, - # otherwise with <tt>include?</tt>. + # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>, + # otherwise with <tt>include?</tt>. When using a proc or lambda the instance + # under validation is passed as an argument. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # * <tt>:message</tt> - Specifies a custom error message (default is: "is # not included in the list"). - # * <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+). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 675fb5f1e5..a96b30cadd 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,10 +1,10 @@ module ActiveModel - # == Active \Model Length \Validator + # == Active \Model Length Validator module Validations class LengthValidator < EachValidator # :nodoc: - MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze - CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze + MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze + CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long] diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 085532c35b..4ba4e3e8f7 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -2,17 +2,18 @@ module ActiveModel module Validations class NumericalityValidator < EachValidator # :nodoc: - CHECKS = { :greater_than => :>, :greater_than_or_equal_to => :>=, - :equal_to => :==, :less_than => :<, :less_than_or_equal_to => :<=, - :odd => :odd?, :even => :even?, :other_than => :!= }.freeze + CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=, + equal_to: :==, less_than: :<, less_than_or_equal_to: :<=, + odd: :odd?, even: :even?, other_than: :!= }.freeze RESERVED_OPTIONS = CHECKS.keys + [:only_integer] 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 @@ -22,6 +23,10 @@ module ActiveModel raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) raw_value ||= value + if record_attribute_changed_in_place?(record, attr_name) + raw_value = value + end + return if options[:allow_nil] && raw_value.nil? unless value = parse_raw_value_as_a_number(raw_value) @@ -29,7 +34,7 @@ module ActiveModel return end - if options[:only_integer] + if allow_only_integer?(record) unless value = parse_raw_value_as_an_integer(raw_value) record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value)) return @@ -43,11 +48,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,24 +65,37 @@ 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) - raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/ + raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\z/ end def filtered_options(value) - options.except(*RESERVED_OPTIONS).merge!(:value => value) + filtered = options.except(*RESERVED_OPTIONS) + filtered[:value] = value + filtered + end + + def allow_only_integer?(record) + case options[:only_integer] + when Symbol + record.send(options[:only_integer]) + when Proc + options[:only_integer].call(record) + else + options[:only_integer] + end + end + + private + + def record_attribute_changed_in_place?(record, attr_name) + record.respond_to?(:attribute_changed_in_place?) && + record.attribute_changed_in_place?(attr_name.to_s) end end @@ -110,7 +132,7 @@ module ActiveModel # * <tt>:even</tt> - Specifies the value must be an even number. # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+ . + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ . # See <tt>ActiveModel::Validation#validates</tt> for more information # # The following checks can also be supplied with a proc or a symbol which @@ -121,6 +143,7 @@ module ActiveModel # * <tt>:equal_to</tt> # * <tt>:less_than</tt> # * <tt>:less_than_or_equal_to</tt> + # * <tt>:only_integer</tt> # # For example: # diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index ab8c8359fc..5d593274eb 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -29,7 +29,7 @@ module ActiveModel # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 1eb0716891..bda436d8d0 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 @@ -71,9 +71,11 @@ module ActiveModel # # There is also a list of options that could be used along with validators: # - # * <tt>:on</tt> - Specifies when this validation is active. Runs in all - # validation contexts by default (+nil+), other options are <tt>:create</tt> - # and <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</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, @@ -83,7 +85,9 @@ module ActiveModel # 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. - # * <tt>:strict</tt> - if the <tt>:strict</tt> option is set to true + # * <tt>:allow_nil</tt> - Skip validation if the attribute is +nil+. + # * <tt>:allow_blank</tt> - Skip validation if the attribute is blank. + # * <tt>:strict</tt> - If the <tt>:strict</tt> option is set to true # will raise ActiveModel::StrictValidationFailed instead of adding the error. # <tt>:strict</tt> option can also be set to any other exception. # @@ -159,9 +163,9 @@ module ActiveModel when Hash options when Range, Array - { :in => options } + { in: options } else - { :with => options } + { with: options } end end end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 2ae335d0f4..a2531327bf 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -52,8 +52,11 @@ module ActiveModel # end # # Configuration options: - # * <tt>:on</tt> - Specifies when this validation is active - # (<tt>:create</tt> or <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</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>). @@ -83,9 +86,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| @@ -138,6 +142,8 @@ module ActiveModel # class version of this method for more information. def validates_with(*args, &block) options = args.extract_options! + options[:class] = self.class + args.each do |klass| validator = klass.new(options, &block) validator.validate(self) diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index f989b21140..0116de68ab 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -60,6 +60,9 @@ module ActiveModel # end # end # + # Note that the validator is initialized only once for the whole application + # life cycle, and not on each validation run. + # # The easiest way to add custom validators for validating individual attributes # is with the convenient <tt>ActiveModel::EachValidator</tt>. # @@ -76,21 +79,19 @@ module ActiveModel # include ActiveModel::Validations # attr_accessor :title # - # validates :title, presence: true + # validates :title, presence: true, title: 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 accessible 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 @@ -104,10 +105,10 @@ module ActiveModel # Accepts options that will be made available through the +options+ reader. def initialize(options = {}) - @options = options.freeze + @options = options.except(:class).freeze end - # Return the kind for this validator. + # Returns the kind for this validator. # # PresenceValidator.new.kind # => :presence # UniquenessValidator.new.kind # => :uniqueness diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index e195c12a4d..b1f9082ea7 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,10 +1,8 @@ -module ActiveModel - module VERSION #:nodoc: - MAJOR = 4 - MINOR = 0 - TINY = 0 - PRE = "beta" +require_relative 'gem_version' - STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') +module ActiveModel + # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt> + def self.version + gem_version end end |