diff options
Diffstat (limited to 'activemodel/lib')
36 files changed, 490 insertions, 845 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index f757ba9843..ef4f2514be 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2012 David Heinemeier Hansson +# Copyright (c) 2004-2013 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,11 +37,8 @@ module ActiveModel autoload :ForbiddenAttributesProtection autoload :Lint autoload :Model - autoload :DeprecatedMassAssignmentSecurity autoload :Name, 'active_model/naming' autoload :Naming - autoload :Observer, 'active_model/observing' - autoload :Observing autoload :SecurePassword autoload :Serialization autoload :TestCase diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index ef04f1fa49..f336c759d2 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,3 +1,5 @@ +require 'thread_safe' +require 'mutex_m' module ActiveModel # Raised when an attribute is not defined. @@ -8,22 +10,24 @@ module ActiveModel # # user = User.first # user.pets.select(:id).first.user_id - # # => ActiveModel::MissingAttributeError: missing attribute: user_id + # # => 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+. + # 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, see below. # # A minimal implementation could be: # @@ -37,6 +41,10 @@ module ActiveModel # # attr_accessor :name # + # def attributes + # {'name' => @name} + # end + # # private # # def attribute_contrived?(attr) @@ -52,10 +60,10 @@ module ActiveModel # 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. + # Note that whenever you include <tt>ActiveModel::AttributeMethods</tt> 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 @@ -178,7 +186,6 @@ module ActiveModel undefine_attribute_methods end - # Allows you to make aliases for attributes. # # class Person @@ -202,7 +209,7 @@ module ActiveModel # person.name # => "Bob" # person.nickname # => "Bob" # person.name_short? # => true - # person.nickname_short? # => true + # person.nickname_short? # => true def alias_attribute(new_name, old_name) self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s) attribute_method_matchers.each do |matcher| @@ -212,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. # @@ -316,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,17 +355,17 @@ module ActiveModel # significantly (in our case our test suite finishes 10% faster with # this cache). def attribute_method_matchers_cache #:nodoc: - @attribute_method_matchers_cache ||= {} + @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4) end def attribute_method_matcher(method_name) #:nodoc: - attribute_method_matchers_cache.fetch(method_name) do |name| + 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(name) } - attribute_method_matchers_cache[name] = match + matchers.detect { |method| match = method.match(method_name) } + match end end @@ -382,15 +400,6 @@ module ActiveModel AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) def initialize(options = {}) - if options[:prefix] == '' || options[:suffix] == '' - ActiveSupport::Deprecation.warn( - "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." - ) - end - @prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '') @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/ @method_missing_target = "#{@prefix}attribute#{@suffix}" @@ -413,17 +422,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 +441,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 - # customise the behaviour. + # +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 diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index e442455a53..377aa6ee27 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -1,7 +1,7 @@ -require 'active_support/callbacks' +require 'active_support/core_ext/array/extract_options' module ActiveModel - # == Active Model Callbacks + # == Active \Model \Callbacks # # Provides an interface for any class to have Active Record like callbacks. # @@ -100,10 +100,10 @@ module ActiveModel 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)) @@ -120,30 +120,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 48c53f0789..21e4eb3c86 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,7 +1,5 @@ -require 'active_support/inflector' - module ActiveModel - # == Active Model Conversions + # == Active \Model Conversion # # Handles default conversions: to_model, to_key, to_param, and to_partial_path. # 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 2ea69991fc..0000000000 --- a/activemodel/lib/active_model/deprecated_mass_assignment_security.rb +++ /dev/null @@ -1,19 +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 " \ - "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 " \ - "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 c0b268fa4d..c5f1b3f11a 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,9 +1,8 @@ -require 'active_model/attribute_methods' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' module ActiveModel - # == Active Model Dirty + # == Active \Model \Dirty # # Provides a way to track changes in your object in the same way as # Active Record does. @@ -15,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: # @@ -40,14 +35,18 @@ module ActiveModel # end # # def save - # @previously_changed = changes - # @changed_attributes.clear + # # do persistence work + # changes_applied + # end + # + # def reload! + # reset_changes # 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: @@ -66,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' @@ -92,7 +97,7 @@ module ActiveModel included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' - attribute_method_affix :prefix => 'reset_', :suffix => '!' + attribute_method_affix prefix: 'reset_', suffix: '!' end # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. @@ -120,7 +125,7 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] + ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] end # Returns a hash of attributes that were changed before the model was saved. @@ -130,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 @@ -140,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+. @@ -155,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) @@ -175,7 +192,10 @@ module ActiveModel # Handle <tt>reset_*!</tt> for +method_missing+. def reset_attribute!(attr) - __send__("#{attr}=", changed_attributes[attr]) if attribute_changed?(attr) + if attribute_changed?(attr) + __send__("#{attr}=", changed_attributes[attr]) + changed_attributes.delete(attr) + end end end end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index b3b9ba8e56..cf7551e4f4 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -4,7 +4,7 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' module ActiveModel - # == Active Model Errors + # == Active \Model \Errors # # Provides a modified +Hash+ that you can include in your object # for handling error messages and interacting with Action Pack helpers. @@ -12,7 +12,6 @@ module ActiveModel # A minimal implementation could be: # # class Person - # # # Required dependency for ActiveModel::Errors # extend ActiveModel::Naming # @@ -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,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.. @@ -75,14 +73,14 @@ module ActiveModel @messages = {} end - def initialize_dup(other) #:nodoc: + def initialize_dup(other) # :nodoc: @messages = other.messages.dup super end # Clear the error messages. # - # person.errors.full_messages # => ["name can not be nil"] + # person.errors.full_messages # => ["name can not be nil"] # person.errors.clear # person.errors.full_messages # => [] def clear @@ -92,9 +90,9 @@ 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=>["can not be nil"]} # person.errors.include?(:name) # => true - # person.errors.include?(:age) # => false + # person.errors.include?(:age) # => false def include?(attribute) (v = messages[attribute]) && v.any? end @@ -103,7 +101,7 @@ module ActiveModel # Get messages for +key+. # - # person.errors.messages # => { :name => ["can not be nil"] } + # person.errors.messages # => {:name=>["can not be nil"]} # person.errors.get(:name) # => ["can not be nil"] # person.errors.get(:age) # => nil def get(key) @@ -122,7 +120,7 @@ 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.delete(:name) # => ["can not be nil"] # person.errors.get(:name) # => nil def delete(key) messages.delete(key) @@ -177,7 +175,7 @@ module ActiveModel # Returns all message values. # - # person.errors.messages # => { :name => ["can not be nil", "must be specified"] } + # person.errors.messages # => {:name=>["can not be nil", "must be specified"]} # person.errors.values # => [["can not be nil", "must be specified"]] def values messages.values @@ -185,7 +183,7 @@ module ActiveModel # Returns all message keys. # - # person.errors.messages # => { :name => ["can not be nil", "must be specified"] } + # person.errors.messages # => {:name=>["can not be nil", "must be specified"]} # person.errors.keys # => [:name] def keys messages.keys @@ -213,7 +211,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 can not be nil"] # person.errors.empty? # => false def empty? all? { |k, v| v && v.empty? && !v.is_a?(String) } @@ -233,24 +231,24 @@ 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=>["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 - # Returns a Hash of attributes with their error messages. If +full_messages+ + # 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 = {} @@ -273,7 +271,7 @@ module ActiveModel # # => ["is invalid", "must be implemented"] # # person.errors.messages - # # => { :name => ["must be implemented", "is invalid"] } + # # => {:name=>["must be implemented", "is invalid"]} # # If +message+ is a symbol, it will be translated using the appropriate # scope (see +generate_message+). @@ -286,12 +284,12 @@ module ActiveModel # <tt>:strict</tt> option can also be set to any other exception. # # person.errors.add(:name, nil, strict: true) - # # => ActiveModel::StrictValidationFailed: name is invalid + # # => ActiveModel::StrictValidationFailed: name is invalid # person.errors.add(:name, nil, strict: NameIsInvalid) - # # => NameIsInvalid: name is invalid + # # => 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 @@ -306,9 +304,9 @@ module ActiveModel # # person.errors.add_on_empty(:name) # person.errors.messages - # # => { :name => ["can't be empty"] } + # # => {:name=>["can't be empty"]} def add_on_empty(attributes, options = {}) - [attributes].flatten.each do |attribute| + Array(attributes).each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) is_empty = value.respond_to?(:empty?) ? value.empty? : false add(attribute, :empty, options) if value.nil? || is_empty @@ -320,9 +318,9 @@ module ActiveModel # # person.errors.add_on_blank(:name) # person.errors.messages - # # => { :name => ["can't be blank"] } + # # => {:name=>["can't be blank"]} def add_on_blank(attributes, options = {}) - [attributes].flatten.each do |attribute| + Array(attributes).each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) add(attribute, :blank, options) if value.blank? end @@ -333,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 @@ -352,17 +350,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 +426,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.class.model_name.human, + attribute: @base.class.human_attribute_name(attribute), + value: value }.merge!(options) I18n.translate(key, options) @@ -425,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/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb index 4c05b19cba..7468f95548 100644 --- a/activemodel/lib/active_model/forbidden_attributes_protection.rb +++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb @@ -16,7 +16,7 @@ module ActiveModel module ForbiddenAttributesProtection # :nodoc: protected - def sanitize_for_mass_assignment(attributes, options = {}) + def sanitize_for_mass_assignment(attributes) if attributes.respond_to?(:permitted?) && !attributes.permitted? raise ActiveModel::ForbiddenAttributesError else diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 550fa474ea..46b446dc08 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -1,8 +1,8 @@ module ActiveModel module Lint - # == Active Model Lint Tests + # == Active \Model \Lint \Tests # - # You can test whether an object is compliant with the Active Model API by + # You can test whether an object is compliant with the Active \Model API by # including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will # include tests that tell you whether your object is fully compliant, # or if not, which aspects of the API are not implemented. @@ -71,7 +71,7 @@ module ActiveModel assert_boolean model.persisted?, "persisted?" end - # == Naming + # == \Naming # # Model.model_name must return a string with some convenience methods: # <tt>:human</tt>, <tt>:singular</tt> and <tt>:plural</tt>. Check @@ -85,7 +85,7 @@ module ActiveModel assert model_name.plural.respond_to?(:to_str) end - # == Errors Testing + # == \Errors Testing # # Returns an object that implements [](attribute) defined which returns an # Array of Strings that are the errors for the attribute in question. @@ -98,7 +98,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 d17848c861..540e8132d3 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -13,6 +13,7 @@ en: accepted: "must be accepted" 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)" diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index 33a530e6bd..f048dda5c6 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. @@ -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 4322592a7e..198efc5088 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,4 +1,3 @@ -require 'active_support/inflector' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' @@ -56,8 +55,8 @@ module ActiveModel # end # # BlogPost.model_name <=> 'BlogPost' # => 0 - # BlogPost.model_name <=> 'Blog' # => 1 - # BlogPost.model_name <=> 'BlogPosts' # => -1 + # BlogPost.model_name <=> 'Blog' # => 1 + # BlogPost.model_name <=> 'BlogPosts' # => -1 ## # :method: =~ @@ -130,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 @@ -184,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 @@ -195,7 +194,7 @@ module ActiveModel end end - # == Active Model Naming + # == Active \Model \Naming # # Creates a +model_name+ method on your object. # diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb deleted file mode 100644 index 77bc0f71e3..0000000000 --- a/activemodel/lib/active_model/observer_array.rb +++ /dev/null @@ -1,152 +0,0 @@ -require 'set' - -module ActiveModel - # Stores the enabled/disabled state of individual observers for - # a particular model class. - class ObserverArray < Array - attr_reader :model_class - def initialize(model_class, *args) #:nodoc: - @model_class = model_class - super(*args) - end - - # Returns +true+ if the given observer is disabled for the model class, - # +false+ otherwise. - def disabled_for?(observer) #:nodoc: - disabled_observers.include?(observer.class) - end - - # Disables one or more observers. This supports multiple forms: - # - # ORM.observers.disable :all - # # => disables all observers for all models subclassed from - # # an ORM base class that includes ActiveModel::Observing - # # e.g. ActiveRecord::Base - # - # ORM.observers.disable :user_observer - # # => disables the UserObserver - # - # User.observers.disable AuditTrail - # # => disables the AuditTrail observer for User notifications. - # # Other models will still notify the AuditTrail observer. - # - # ORM.observers.disable :observer_1, :observer_2 - # # => disables Observer1 and Observer2 for all models. - # - # User.observers.disable :all do - # # all user observers are disabled for - # # just the duration of the block - # end - def disable(*observers, &block) - set_enablement(false, observers, &block) - end - - # Enables one or more observers. This supports multiple forms: - # - # ORM.observers.enable :all - # # => enables all observers for all models subclassed from - # # an ORM base class that includes ActiveModel::Observing - # # e.g. ActiveRecord::Base - # - # ORM.observers.enable :user_observer - # # => enables the UserObserver - # - # User.observers.enable AuditTrail - # # => enables the AuditTrail observer for User notifications. - # # Other models will not be affected (i.e. they will not - # # trigger notifications to AuditTrail if previously disabled) - # - # ORM.observers.enable :observer_1, :observer_2 - # # => enables Observer1 and Observer2 for all models. - # - # User.observers.enable :all do - # # all user observers are enabled for - # # just the duration of the block - # end - # - # Note: all observers are enabled by default. This method is only - # useful when you have previously disabled one or more observers. - def enable(*observers, &block) - set_enablement(true, observers, &block) - end - - protected - - def disabled_observers #:nodoc: - @disabled_observers ||= Set.new - end - - def observer_class_for(observer) #:nodoc: - return observer if observer.is_a?(Class) - - if observer.respond_to?(:to_sym) # string/symbol - observer.to_s.camelize.constantize - else - raise ArgumentError, "#{observer} was not a class or a " + - "lowercase, underscored class name as expected." - end - end - - def start_transaction #:nodoc: - disabled_observer_stack.push(disabled_observers.dup) - each_subclass_array do |array| - array.start_transaction - end - end - - def disabled_observer_stack #:nodoc: - @disabled_observer_stack ||= [] - end - - def end_transaction #:nodoc: - @disabled_observers = disabled_observer_stack.pop - each_subclass_array do |array| - array.end_transaction - end - end - - def transaction #:nodoc: - start_transaction - - begin - yield - ensure - end_transaction - end - end - - def each_subclass_array #:nodoc: - model_class.descendants.each do |subclass| - yield subclass.observers - end - end - - def set_enablement(enabled, observers) #:nodoc: - if block_given? - transaction do - set_enablement(enabled, observers) - yield - end - else - observers = ActiveModel::Observer.descendants if observers == [:all] - observers.each do |obs| - klass = observer_class_for(obs) - - unless klass < ActiveModel::Observer - raise ArgumentError.new("#{obs} does not refer to a valid observer") - end - - if enabled - disabled_observers.delete(klass) - else - disabled_observers << klass - end - end - - each_subclass_array do |array| - array.set_enablement(enabled, observers) - end - end - end - end -end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb deleted file mode 100644 index 9db7639ea3..0000000000 --- a/activemodel/lib/active_model/observing.rb +++ /dev/null @@ -1,374 +0,0 @@ -require 'singleton' -require 'active_model/observer_array' -require 'active_support/core_ext/module/aliasing' -require 'active_support/core_ext/module/remove_method' -require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/object/try' -require 'active_support/descendants_tracker' - -module ActiveModel - # == Active Model Observers Activation - module Observing - extend ActiveSupport::Concern - - included do - extend ActiveSupport::DescendantsTracker - end - - module ClassMethods - # Activates the observers assigned. - # - # class ORM - # include ActiveModel::Observing - # end - # - # # Calls PersonObserver.instance - # ORM.observers = :person_observer - # - # # Calls Cacher.instance and GarbageCollector.instance - # ORM.observers = :cacher, :garbage_collector - # - # # Same as above, just using explicit class references - # ORM.observers = Cacher, GarbageCollector - # - # Note: Setting this does not instantiate the observers yet. - # <tt>instantiate_observers</tt> is called during startup, and before - # each development request. - def observers=(*values) - observers.replace(values.flatten) - end - - # Gets an array of observers observing this model. The array also provides - # +enable+ and +disable+ methods that allow you to selectively enable and - # disable observers (see ActiveModel::ObserverArray.enable and - # ActiveModel::ObserverArray.disable for more on this). - # - # class ORM - # include ActiveModel::Observing - # end - # - # ORM.observers = :cacher, :garbage_collector - # ORM.observers # => [:cacher, :garbage_collector] - # ORM.observers.class # => ActiveModel::ObserverArray - def observers - @observers ||= ObserverArray.new(self) - end - - # Returns the current observer instances. - # - # class Foo - # include ActiveModel::Observing - # - # attr_accessor :status - # end - # - # class FooObserver < ActiveModel::Observer - # def on_spec(record, *args) - # record.status = true - # end - # end - # - # Foo.observers = FooObserver - # Foo.instantiate_observers - # - # Foo.observer_instances # => [#<FooObserver:0x007fc212c40820>] - def observer_instances - @observer_instances ||= [] - end - - # Instantiate the global observers. - # - # class Foo - # include ActiveModel::Observing - # - # attr_accessor :status - # end - # - # class FooObserver < ActiveModel::Observer - # def on_spec(record, *args) - # record.status = true - # end - # end - # - # Foo.observers = FooObserver - # - # foo = Foo.new - # foo.status = false - # foo.notify_observers(:on_spec) - # foo.status # => false - # - # Foo.instantiate_observers # => [FooObserver] - # - # foo = Foo.new - # foo.status = false - # foo.notify_observers(:on_spec) - # foo.status # => true - def instantiate_observers - observers.each { |o| instantiate_observer(o) } - end - - # Add a new observer to the pool. The new observer needs to respond to - # <tt>update</tt>, otherwise it raises an +ArgumentError+ exception. - # - # class Foo - # include ActiveModel::Observing - # end - # - # class FooObserver < ActiveModel::Observer - # end - # - # Foo.add_observer(FooObserver.instance) - # - # Foo.observers_instance - # # => [#<FooObserver:0x007fccf55d9390>] - def add_observer(observer) - unless observer.respond_to? :update - raise ArgumentError, "observer needs to respond to 'update'" - end - observer_instances << observer - end - - # Fires notifications to model's observers. - # - # def save - # notify_observers(:before_save) - # ... - # notify_observers(:after_save) - # end - # - # Custom notifications can be sent in a similar fashion: - # - # notify_observers(:custom_notification, :foo) - # - # This will call <tt>custom_notification</tt>, passing as arguments - # the current object and <tt>:foo</tt>. - def notify_observers(*args) - observer_instances.each { |observer| observer.update(*args) } - end - - # Returns the total number of instantiated observers. - # - # class Foo - # include ActiveModel::Observing - # - # attr_accessor :status - # end - # - # class FooObserver < ActiveModel::Observer - # def on_spec(record, *args) - # record.status = true - # end - # end - # - # Foo.observers = FooObserver - # Foo.observers_count # => 0 - # Foo.instantiate_observers - # Foo.observers_count # => 1 - def observers_count - observer_instances.size - end - - # <tt>count_observers</tt> is deprecated. Use #observers_count. - def count_observers - msg = "count_observers is deprecated in favor of observers_count" - ActiveSupport::Deprecation.warn(msg) - observers_count - end - - protected - def instantiate_observer(observer) #:nodoc: - # string/symbol - if observer.respond_to?(:to_sym) - observer = observer.to_s.camelize.constantize - end - if observer.respond_to?(:instance) - observer.instance - else - raise ArgumentError, - "#{observer} must be a lowercase, underscored class name (or " + - "the class itself) responding to the method :instance. " + - "Example: Person.observers = :big_brother # calls " + - "BigBrother.instance" - end - end - - # Notify observers when the observed class is subclassed. - def inherited(subclass) #:nodoc: - super - notify_observers :observed_class_inherited, subclass - end - end - - # Notify a change to the list of observers. - # - # class Foo - # include ActiveModel::Observing - # - # attr_accessor :status - # end - # - # class FooObserver < ActiveModel::Observer - # def on_spec(record, *args) - # record.status = true - # end - # end - # - # Foo.observers = FooObserver - # Foo.instantiate_observers # => [FooObserver] - # - # foo = Foo.new - # foo.status = false - # foo.notify_observers(:on_spec) - # foo.status # => true - # - # See ActiveModel::Observing::ClassMethods.notify_observers for more - # information. - def notify_observers(method, *extra_args) - self.class.notify_observers(method, self, *extra_args) - end - end - - # == Active Model Observers - # - # Observer classes respond to life cycle callbacks to implement trigger-like - # behavior outside the original class. This is a great way to reduce the - # clutter that normally comes when the model class is burdened with - # functionality that doesn't pertain to the core responsibility of the - # class. - # - # class CommentObserver < ActiveModel::Observer - # def after_save(comment) - # Notifications.comment('admin@do.com', 'New comment was posted', comment).deliver - # end - # end - # - # This Observer sends an email when a <tt>Comment#save</tt> is finished. - # - # class ContactObserver < ActiveModel::Observer - # def after_create(contact) - # contact.logger.info('New contact added!') - # end - # - # def after_destroy(contact) - # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!") - # end - # end - # - # This Observer uses logger to log when specific callbacks are triggered. - # - # == Observing a class that can't be inferred - # - # Observers will by default be mapped to the class with which they share a - # name. So <tt>CommentObserver</tt> will be tied to observing <tt>Comment</tt>, - # <tt>ProductManagerObserver</tt> to <tt>ProductManager</tt>, and so on. If - # you want to name your observer differently than the class you're interested - # in observing, you can use the <tt>Observer.observe</tt> class method which - # takes either the concrete class (<tt>Product</tt>) or a symbol for that - # class (<tt>:product</tt>): - # - # class AuditObserver < ActiveModel::Observer - # observe :account - # - # def after_update(account) - # AuditTrail.new(account, 'UPDATED') - # end - # end - # - # If the audit observer needs to watch more than one kind of object, this can - # be specified with multiple arguments: - # - # class AuditObserver < ActiveModel::Observer - # observe :account, :balance - # - # def after_update(record) - # AuditTrail.new(record, 'UPDATED') - # end - # end - # - # The <tt>AuditObserver</tt> will now act on both updates to <tt>Account</tt> - # and <tt>Balance</tt> by treating them both as records. - # - # If you're using an Observer in a Rails application with Active Record, be - # sure to read about the necessary configuration in the documentation for - # ActiveRecord::Observer. - class Observer - include Singleton - extend ActiveSupport::DescendantsTracker - - class << self - # Attaches the observer to the supplied model classes. - # - # class AuditObserver < ActiveModel::Observer - # observe :account, :balance - # end - # - # AuditObserver.observed_classes # => [Account, Balance] - def observe(*models) - models.flatten! - models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } - singleton_class.redefine_method(:observed_classes) { models } - end - - # Returns an array of Classes to observe. - # - # AccountObserver.observed_classes # => [Account] - # - # You can override this instead of using the +observe+ helper. - # - # class AuditObserver < ActiveModel::Observer - # def self.observed_classes - # [Account, Balance] - # end - # end - def observed_classes - Array(observed_class) - end - - # Returns the class observed by default. It's inferred from the observer's - # class name. - # - # PersonObserver.observed_class # => Person - # AccountObserver.observed_class # => Account - def observed_class - name[/(.*)Observer/, 1].try :constantize - end - end - - # Start observing the declared classes and their subclasses. - # Called automatically by the instance method. - def initialize #:nodoc: - observed_classes.each { |klass| add_observer!(klass) } - end - - def observed_classes #:nodoc: - self.class.observed_classes - end - - # Send observed_method(object) if the method exists and - # the observer is enabled for the given object's class. - def update(observed_method, object, *extra_args, &block) #:nodoc: - return if !respond_to?(observed_method) || disabled_for?(object) - send(observed_method, object, *extra_args, &block) - end - - # Special method sent by the observed class when it is inherited. - # Passes the new subclass. - def observed_class_inherited(subclass) #:nodoc: - self.class.observe(observed_classes + [subclass]) - add_observer!(subclass) - end - - protected - def add_observer!(klass) #:nodoc: - klass.add_observer(self) - end - - # Returns true if notifications are disabled for this object. - def disabled_for?(object) #:nodoc: - klass = object.class - return false unless klass.respond_to?(:observers) - klass.observers.disabled_for?(self) - end - end -end diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb index f239758b35..1671eb7bd4 100644 --- a/activemodel/lib/active_model/railtie.rb +++ b/activemodel/lib/active_model/railtie.rb @@ -2,7 +2,11 @@ require "active_model" require "rails" module ActiveModel - class Railtie < Rails::Railtie + class Railtie < Rails::Railtie # :nodoc: config.eager_load_namespaces << ActiveModel + + initializer "active_model.secure_password" do + ActiveModel::SecurePassword.min_cost = Rails.env.test? + end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index d011402081..7e694b5c50 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -2,6 +2,11 @@ module ActiveModel module SecurePassword extend ActiveSupport::Concern + 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. @@ -11,9 +16,13 @@ module ActiveModel # you wish to turn off validations, pass <tt>validations: false</tt> as an # argument. You can add more validations by hand if need be. # - # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password: + # If you don't need the confirmation validation, just don't set any + # value to the password_confirmation attribute and the validation + # will not be triggered. # - # gem 'bcrypt-ruby', '~> 3.0.0' + # You need to add bcrypt-ruby (~> 3.1.2) to Gemfile to use #has_secure_password: + # + # gem 'bcrypt-ruby', '~> 3.1.2' # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # @@ -23,33 +32,38 @@ 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. # 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-ruby 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 - + 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 - include InstanceMethodsOnActivation - if respond_to?(:attributes_protected_by_default) def self.attributes_protected_by_default #:nodoc: super + ['password_digest'] @@ -68,7 +82,7 @@ module ActiveModel # user = User.new(name: 'david', password: 'mUc3m00RsqyRe') # user.save # user.authenticate('notright') # => false - # user.authenticate('mUc3m00RsqyRe') # => user + # user.authenticate('mUc3m00RsqyRe') # => user def authenticate(unencrypted_password) BCrypt::Password.new(password_digest) == unencrypted_password && self end @@ -84,13 +98,24 @@ module ActiveModel # user.password = nil # user.password_digest # => nil # user.password = 'mUc3m00RsqyRe' - # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4." + # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4." def password=(unencrypted_password) unless unencrypted_password.blank? @password = unencrypted_password - self.password_digest = BCrypt::Password.create(unencrypted_password) + 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 + + private + + def should_confirm_password? + password_confirmation && password.present? + end end end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 8a63014ffb..fdb06aebb9 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' module ActiveModel - # == Active Model Serialization + # == Active \Model \Serialization # # Provides a basic serialization to a serializable_hash for your object. # @@ -90,7 +90,7 @@ module ActiveModel # person.name = 'bob' # person.age = 22 # person.serializable_hash # => {"name"=>"bob", "age"=>22} - # person.serializable_hash(only: :name) # => {"name"=>"bob"} + # person.serializable_hash(only: :name) # => {"name"=>"bob"} # person.serializable_hash(except: :name) # => {"age"=>22} # person.serializable_hash(methods: :capitalized_name) # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"} diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index a4252b995d..05e2e089e5 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -1,8 +1,8 @@ require 'active_support/json' module ActiveModel - # == Active Model JSON Serializer module Serializers + # == Active Model JSON Serializer module JSON extend ActiveSupport::Concern include ActiveModel::Serialization @@ -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 cf742d0569..2864c2ba11 100755..100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -2,21 +2,30 @@ require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/time/acts_like' module ActiveModel - # == Active Model XML Serializer module Serializers + # == Active Model XML Serializer module Xml extend ActiveSupport::Concern include ActiveModel::Serialization + included do + extend ActiveModel::Naming + end + class Serializer #:nodoc: class Attribute #:nodoc: attr_reader :name, :value, :type def initialize(name, serializable, value) @name, @serializable = name, serializable - value = value.in_time_zone if value.respond_to?(:in_time_zone) + + if value.acts_like?(:time) && value.respond_to?(:in_time_zone) + value = value.in_time_zone + end + @value = value @type = compute_type end @@ -70,7 +79,7 @@ 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] @@ -79,8 +88,8 @@ module ActiveModel 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 @@ -123,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 @@ -136,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) @@ -145,7 +154,12 @@ module ActiveModel end else merged_options[:root] = association.to_s - records.to_xml(merged_options) + + unless records.class.to_s.underscore == association.to_s + merged_options[:type] = records.class.name + end + + records.to_xml merged_options end end @@ -191,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 7a86701f73..8470915abb 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -1,6 +1,6 @@ module ActiveModel - # == Active Model Translation + # == Active \Model \Translation # # Provides integration between your object and the Rails internationalization # (i18n) framework. @@ -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 243d911f71..31c2245265 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,12 +1,10 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/except' -require 'active_model/errors' -require 'active_model/validations/callbacks' module ActiveModel - # == Active Model Validations + # == Active \Model Validations # # Provides a full validation framework to your objects. # @@ -32,7 +30,7 @@ module ActiveModel # person.first_name = 'zoolander' # person.valid? # => false # person.invalid? # => true - # person.errors.messages # => {:first_name=>["starts with z."]} + # person.errors.messages # => {first_name:["starts with z."]} # # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+ # method to your instances initialized with a new <tt>ActiveModel::Errors</tt> @@ -48,7 +46,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] = [] } @@ -144,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) @@ -164,13 +164,56 @@ module ActiveModel # Person.validators # # => [ # # #<MyValidator:0x007fbff403e808 @options={}>, - # # #<OtherValidator:0x007fbff403d930 @options={:on=>:create}>, - # # #<StrictValidator:0x007fbff3204a30 @options={:strict=>true}> + # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>, + # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}> # # ] def validators _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 Person.clear_validators! and then checks to see what + # validators this class has, you would obtain: + # + # Person.validators # => [] + # + # Also, the callback set by +validate :cannot_be_robot+ 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 @@ -185,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| @@ -233,7 +275,7 @@ module ActiveModel # # person = Person.new # person.valid? # => false - # person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={:name=>["can't be blank"]}> + # person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={name:["can't be blank"]}> def errors @errors ||= Errors.new(self) end @@ -250,7 +292,7 @@ module ActiveModel # # person = Person.new # person.name = '' - # person.valid? # => false + # person.valid? # => false # person.name = 'david' # person.valid? # => true # @@ -265,7 +307,7 @@ module ActiveModel # end # # person = Person.new - # person.valid? # => true + # person.valid? # => true # person.valid?(:new) # => false def valid?(context = nil) current_context, self.validation_context = validation_context, context @@ -287,7 +329,7 @@ module ActiveModel # # person = Person.new # person.name = '' - # person.invalid? # => true + # person.invalid? # => true # person.name = 'david' # person.invalid? # => false # @@ -302,7 +344,7 @@ module ActiveModel # end # # person = Person.new - # person.invalid? # => false + # person.invalid? # => false # person.invalid?(:new) # => true def invalid?(context = nil) !valid?(context) @@ -335,7 +377,4 @@ module ActiveModel 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 new file mode 100644 index 0000000000..1a1863370b --- /dev/null +++ b/activemodel/lib/active_model/validations/absence.rb @@ -0,0 +1,31 @@ +module ActiveModel + module Validations + # == Active Model Absence Validator + class AbsenceValidator < EachValidator #:nodoc: + def validate_each(record, attr_name, value) + record.errors.add(attr_name, :present, options) if value.present? + end + end + + module HelperMethods + # Validates that the specified attributes are blank (as defined by + # Object#blank?). Happens by default on save. + # + # class Person < ActiveRecord::Base + # validates_absence_of :first_name + # end + # + # The first_name attribute must be in the object and it must be blank. + # + # Configuration options: + # * <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+. + # See <tt>ActiveModel::Validation#validates</tt> for more information + def validates_absence_of(*attr_names) + validates_with AbsenceValidator, _merge_attributes(attr_names) + end + end + end +end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 8d5ebf527f..139de16326 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -1,10 +1,10 @@ module ActiveModel - # == Active Model Acceptance Validator module Validations - class AcceptanceValidator < EachValidator #:nodoc: + 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) @@ -13,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) @@ -27,7 +28,7 @@ module ActiveModel # # class Person < ActiveRecord::Base # validates_acceptance_of :terms_of_service - # validates_acceptance_of :eula, message: "must be abided" + # validates_acceptance_of :eula, message: 'must be abided' # end # # If the database column does not exist, the +terms_of_service+ attribute diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index c153ef4309..fde53b9f89 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -1,8 +1,6 @@ -require 'active_support/callbacks' - 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. @@ -24,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 @@ -57,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 @@ -85,8 +88,8 @@ module ActiveModel # person = Person.new # person.name = '' # person.valid? # => false - # person.status # => false - # person.name = 'bob' + # person.status # => false + # person.name = 'bob' # person.valid? # => true # person.status # => true def after_validation(*args, &block) diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb index 3d7067fbcb..fd6cc1edb4 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/range' module ActiveModel module Validations module Clusivity #:nodoc: - ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " << + ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " \ "and must be supplied as the :in (or :within) option of the configuration hash" def check_validity! @@ -15,26 +15,33 @@ 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? + return :include? unless enumerable.is_a?(Range) + case enumerable.first + when Numeric, Time, DateTime + :cover? + 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 baa034eca6..b0542661af 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -1,19 +1,28 @@ module ActiveModel - # == Active Model Confirmation Validator module Validations - class ConfirmationValidator < EachValidator #:nodoc: + 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 diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 3ec552c372..48bf5cd802 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -2,14 +2,13 @@ require "active_model/validations/clusivity" module ActiveModel - # == Active Model Exclusion Validator module Validations - class ExclusionValidator < EachValidator #:nodoc: + class ExclusionValidator < EachValidator # :nodoc: include Clusivity 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 diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 80150229a0..be7cae588f 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -1,8 +1,7 @@ module ActiveModel - # == Active Model Format Validator module Validations - class FormatValidator < EachValidator #:nodoc: + class FormatValidator < EachValidator # :nodoc: def validate_each(record, attribute, value) if options[:with] regexp = option_call(record, :with) @@ -30,7 +29,7 @@ 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) diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index babc8982da..24337614c5 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -2,14 +2,13 @@ require "active_model/validations/clusivity" module ActiveModel - # == Active Model Inclusion Validator module Validations - class InclusionValidator < EachValidator #:nodoc: + class InclusionValidator < EachValidator # :nodoc: include Clusivity 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 @@ -29,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/length.rb b/activemodel/lib/active_model/validations/length.rb index e4a1f9e80a..ddfd8a342e 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 + class LengthValidator < EachValidator # :nodoc: + 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] @@ -14,6 +14,10 @@ module ActiveModel options[:minimum], options[:maximum] = range.min, range.max end + if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil? + options[:minimum] = 1 + end + super end @@ -37,10 +41,13 @@ module ActiveModel value = tokenize(value) value_length = value.respond_to?(:length) ? value.length : value.to_s.length errors_options = options.except(*RESERVED_OPTIONS) - + CHECKS.each do |key, validity_check| next unless check_value = options[key] - next if value_length.send(validity_check, check_value) + + if !value.nil? || skip_nil_check?(key) + next if value_length.send(validity_check, check_value) + end errors_options[:count] = check_value @@ -58,6 +65,10 @@ module ActiveModel options[:tokenizer].call(value) end || value end + + def skip_nil_check?(key) + key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil? + end end module HelperMethods @@ -79,7 +90,8 @@ module ActiveModel # # Configuration options: # * <tt>:minimum</tt> - The minimum size of the attribute. - # * <tt>:maximum</tt> - The maximum size of the attribute. + # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by + # default if not used with :minimum. # * <tt>:is</tt> - The exact size of the attribute. # * <tt>:within</tt> - A range specifying the minimum and maximum size of # the attribute. diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index edebca94a8..c6abe45f4a 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -1,11 +1,10 @@ module ActiveModel - # == Active Model Numericality Validator 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 + 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 RESERVED_OPTIONS = CHECKS.keys + [:only_integer] @@ -18,9 +17,9 @@ module ActiveModel end def validate_each(record, attr_name, value) - before_type_cast = "#{attr_name}_before_type_cast" + before_type_cast = :"#{attr_name}_before_type_cast" - raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast.to_sym) + raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) raw_value ||= value return if options[:allow_nil] && raw_value.nil? @@ -48,7 +47,7 @@ module ActiveModel option_value = record.send(option_value) if option_value.is_a?(Symbol) 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 @@ -74,7 +73,7 @@ module ActiveModel end def filtered_options(value) - options.except(*RESERVED_OPTIONS).merge!(:value => value) + options.except(*RESERVED_OPTIONS).merge!(value: value) end end @@ -126,7 +125,7 @@ module ActiveModel # For example: # # class Person < ActiveRecord::Base - # validates_numericality_of :width, less_than: Proc.new { |person| person.height } + # validates_numericality_of :width, less_than: ->(person) { person.height } # validates_numericality_of :width, greater_than: :minimum_weight # end def validates_numericality_of(*attr_names) diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index f159e40858..ab8c8359fc 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -1,11 +1,10 @@ module ActiveModel - # == Active Model Presence Validator module Validations - class PresenceValidator < EachValidator #:nodoc: - def validate(record) - record.errors.add_on_blank(attributes, options) + class PresenceValidator < EachValidator # :nodoc: + def validate_each(record, attr_name, value) + record.errors.add(attr_name, :blank, options) if value.blank? end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 03046a543a..9a1ff2ad39 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -1,7 +1,6 @@ require 'active_support/core_ext/hash/slice' module ActiveModel - # == Active Model validates method module Validations module ClassMethods # This method is a shortcut to all default validators and any custom @@ -105,7 +104,7 @@ module ActiveModel raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? raise ArgumentError, "You need to supply at least one validation" if validations.empty? - defaults.merge!(:attributes => attributes) + defaults[:attributes] = attributes validations.each do |key, options| next unless options @@ -129,15 +128,15 @@ module ActiveModel # the validation itself. # # class Person - # include ActiveModel::Validations + # include ActiveModel::Validations # # attr_accessor :name # validates! :name, presence: true # end # # person = Person.new - # person.name = '' - # person.valid? + # person.name = '' + # person.valid? # # => ActiveModel::StrictValidationFailed: Name can't be blank def validates!(*attributes) options = attributes.extract_options! @@ -149,20 +148,20 @@ module ActiveModel # When creating custom validators, it might be useful to be able to specify # additional default keys. This can be done by overwriting this method. - def _validates_default_keys #:nodoc: + def _validates_default_keys # :nodoc: [:if, :unless, :on, :allow_blank, :allow_nil , :strict] end - def _parse_validates_options(options) #:nodoc: + def _parse_validates_options(options) # :nodoc: case options when TrueClass {} 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 869591cd9e..16bd6670d1 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -10,7 +10,7 @@ module ActiveModel end end - class WithValidator < EachValidator #:nodoc: + class WithValidator < EachValidator # :nodoc: def validate_each(record, attr, val) method_name = options[:with] @@ -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 85aec00f25..690856aee1 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -2,7 +2,7 @@ require "active_support/core_ext/module/anonymous" module ActiveModel - # == Active Model Validator + # == Active \Model \Validator # # A simple base class that can be used along with # ActiveModel::Validations::ClassMethods.validates_with @@ -60,6 +60,9 @@ module ActiveModel # end # end # + # Note that the validator is initialized only once for the whole application + # lifecycle, 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 # 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 @@ -103,14 +104,15 @@ module ActiveModel end # Accepts options that will be made available through the +options+ reader. - def initialize(options) - @options = options.freeze + def initialize(options = {}) + @options = options.except(:class).freeze + deprecated_setup(options) end # Return the kind for this validator. # - # PresenceValidator.new.kind # => :presence - # UniquenessValidator.new.kind # => :uniqueness + # PresenceValidator.new.kind # => :presence + # UniquenessValidator.new.kind # => :uniqueness def kind self.class.kind end @@ -120,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 @@ -135,7 +152,7 @@ module ActiveModel # and instead be made available through the +attributes+ reader. def initialize(options) @attributes = Array(options.delete(:attributes)) - raise ":attributes cannot be blank" if @attributes.empty? + raise ArgumentError, ":attributes cannot be blank" if @attributes.empty? super check_validity! end diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index e195c12a4d..86340bba37 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,10 +1,11 @@ module ActiveModel - module VERSION #:nodoc: - MAJOR = 4 - MINOR = 0 - TINY = 0 - PRE = "beta" + # Returns the version of the currently loaded ActiveModel as a Gem::Version + def self.version + Gem::Version.new "4.1.0.beta" + end - STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') + module VERSION #:nodoc: + MAJOR, MINOR, TINY, PRE = ActiveModel.version.segments + STRING = ActiveModel.version.to_s end end |