diff options
Diffstat (limited to 'activemodel/lib')
48 files changed, 1187 insertions, 397 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 8aa1b6f664..7de259a60d 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -49,6 +49,7 @@ module ActiveModel eager_autoload do autoload :Errors + autoload :RangeError, 'active_model/errors' autoload :StrictValidationFailed, 'active_model/errors' autoload :UnknownAttributeError, 'active_model/errors' end @@ -58,7 +59,6 @@ module ActiveModel eager_autoload do autoload :JSON - autoload :Xml end end diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index 087d11f708..62014cd1cd 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -27,7 +27,7 @@ module ActiveModel if !new_attributes.respond_to?(:stringify_keys) raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." end - return if new_attributes.blank? + return if new_attributes.nil? || new_attributes.empty? attributes = new_attributes.stringify_keys _assign_attributes(sanitize_for_mass_assignment(attributes)) diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 96be551264..cc6285f932 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent/map' require 'mutex_m' module ActiveModel @@ -23,7 +23,7 @@ module ActiveModel # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to: # # * <tt>include ActiveModel::AttributeMethods</tt> in your class. - # * Call each of its method you want to add, such as +attribute_method_suffix+ + # * Call each of its methods 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. @@ -225,9 +225,9 @@ module ActiveModel end # Declares the attributes that should be prefixed and suffixed by - # ActiveModel::AttributeMethods. + # <tt>ActiveModel::AttributeMethods</tt>. # - # To use, pass attribute names (as strings or symbols), be sure to declare + # To use, pass attribute names (as strings or symbols). Be sure to declare # +define_attribute_methods+ after you define any prefix, suffix or affix # methods, or they will not hook in. # @@ -239,7 +239,7 @@ module ActiveModel # # # Call to define_attribute_methods must appear after the # # attribute_method_prefix, attribute_method_suffix or - # # attribute_method_affix declares. + # # attribute_method_affix declarations. # define_attribute_methods :name, :age, :address # # private @@ -253,9 +253,9 @@ module ActiveModel end # Declares an attribute that should be prefixed and suffixed by - # ActiveModel::AttributeMethods. + # <tt>ActiveModel::AttributeMethods</tt>. # - # To use, pass an attribute name (as string or symbol), be sure to declare + # To use, pass an attribute name (as string or symbol). Be sure to declare # +define_attribute_method+ after you define any prefix, suffix or affix # method, or they will not hook in. # @@ -267,7 +267,7 @@ module ActiveModel # # # Call to define_attribute_method must appear after the # # attribute_method_prefix, attribute_method_suffix or - # # attribute_method_affix declares. + # # attribute_method_affix declarations. # define_attribute_method :name # # private @@ -342,7 +342,7 @@ module ActiveModel private # The methods +method_missing+ and +respond_to?+ of this module are # invoked often in a typical rails, both of which invoke the method - # +match_attribute_method?+. The latter method iterates through an + # +matched_attribute_method+. The latter method iterates through an # array doing regular expression matches, which results in a lot of # 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 @@ -350,7 +350,7 @@ 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 ||= ThreadSafe::Cache.new(initial_capacity: 4) + @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4) end def attribute_method_matchers_matching(method_name) #:nodoc: @@ -363,7 +363,7 @@ module ActiveModel end # Define a method `name` in `mod` that dispatches to `send` - # using the given `extra` args. This fallbacks `define_method` + # using the given `extra` args. This falls back on `define_method` # and `send` if the given names cannot be compiled. def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: defn = if name =~ NAME_COMPILABLE_REGEXP @@ -372,7 +372,7 @@ module ActiveModel "define_method(:'#{name}') do |*args|" end - extra = (extra.map!(&:inspect) << "*args").join(", ") + extra = (extra.map!(&:inspect) << "*args").join(", ".freeze) target = if send =~ CALL_COMPILABLE_REGEXP "#{"self." unless include_private}#{send}(#{extra})" @@ -419,7 +419,7 @@ module ActiveModel # 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 + # the attributes hash -- except for multiple assignments with # <tt>ActiveRecord::Base#attributes=</tt>. # # It's also possible to instantiate related objects, so a <tt>Client</tt> @@ -429,7 +429,7 @@ module ActiveModel if respond_to_without_attributes?(method, true) super else - match = match_attribute_method?(method.to_s) + match = matched_attribute_method(method.to_s) match ? attribute_missing(match, *args, &block) : super end end @@ -454,7 +454,7 @@ module ActiveModel # but found among all methods. Which means that the given method is private. false else - !match_attribute_method?(method.to_s).nil? + !matched_attribute_method(method.to_s).nil? end end @@ -466,7 +466,7 @@ module ActiveModel private # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. - def match_attribute_method?(method_name) + def matched_attribute_method(method_name) matches = self.class.send(:attribute_method_matchers_matching, method_name) matches.detect { |match| attribute_method?(match.attr_name) } end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 2cf39b68fb..0d6a3dc52d 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -103,6 +103,7 @@ module ActiveModel def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { + terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name], only: [:before, :around, :after] diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 9de6ea65be..a932ada45c 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -40,8 +40,8 @@ module ActiveModel self end - # 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. + # Returns an Array of all key attributes if any of the attributes is set, whether or not + # the object is persisted. Returns +nil+ if there are no key attributes. # # class Person # include ActiveModel::Conversion diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index c03e5fac79..90047c3c12 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -26,6 +26,10 @@ module ActiveModel # # define_attribute_methods :name # + # def initialize(name) + # @name = name + # end + # # def name # @name # end @@ -54,7 +58,7 @@ module ActiveModel # # A newly instantiated +Person+ object is unchanged: # - # person = Person.new + # person = Person.new("Uncle Bob") # person.changed? # => false # # Change the name: @@ -76,9 +80,11 @@ module ActiveModel # # Reset the changes: # - # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.name_previously_changed? # => true + # person.name_previous_change # => ["Uncle Bob", "Bill"] # person.reload! - # person.previous_changes # => {} + # person.previous_changes # => {} # # Rollback the changes: # @@ -100,10 +106,10 @@ module ActiveModel # person.changes # => {"name" => ["Bill", "Bob"]} # # If an attribute is modified in-place then make use of - # +[attribute_name]_will_change!+ to mark that the attribute is changing. + # <tt>[attribute_name]_will_change!</tt> 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. + # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models. # # person.name_will_change! # person.name_change # => ["Bill", "Bill"] @@ -113,8 +119,12 @@ module ActiveModel extend ActiveSupport::Concern include ActiveModel::AttributeMethods + OPTION_NOT_GIVEN = Object.new # :nodoc: + private_constant :OPTION_NOT_GIVEN + included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' + attribute_method_suffix '_previously_changed?', '_previous_change' attribute_method_affix prefix: 'restore_', suffix: '!' end @@ -167,11 +177,10 @@ module ActiveModel end # Handles <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 + def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: + !!changes_include?(attr) && + (to == OPTION_NOT_GIVEN || to == __send__(attr)) && + (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) end # Handles <tt>*_was</tt> for +method_missing+. @@ -179,6 +188,11 @@ module ActiveModel attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end + # Handles <tt>*_previously_changed?</tt> for +method_missing+. + def attribute_previously_changed?(attr) #:nodoc: + previous_changes_include?(attr) + end + # Restore all previous data of the provided attributes. def restore_attributes(attributes = changed) attributes.each { |attr| restore_attribute! attr } @@ -192,6 +206,12 @@ module ActiveModel end alias attribute_changed_by_setter? changes_include? + # Returns +true+ if attr_name were changed before the model was saved, + # +false+ otherwise. + def previous_changes_include?(attr_name) + previous_changes.include?(attr_name) + end + # Removes current changes and makes them accessible through +previous_changes+. def changes_applied # :doc: @previously_changed = changes @@ -209,6 +229,11 @@ module ActiveModel [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) end + # Handles <tt>*_previous_change</tt> for +method_missing+. + def attribute_previous_change(attr) + previous_changes[attr] if attribute_previously_changed?(attr) + end + # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) return if attribute_changed?(attr) diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index f324788979..696e6d31da 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- - require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/string/filters' module ActiveModel # == Active \Model \Errors @@ -33,20 +32,20 @@ module ActiveModel # send(attr) # end # - # def Person.human_attribute_name(attr, options = {}) + # def self.human_attribute_name(attr, options = {}) # attr # end # - # def Person.lookup_ancestors + # def self.lookup_ancestors # [self] # end # end # - # The last three methods are required in your object for Errors to be + # The last three methods are required in your object for +Errors+ to be # able to generate error messages correctly and also handle multiple - # languages. Of course, if you extend your object with ActiveModel::Translation + # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt> # you will not need to implement the last two. Likewise, using - # ActiveModel::Validations will handle the validation related methods + # <tt>ActiveModel::Validations</tt> will handle the validation related methods # for you. # # The above allows you to do: @@ -82,6 +81,18 @@ module ActiveModel super end + # Copies the errors from <tt>other</tt>. + # + # other - The ActiveModel::Errors instance. + # + # Examples + # + # person.errors.copy!(other) + def copy!(other) # :nodoc: + @messages = other.messages.dup + @details = other.details.dup + end + # Clear the error messages. # # person.errors.full_messages # => ["name cannot be nil"] @@ -99,11 +110,9 @@ module ActiveModel # person.errors.include?(:name) # => true # person.errors.include?(:age) # => false def include?(attribute) - messages[attribute].present? + messages.key?(attribute) && messages[attribute].present? end - # aliases include? alias :has_key? :include? - # aliases include? alias :key? :include? # Get messages for +key+. @@ -123,9 +132,9 @@ module ActiveModel # Set messages for +key+ to +value+. # - # person.errors.get(:name) # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.set(:name, ["can't be nil"]) - # person.errors.get(:name) # => ["can't be nil"] + # person.errors[:name] # => ["can't be nil"] def set(key, value) ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1. @@ -138,12 +147,12 @@ module ActiveModel # Delete messages for +key+. Returns the deleted messages. # - # person.errors.get(:name) # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] - # person.errors.get(:name) # => [] + # person.errors[:name] # => [] def delete(key) - messages.delete(key) details.delete(key) + messages.delete(key) end # When passed a symbol or a name of a method, returns an array of errors @@ -151,6 +160,15 @@ module ActiveModel # # person.errors[:name] # => ["cannot be nil"] # person.errors['name'] # => ["cannot be nil"] + # + # Note that, if you try to get errors of an attribute which has + # no errors associated with it, this method will instantiate + # an empty error list for it and +keys+ will return an array + # of error keys which includes this attribute. + # + # person.errors.keys # => [] + # person.errors[:name] # => [] + # person.errors.keys # => [:name] def [](attribute) messages[attribute.to_sym] end @@ -198,6 +216,7 @@ module ActiveModel def size values.flatten.size end + alias :count :size # Returns all message values. # @@ -215,35 +234,15 @@ module ActiveModel messages.keys end - # Returns an array of error messages, with the attribute name included. - # - # person.errors.add(:name, :blank, message: "can't be blank") - # person.errors.add(:name, :not_specified, message: "must be specified") - # person.errors.to_a # => ["name can't be blank", "name must be specified"] - def to_a - full_messages - end - - # Returns the number of error messages. - # - # person.errors.add(:name, :blank, message: "can't be blank") - # person.errors.count # => 1 - # person.errors.add(:name, :not_specified, message: "must be specified") - # person.errors.count # => 2 - def count - to_a.size - end - # 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 cannot be nil"] # person.errors.empty? # => false def empty? - all? { |k, v| v && v.empty? && !v.is_a?(String) } + size.zero? end - # aliases empty? - alias_method :blank?, :empty? + alias :blank? :empty? # Returns an xml formatted representation of the Errors hash. # @@ -311,9 +310,9 @@ module ActiveModel # <tt>:strict</tt> option can also be set to any other exception. # # person.errors.add(:name, :invalid, strict: true) - # # => ActiveModel::StrictValidationFailed: name is invalid + # # => ActiveModel::StrictValidationFailed: Name is invalid # person.errors.add(:name, :invalid, strict: NameIsInvalid) - # # => NameIsInvalid: name is invalid + # # => NameIsInvalid: Name is invalid # # person.errors.messages # => {} # @@ -328,7 +327,7 @@ module ActiveModel # # => {:base=>[{error: :name_or_email_blank}]} def add(attribute, message = :invalid, options = {}) message = message.call if message.respond_to?(:call) - detail = normalize_detail(attribute, message, options) + detail = normalize_detail(message, options) message = normalize_message(attribute, message, options) if exception = options[:strict] exception = ActiveModel::StrictValidationFailed if exception == true @@ -347,7 +346,7 @@ module ActiveModel # # => {:name=>["can't be empty"]} def add_on_empty(attributes, options = {}) ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1 + ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1. To achieve the same use: @@ -369,7 +368,7 @@ module ActiveModel # # => {:name=>["can't be blank"]} def add_on_blank(attributes, options = {}) ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1 + ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1. To achieve the same use: @@ -406,6 +405,7 @@ module ActiveModel def full_messages map { |attribute, message| full_message(attribute, message) } end + alias :to_a :full_messages # Returns all the full error messages for a given attribute in an array. # @@ -471,7 +471,6 @@ module ActiveModel defaults = [] end - defaults << options.delete(:message) defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope) defaults << :"errors.attributes.#{attribute}.#{type}" defaults << :"errors.messages.#{type}" @@ -480,13 +479,15 @@ module ActiveModel defaults.flatten! key = defaults.shift + defaults = options.delete(:message) if options[:message] value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil) options = { default: defaults, model: @base.model_name.human, attribute: @base.class.human_attribute_name(attribute), - value: value + value: value, + object: @base }.merge!(options) I18n.translate(key, options) @@ -502,7 +503,7 @@ module ActiveModel end end - def normalize_detail(attribute, message, options) + def normalize_detail(message, options) { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)) end end @@ -525,7 +526,20 @@ module ActiveModel class StrictValidationFailed < StandardError end + # Raised when attribute values are out of range. + class RangeError < ::RangeError + end + # Raised when unknown attributes are supplied via mass assignment. + # + # class Person + # include ActiveModel::AttributeAssignment + # include ActiveModel::Validations + # end + # + # person = Person.new + # person.assign_attributes(name: 'Gorby') + # # => ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person. class UnknownAttributeError < NoMethodError attr_reader :record, :attribute diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb index b4fa378601..d2c6a89cc2 100644 --- a/activemodel/lib/active_model/forbidden_attributes_protection.rb +++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb @@ -17,8 +17,9 @@ module ActiveModel module ForbiddenAttributesProtection # :nodoc: protected def sanitize_for_mass_assignment(attributes) - if attributes.respond_to?(:permitted?) && !attributes.permitted? - raise ActiveModel::ForbiddenAttributesError + if attributes.respond_to?(:permitted?) + raise ActiveModel::ForbiddenAttributesError if !attributes.permitted? + attributes.to_h else attributes end diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 762f4fe939..4a8ee915cf 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -6,7 +6,7 @@ module ActiveModel module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 22010b517c..d86ef6224e 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/delegation' module ActiveModel class Name @@ -163,7 +164,7 @@ module ActiveModel @route_key << "_index" if @plural == @singular end - # Transform the model name into a more humane format, using I18n. By default, + # Transform the model name into a more human format, using I18n. By default, # it will underscore then humanize the class name. # # class BlogPost @@ -191,7 +192,7 @@ module ActiveModel private def _singularize(string) - ActiveSupport::Inflector.underscore(string).tr('/', '_') + ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze) end end @@ -225,7 +226,7 @@ module ActiveModel # (See ActiveModel::Name for more information). # # class Person - # include ActiveModel::Model + # extend ActiveModel::Naming # end # # Person.model_name.name # => "Person" diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 976f50b13e..70e10fa06d 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -31,16 +31,14 @@ module ActiveModel # 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, 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. + # ActiveModel::Serializers::JSON module automatically includes + # the <tt>ActiveModel::Serialization</tt> module, so there is no need to + # explicitly include <tt>ActiveModel::Serialization</tt>. # - # A minimal implementation including XML and JSON would be: + # A minimal implementation including JSON would be: # # class Person # include ActiveModel::Serializers::JSON - # include ActiveModel::Serializers::Xml # # attr_accessor :name # @@ -55,13 +53,11 @@ module ActiveModel # person.serializable_hash # => {"name"=>nil} # person.as_json # => {"name"=>nil} # person.to_json # => "{\"name\":null}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # person.as_json # => {"name"=>"Bob"} # person.to_json # => "{\"name\":\"Bob\"}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and # <tt>:include</tt>. The following are all valid examples: @@ -94,6 +90,37 @@ module ActiveModel # person.serializable_hash(except: :name) # => {"age"=>22} # person.serializable_hash(methods: :capitalized_name) # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"} + # + # Example with <tt>:include</tt> option + # + # class User + # include ActiveModel::Serializers::JSON + # attr_accessor :name, :notes # Emulate has_many :notes + # def attributes + # {'name' => nil} + # end + # end + # + # class Note + # include ActiveModel::Serializers::JSON + # attr_accessor :title, :text + # def attributes + # {'title' => nil, 'text' => nil} + # end + # end + # + # note = Note.new + # note.title = 'Battle of Austerlitz' + # note.text = 'Some text here' + # + # user = User.new + # user.name = 'Napoleon' + # user.notes = [note] + # + # user.serializable_hash + # # => {"name" => "Napoleon"} + # user.serializable_hash(include: { notes: { only: 'title' }}) + # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]} def serializable_hash(options = nil) options ||= {} @@ -107,7 +134,7 @@ module ActiveModel hash = {} attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } + Array(options[:methods]).each { |m| hash[m.to_s] = send(m) } serializable_add_includes(options) do |association, records, opts| hash[association.to_s] = if records.respond_to?(:to_ary) diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index b66dbf1afe..b64a8299e6 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,7 +10,7 @@ module ActiveModel included do extend ActiveModel::Naming - class_attribute :include_root_in_json + class_attribute :include_root_in_json, instance_writer: false self.include_root_in_json = false end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb deleted file mode 100644 index e33c766627..0000000000 --- a/activemodel/lib/active_model/serializers/xml.rb +++ /dev/null @@ -1,238 +0,0 @@ -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' -require 'active_support/core_ext/time/acts_like' - -module ActiveModel - 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 - - if value.acts_like?(:time) && value.respond_to?(:in_time_zone) - value = value.in_time_zone - end - - @value = value - @type = compute_type - end - - def decorations - decorations = {} - decorations[:encoding] = 'base64' if type == :binary - decorations[:type] = (type == :string) ? nil : type - decorations[:nil] = true if value.nil? - decorations - end - - protected - - def compute_type - return if value.nil? - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] - type ||= :string if value.respond_to?(:to_str) - type ||= :yaml - type - end - end - - class MethodAttribute < Attribute #:nodoc: - end - - attr_reader :options - - def initialize(serializable, options = nil) - @serializable = serializable - @options = options ? options.dup : {} - end - - def serializable_hash - @serializable.serializable_hash(@options.except(:include)) - end - - def serializable_collection - methods = Array(options[:methods]).map(&:to_s) - serializable_hash.map do |name, value| - name = name.to_s - if methods.include?(name) - self.class::MethodAttribute.new(name, @serializable, value) - else - self.class::Attribute.new(name, @serializable, value) - end - end - end - - def serialize - require 'builder' unless defined? ::Builder - - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - - @builder = options[:builder] - @builder.instruct! unless options[:skip_instruct] - - 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] - - @builder.tag!(*args) do - add_attributes_and_methods - add_includes - add_extra_behavior - add_procs - yield @builder if block_given? - end - end - - private - - def add_extra_behavior - end - - def add_attributes_and_methods - serializable_collection.each do |attribute| - key = ActiveSupport::XmlMini.rename_key(attribute.name, options) - ActiveSupport::XmlMini.to_tag(key, attribute.value, - options.merge(attribute.decorations)) - end - end - - def add_includes - @serializable.send(:serializable_add_includes, options) do |association, records, opts| - add_associations(association, records, opts) - end - end - - # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. - def add_associations(association, records, opts) - merged_options = opts.merge(options.slice(:builder, :indent)) - merged_options[:skip_instruct] = true - - [:skip_types, :dasherize, :camelize].each do |key| - merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil? - end - - if records.respond_to?(:to_ary) - records = records.to_ary - - tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) - type = options[:skip_types] ? { } : { type: "array" } - association_name = association.to_s.singularize - merged_options[:root] = association_name - - if records.empty? - @builder.tag!(tag, type) - else - @builder.tag!(tag, type) do - records.each do |record| - if options[:skip_types] - record_type = {} - else - record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name - record_type = { type: record_class } - end - - record.to_xml merged_options.merge(record_type) - end - end - end - else - merged_options[:root] = association.to_s - - unless records.class.to_s.underscore == association.to_s - merged_options[:type] = records.class.name - end - - records.to_xml merged_options - end - end - - def add_procs - if procs = options.delete(:procs) - Array(procs).each do |proc| - if proc.arity == 1 - proc.call(options) - else - proc.call(options, @serializable) - end - end - end - end - end - - # Returns XML representing the model. Configuration can be - # passed through +options+. - # - # Without any +options+, the returned XML string will include all the - # model's attributes. - # - # user = User.find(1) - # user.to_xml - # - # <?xml version="1.0" encoding="UTF-8"?> - # <user> - # <id type="integer">1</id> - # <name>David</name> - # <age type="integer">16</age> - # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> - # </user> - # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the - # attributes included, and work similar to the +attributes+ method. - # - # To include the result of some method calls on the model use <tt>:methods</tt>. - # - # To include associations use <tt>:include</tt>. - # - # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt> - def to_xml(options = {}, &block) - Serializer.new(self, options).serialize(&block) - end - - # Sets the model +attributes+ from an XML string. Returns +self+. - # - # class Person - # include ActiveModel::Serializers::Xml - # - # attr_accessor :name, :age, :awesome - # - # def attributes=(hash) - # hash.each do |key, value| - # instance_variable_set("@#{key}", value) - # end - # end - # - # def attributes - # instance_values - # end - # end - # - # xml = { name: 'bob', age: 22, awesome:true }.to_xml - # person = Person.new - # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob"> - # person.name # => "bob" - # person.age # => 22 - # person.awesome # => true - def from_xml(xml) - self.attributes = Hash.from_xml(xml).values.first - self - end - end - end -end diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb new file mode 100644 index 0000000000..6ec3452478 --- /dev/null +++ b/activemodel/lib/active_model/type.rb @@ -0,0 +1,59 @@ +require 'active_model/type/helpers' +require 'active_model/type/value' + +require 'active_model/type/big_integer' +require 'active_model/type/binary' +require 'active_model/type/boolean' +require 'active_model/type/date' +require 'active_model/type/date_time' +require 'active_model/type/decimal' +require 'active_model/type/decimal_without_scale' +require 'active_model/type/float' +require 'active_model/type/immutable_string' +require 'active_model/type/integer' +require 'active_model/type/string' +require 'active_model/type/text' +require 'active_model/type/time' +require 'active_model/type/unsigned_integer' + +require 'active_model/type/registry' + +module ActiveModel + module Type + @registry = Registry.new + + class << self + attr_accessor :registry # :nodoc: + delegate :add_modifier, to: :registry + + # Add a new type to the registry, allowing it to be referenced as a + # symbol by ActiveModel::Attributes::ClassMethods#attribute. If your + # type is only meant to be used with a specific database adapter, you can + # do so by passing +adapter: :postgresql+. If your type has the same + # name as a native type for the current adapter, an exception will be + # raised unless you specify an +:override+ option. +override: true+ will + # cause your type to be used instead of the native type. +override: + # false+ will cause the native type to be used over yours if one exists. + def register(type_name, klass = nil, **options, &block) + registry.register(type_name, klass, **options, &block) + end + + def lookup(*args, **kwargs) # :nodoc: + registry.lookup(*args, **kwargs) + end + end + + register(:big_integer, Type::BigInteger) + register(:binary, Type::Binary) + register(:boolean, Type::Boolean) + register(:date, Type::Date) + register(:datetime, Type::DateTime) + register(:decimal, Type::Decimal) + register(:float, Type::Float) + register(:immutable_string, Type::ImmutableString) + register(:integer, Type::Integer) + register(:string, Type::String) + register(:text, Type::Text) + register(:time, Type::Time) + end +end diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb new file mode 100644 index 0000000000..4168cbfce7 --- /dev/null +++ b/activemodel/lib/active_model/type/big_integer.rb @@ -0,0 +1,13 @@ +require 'active_model/type/integer' + +module ActiveModel + module Type + class BigInteger < Integer # :nodoc: + private + + def max_value + ::Float::INFINITY + end + end + end +end diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb new file mode 100644 index 0000000000..a0cc45b4c3 --- /dev/null +++ b/activemodel/lib/active_model/type/binary.rb @@ -0,0 +1,50 @@ +module ActiveModel + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def cast(value) + if value.is_a?(Data) + value.to_s + else + super + end + end + + def serialize(value) + return if value.nil? + Data.new(super) + end + + def changed_in_place?(raw_old_value, value) + old_value = deserialize(raw_old_value) + old_value != value + end + + class Data # :nodoc: + def initialize(value) + @value = value.to_s + end + + def to_s + @value + end + alias_method :to_str, :to_s + + def hex + @value.unpack('H*')[0] + end + + def ==(other) + other == to_s || super + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb new file mode 100644 index 0000000000..c1bce98c87 --- /dev/null +++ b/activemodel/lib/active_model/type/boolean.rb @@ -0,0 +1,21 @@ +module ActiveModel + module Type + class Boolean < Value # :nodoc: + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set + + def type + :boolean + end + + private + + def cast_value(value) + if value == '' + nil + else + !FALSE_VALUES.include?(value) + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb new file mode 100644 index 0000000000..f74243a22c --- /dev/null +++ b/activemodel/lib/active_model/type/date.rb @@ -0,0 +1,50 @@ +module ActiveModel + module Type + class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + + def type + :date + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ + def fast_string_to_date(string) + if string =~ ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end + + def value_from_multiparameter_assignment(*) + time = super + time && time.to_date + end + end + end +end diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb new file mode 100644 index 0000000000..2f2df4320f --- /dev/null +++ b/activemodel/lib/active_model/type/date_time.rb @@ -0,0 +1,44 @@ +module ActiveModel + module Type + class DateTime < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) + + def type + :datetime + end + + private + + def cast_value(value) + return apply_seconds_precision(value) unless value.is_a?(::String) + return if value.empty? + + fast_string_to_time(value) || fallback_string_to_time(value) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + + def value_from_multiparameter_assignment(values_hash) + missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } + if missing_parameter + raise ArgumentError, missing_parameter + end + super + end + end + end +end diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb new file mode 100644 index 0000000000..11ea327026 --- /dev/null +++ b/activemodel/lib/active_model/type/decimal.rb @@ -0,0 +1,60 @@ +require "bigdecimal/util" + +module ActiveModel + module Type + class Decimal < Value # :nodoc: + include Helpers::Numeric + + def type + :decimal + end + + def type_cast_for_schema(value) + value.to_s.inspect + end + + private + + def cast_value(value) + casted_value = case value + when ::Float + convert_float_to_big_decimal(value) + when ::Numeric, ::String + BigDecimal(value, precision.to_i) + else + if value.respond_to?(:to_d) + value.to_d + else + cast_value(value.to_s) + end + end + + apply_scale(casted_value) + end + + def convert_float_to_big_decimal(value) + if precision + BigDecimal(apply_scale(value), float_precision) + else + value.to_d + end + end + + def float_precision + if precision.to_i > ::Float::DIG + 1 + ::Float::DIG + 1 + else + precision.to_i + end + end + + def apply_scale(value) + if scale + value.round(scale) + else + value + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb new file mode 100644 index 0000000000..129baa0c10 --- /dev/null +++ b/activemodel/lib/active_model/type/decimal_without_scale.rb @@ -0,0 +1,11 @@ +require 'active_model/type/big_integer' + +module ActiveModel + module Type + class DecimalWithoutScale < BigInteger # :nodoc: + def type + :decimal + end + end + end +end diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb new file mode 100644 index 0000000000..0f925bc7e1 --- /dev/null +++ b/activemodel/lib/active_model/type/float.rb @@ -0,0 +1,25 @@ +module ActiveModel + module Type + class Float < Value # :nodoc: + include Helpers::Numeric + + def type + :float + end + + alias serialize cast + + private + + def cast_value(value) + case value + when ::Float then value + when "Infinity" then ::Float::INFINITY + when "-Infinity" then -::Float::INFINITY + when "NaN" then ::Float::NAN + else value.to_f + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb new file mode 100644 index 0000000000..a805a359ab --- /dev/null +++ b/activemodel/lib/active_model/type/helpers.rb @@ -0,0 +1,4 @@ +require 'active_model/type/helpers/accepts_multiparameter_time' +require 'active_model/type/helpers/numeric' +require 'active_model/type/helpers/mutable' +require 'active_model/type/helpers/time_value' diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb new file mode 100644 index 0000000000..facea12704 --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb @@ -0,0 +1,35 @@ +module ActiveModel + module Type + module Helpers + class AcceptsMultiparameterTime < Module # :nodoc: + def initialize(defaults: {}) + define_method(:cast) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:assert_valid_value) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:value_from_multiparameter_assignment) do |values_hash| + defaults.each do |k, v| + values_hash[k] ||= v + end + return unless values_hash[1] && values_hash[2] && values_hash[3] + values = values_hash.sort.map(&:last) + ::Time.send(default_timezone, *values) + end + private :value_from_multiparameter_assignment + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb new file mode 100644 index 0000000000..4dddbe4e5e --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/mutable.rb @@ -0,0 +1,18 @@ +module ActiveModel + module Type + module Helpers + module Mutable # :nodoc: + def cast(value) + deserialize(serialize(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != serialize(new_value) + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb new file mode 100644 index 0000000000..c883010506 --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -0,0 +1,34 @@ +module ActiveModel + module Type + module Helpers + module Numeric # :nodoc: + def cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A-?\d+\.?\d*\z/ + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb new file mode 100644 index 0000000000..63993c0d93 --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -0,0 +1,77 @@ +require "active_support/core_ext/time/zones" + +module ActiveModel + module Type + module Helpers + module TimeValue # :nodoc: + def serialize(value) + value = apply_seconds_precision(value) + + if value.acts_like?(:time) + zone_conversion_method = is_utc? ? :getutc : :getlocal + + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + end + + value + end + + def is_utc? + ::Time.zone_default.nil? || ::Time.zone_default =~ 'UTC' + end + + def default_timezone + if is_utc? + :utc + else + :local + end + end + + def apply_seconds_precision(value) + return value unless precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value.change(usec: value.usec / round_power * round_power) + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + def user_input_in_time_zone(value) + value.in_time_zone + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + is_utc? ? time : time.getlocal + else + ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/immutable_string.rb b/activemodel/lib/active_model/type/immutable_string.rb new file mode 100644 index 0000000000..20b8ca0cc4 --- /dev/null +++ b/activemodel/lib/active_model/type/immutable_string.rb @@ -0,0 +1,29 @@ +module ActiveModel + module Type + class ImmutableString < Value # :nodoc: + def type + :string + end + + def serialize(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when true then "t" + when false then "f" + else super + end + end + + private + + def cast_value(value) + result = case value + when true then "t" + when false then "f" + else value.to_s + end + result.freeze + end + end + end +end diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb new file mode 100644 index 0000000000..eea2280b22 --- /dev/null +++ b/activemodel/lib/active_model/type/integer.rb @@ -0,0 +1,66 @@ +module ActiveModel + module Type + class Integer < Value # :nodoc: + include Helpers::Numeric + + # Column storage size in bytes. + # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. + DEFAULT_LIMIT = 4 + + def initialize(*) + super + @range = min_value...max_value + end + + def type + :integer + end + + def deserialize(value) + return if value.nil? + value.to_i + end + + def serialize(value) + result = cast(value) + if result + ensure_in_range(result) + end + result + end + + protected + + attr_reader :range + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else + value.to_i rescue nil + end + end + + def ensure_in_range(value) + unless range.cover?(value) + raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" + end + end + + def max_value + 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign + end + + def min_value + -max_value + end + + def _limit + self.limit || DEFAULT_LIMIT + end + end + end +end diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb new file mode 100644 index 0000000000..adc88eb624 --- /dev/null +++ b/activemodel/lib/active_model/type/registry.rb @@ -0,0 +1,64 @@ +module ActiveModel + # :stopdoc: + module Type + class Registry + def initialize + @registrations = [] + end + + def register(type_name, klass = nil, **options, &block) + block ||= proc { |_, *args| klass.new(*args) } + registrations << registration_klass.new(type_name, block, **options) + end + + def lookup(symbol, *args) + registration = find_registration(symbol, *args) + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + protected + + attr_reader :registrations + + private + + def registration_klass + Registration + end + + def find_registration(symbol, *args) + registrations.find { |r| r.matches?(symbol, *args) } + end + end + + class Registration + # Options must be taken because of https://bugs.ruby-lang.org/issues/10856 + def initialize(name, block, **) + @name = name + @block = block + end + + def call(_registry, *args, **kwargs) + if kwargs.any? # https://bugs.ruby-lang.org/issues/10856 + block.call(*args, **kwargs) + else + block.call(*args) + end + end + + def matches?(type_name, *args, **kwargs) + type_name == name + end + + protected + + attr_reader :name, :block + end + end + # :startdoc: +end diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb new file mode 100644 index 0000000000..8a91410998 --- /dev/null +++ b/activemodel/lib/active_model/type/string.rb @@ -0,0 +1,19 @@ +require "active_model/type/immutable_string" + +module ActiveModel + module Type + class String < ImmutableString # :nodoc: + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + private + + def cast_value(value) + ::String.new(super) + end + end + end +end diff --git a/activemodel/lib/active_model/type/text.rb b/activemodel/lib/active_model/type/text.rb new file mode 100644 index 0000000000..1ad04daba4 --- /dev/null +++ b/activemodel/lib/active_model/type/text.rb @@ -0,0 +1,11 @@ +require 'active_model/type/string' + +module ActiveModel + module Type + class Text < String # :nodoc: + def type + :text + end + end + end +end diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb new file mode 100644 index 0000000000..34e09f0aba --- /dev/null +++ b/activemodel/lib/active_model/type/time.rb @@ -0,0 +1,46 @@ +module ActiveModel + module Type + class Time < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + ) + + def type + :time + end + + def user_input_in_time_zone(value) + return unless value.present? + + case value + when ::String + value = "2000-01-01 #{value}" + when ::Time + value = value.change(year: 2000, day: 1, month: 1) + end + + super(value) + end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + if value =~ /^2000-01-01/ + dummy_time_value = value + else + dummy_time_value = "2000-01-01 #{value}" + end + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/unsigned_integer.rb b/activemodel/lib/active_model/type/unsigned_integer.rb new file mode 100644 index 0000000000..3f49f9f5f7 --- /dev/null +++ b/activemodel/lib/active_model/type/unsigned_integer.rb @@ -0,0 +1,15 @@ +module ActiveModel + module Type + class UnsignedInteger < Integer # :nodoc: + private + + def max_value + super * 2 + end + + def min_value + 0 + end + end + end +end diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb new file mode 100644 index 0000000000..0d2d6873a8 --- /dev/null +++ b/activemodel/lib/active_model/type/value.rb @@ -0,0 +1,116 @@ +module ActiveModel + module Type + class Value + attr_reader :precision, :scale, :limit + + def initialize(precision: nil, limit: nil, scale: nil) + @precision = precision + @scale = scale + @limit = limit + end + + def type # :nodoc: + end + + # Converts a value from database input to the appropriate ruby type. The + # return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. The default + # implementation just calls Value#cast. + # + # +value+ The raw input, as provided from the database. + def deserialize(value) + cast(value) + end + + # Type casts a value from user input (e.g. from a setter). This value may + # be a string from the form builder, or a ruby object passed to a setter. + # There is currently no way to differentiate between which source it came + # from. + # + # The return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. See also: + # Value#cast_value. + # + # +value+ The raw input, as provided to the attribute setter. + def cast(value) + cast_value(value) unless value.nil? + end + + # Casts a value from the ruby type to a type that the database knows how + # to understand. The returned value from this method should be a + # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or + # +nil+. + def serialize(value) + value + end + + # Type casts a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: + value.inspect + end + + # These predicates are not documented, as I need to look further into + # their use, and see if they can be removed entirely. + def binary? # :nodoc: + false + end + + # Determines whether a value has changed for dirty checking. +old_value+ + # and +new_value+ will always be type-cast. Types should not need to + # override this method. + def changed?(old_value, new_value, _new_value_before_type_cast) + old_value != new_value + end + + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. If your type returns an object + # which could be mutated, you should override this method. You will need + # to either: + # + # - pass +new_value+ to Value#serialize and compare it to + # +raw_old_value+ + # + # or + # + # - pass +raw_old_value+ to Value#deserialize and compare it to + # +new_value+ + # + # +raw_old_value+ The original value, before being passed to + # +deserialize+. + # + # +new_value+ The current value, after type casting. + def changed_in_place?(raw_old_value, new_value) + false + end + + def map(value) # :nodoc: + yield value + end + + def ==(other) + self.class == other.class && + precision == other.precision && + scale == other.scale && + limit == other.limit + end + alias eql? == + + def hash + [self.class, precision, scale, limit].hash + end + + def assert_valid_value(*) + end + + private + + # Convenience method for types which do not need separate type casting + # behavior for user and database inputs. Called by Value#cast for + # values except +nil+. + def cast_value(value) # :doc: + value + end + end + end +end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 176d4c0607..8159b9b1d3 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -47,9 +47,10 @@ module ActiveModel include HelperMethods attr_accessor :validation_context + private :validation_context= define_callbacks :validate, scope: :name - class_attribute :_validators + class_attribute :_validators, instance_writer: false self._validators = Hash.new { |h,k| h[k] = [] } end @@ -87,7 +88,7 @@ module ActiveModel validates_with BlockValidator, _merge_attributes(attr_names), &block end - VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze + VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc: # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldy and @@ -129,6 +130,9 @@ module ActiveModel # end # end # + # Note that the return value of validation methods is not relevant. + # It's not possible to halt the validate callback chain. + # # Options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. # Runs in all validation contexts by default (nil). You can pass a symbol @@ -159,7 +163,7 @@ module ActiveModel options = options.dup options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - Array(options[:on]).include?(o.validation_context) + !(Array(options[:on]) & Array(o.validation_context)).empty? } end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index ee160fb483..a04e5f347e 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -14,16 +14,59 @@ module ActiveModel end 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) - klass.send(:attr_writer, *attr_writers) + klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes))) end def acceptable_option?(value) Array(options[:accept]).include?(value) end + + class LazilyDefineAttributes < Module + def initialize(attribute_definition) + define_method(:respond_to_missing?) do |method_name, include_private=false| + super(method_name, include_private) || attribute_definition.matches?(method_name) + end + + define_method(:method_missing) do |method_name, *args, &block| + if attribute_definition.matches?(method_name) + attribute_definition.define_on(self.class) + send(method_name, *args, &block) + else + super(method_name, *args, &block) + end + end + end + end + + class AttributeDefinition + def initialize(attributes) + @attributes = attributes.map(&:to_s) + end + + def matches?(method_name) + attr_name = convert_to_reader_name(method_name) + attributes.include?(attr_name) + end + + def define_on(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) + klass.send(:attr_writer, *attr_writers) + end + + protected + + attr_reader :attributes + + private + + def convert_to_reader_name(method_name) + method_name.to_s.chomp('=') + end + end end module HelperMethods @@ -42,9 +85,10 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <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 + # * <tt>:accept</tt> - Specifies a value that is considered accepted. + # Also accepts an array of possible values. The default value is + # an array ["1", true], which makes it easy to relate to an HTML + # checkbox. This should be set to, or include, +true+ if you are validating # a database column, since the attribute is typecast from "1" to +true+ # before validation. # diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4b58ef66e3..a201f72ed0 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -23,13 +23,13 @@ module ActiveModel included do include ActiveSupport::Callbacks define_callbacks :validation, + terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name] end module ClassMethods - # Defines a callback that will get called right before validation - # happens. + # Defines a callback that will get called right before validation. # # class Person # include ActiveModel::Validations @@ -64,8 +64,7 @@ module ActiveModel set_callback(:validation, :before, *args, &block) end - # Defines a callback that will get called right after validation - # happens. + # Defines a callback that will get called right after validation. # # class Person # include ActiveModel::Validations diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb index bad9e4f9a9..d49af603bb 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -30,14 +30,15 @@ module ActiveModel @delimiter ||= options[:in] || options[:within] end - # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all + # In Ruby 2.2 <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. + # endpoints, which is fast but is only accurate on Numeric, Time, Date, + # or DateTime ranges. def inclusion_method(enumerable) if enumerable.is_a? Range case enumerable.first - when Numeric, Time, DateTime + when Numeric, Time, DateTime, Date :cover? else :include? diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 1b11c28087..8f8ade90bb 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -3,14 +3,16 @@ module ActiveModel module Validations class ConfirmationValidator < EachValidator # :nodoc: def initialize(options) - super + super({ case_sensitive: true }.merge!(options)) 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)) + if (confirmed = record.send("#{attribute}_confirmation")) + unless confirmation_value_equal?(record, attribute, value, confirmed) + human_attribute_name = record.class.human_attribute_name(attribute) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name)) + end end end @@ -24,6 +26,14 @@ module ActiveModel :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=") end.compact) end + + def confirmation_value_equal?(record, attribute, value, confirmed) + if !options[:case_sensitive] && value.is_a?(String) + value.casecmp(confirmed) == 0 + else + value == confirmed + end + end end module HelperMethods @@ -55,6 +65,8 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "doesn't match # <tt>%{translated_attribute_name}</tt>"). + # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by + # non-text columns (+true+ by default). # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index f342d27275..6f4276cc2a 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -29,7 +29,9 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of items that the value shouldn't # be part of. 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 + # enumerable. If the enumerable is a numerical, time or datetime 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>Range#cover?</tt>, otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is diff --git a/activemodel/lib/active_model/validations/helper_methods.rb b/activemodel/lib/active_model/validations/helper_methods.rb new file mode 100644 index 0000000000..2176115334 --- /dev/null +++ b/activemodel/lib/active_model/validations/helper_methods.rb @@ -0,0 +1,13 @@ +module ActiveModel + module Validations + module HelperMethods # :nodoc: + private + def _merge_attributes(attr_names) + options = attr_names.extract_options!.symbolize_keys + attr_names.flatten! + options[:attributes] = attr_names + options + end + end + end +end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index c84025f083..03e0ef56d8 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -28,9 +28,9 @@ 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 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. + # enumerable is a numerical, time or datetime 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"). diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 23201b264a..79297ac119 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,6 +1,6 @@ -module ActiveModel +require "active_support/core_ext/string/strip" - # == Active \Model Length Validator +module ActiveModel module Validations class LengthValidator < EachValidator # :nodoc: MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze @@ -18,6 +18,27 @@ module ActiveModel options[:minimum] = 1 end + if options[:tokenizer] + ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc) + The `:tokenizer` option is deprecated, and will be removed in Rails 5.1. + You can achieve the same functionality by defining an instance method + with the value that you want to validate the length of. For example, + + validates_length_of :essay, minimum: 100, + tokenizer: ->(str) { str.scan(/\w+/) } + + should be written as + + validates_length_of :words_in_essay, minimum: 100 + + private + + def words_in_essay + essay.scan(/\w+/) + end + EOS + end + super end @@ -77,8 +98,9 @@ module ActiveModel module HelperMethods - # Validates that the specified attribute matches the length restrictions - # supplied. Only one option can be used at a time: + # Validates that the specified attributes match the length restrictions + # supplied. Only one constraint option can be used at a time apart from + # +:minimum+ and +:maximum+ that can be combined together: # # class Person < ActiveRecord::Base # validates_length_of :first_name, maximum: 30 @@ -88,36 +110,39 @@ module ActiveModel # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name' # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters' # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.', - # tokenizer: ->(str) { str.scan(/\w+/) } + # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.' + # + # private + # + # def words_in_essay + # essay.scan(/\w+/) + # end # end # - # Configuration options: + # Constraint options: + # # * <tt>:minimum</tt> - The minimum size of the attribute. # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by - # default if not used with :minimum. + # 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. # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>. + # + # Other options: + # # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. # * <tt>:too_long</tt> - The error message if the attribute goes over the # maximum (default is: "is too long (maximum is %{count} characters)"). # * <tt>:too_short</tt> - The error message if the attribute goes under the - # minimum (default is: "is too short (min is %{count} characters)"). + # minimum (default is: "is too short (minimum is %{count} characters)"). # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> # method and the attribute is the wrong size (default is: "is the wrong # length (should be %{count} characters)"). # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. - # * <tt>:tokenizer</tt> - A method (as a symbol), proc or string to - # specify how to split up the attribute string. (e.g. - # <tt>tokenizer: :word_tokenizer</tt> to call the +word_tokenizer+ method - # or <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> to count words as in - # above example). Defaults to <tt>->(value) { value.split(//) }</tt> which - # counts individual characters. # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+ and +:strict+. diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 4ba4e3e8f7..ad7012df48 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -20,7 +20,7 @@ module ActiveModel def validate_each(record, attr_name, value) before_type_cast = :"#{attr_name}_before_type_cast" - raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) + raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) && record.send(before_type_cast) != value raw_value ||= value if record_attribute_changed_in_place?(record, attr_name) @@ -29,16 +29,18 @@ module ActiveModel return if options[:allow_nil] && raw_value.nil? - unless value = parse_raw_value_as_a_number(raw_value) + unless is_number?(raw_value) record.errors.add(attr_name, :not_a_number, filtered_options(raw_value)) return end - 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 - end + if allow_only_integer?(record) && !is_integer?(raw_value) + record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value)) + return + end + + unless raw_value.is_a?(Numeric) + value = parse_raw_value_as_a_number(raw_value) end options.slice(*CHECKS.keys).each do |option, option_value| @@ -64,14 +66,18 @@ module ActiveModel protected + def is_number?(raw_value) + !parse_raw_value_as_a_number(raw_value).nil? + rescue ArgumentError, TypeError + false + end + def parse_raw_value_as_a_number(raw_value) 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/ + def is_integer?(raw_value) + /\A[+-]?\d+\z/ === raw_value.to_s end def filtered_options(value) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index bda436d8d0..1da4df21e7 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -115,7 +115,7 @@ module ActiveModel key = "#{key.to_s.camelize}Validator" begin - validator = key.include?('::') ? key.constantize : const_get(key) + validator = key.include?('::'.freeze) ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index a2531327bf..6de01b3392 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -1,15 +1,5 @@ module ActiveModel module Validations - module HelperMethods - private - def _merge_attributes(attr_names) - options = attr_names.extract_options!.symbolize_keys - attr_names.flatten! - options[:attributes] = attr_names - options - end - end - class WithValidator < EachValidator # :nodoc: def validate_each(record, attr, val) method_name = options[:with] diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 1d2888a818..699f74ed17 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -100,7 +100,7 @@ module ActiveModel # PresenceValidator.kind # => :presence # UniquenessValidator.kind # => :uniqueness def self.kind - @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous? + @kind ||= name.split('::').last.underscore.chomp('_validator').to_sym unless anonymous? end # Accepts options that will be made available through the +options+ reader. @@ -163,10 +163,6 @@ module ActiveModel # +ArgumentError+ when invalid options are supplied. def check_validity! end - - def should_validate?(record) # :nodoc: - !record.persisted? || record.changed? || record.marked_for_destruction? - end end # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization |