diff options
Diffstat (limited to 'activemodel/lib/active_model')
33 files changed, 594 insertions, 377 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index e69cb5c459..97a83e58af 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -61,69 +61,12 @@ module ActiveModel CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do - class_attribute :attribute_method_matchers, :instance_writer => false + extend ActiveModel::Configuration + config_attribute :attribute_method_matchers self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] end module ClassMethods - # Defines an "attribute" method (like +inheritance_column+ or +table_name+). - # A new (class) method will be created with the given name. If a value is - # specified, the new method will return that value (as a string). - # Otherwise, the given block will be used to compute the value of the - # method. - # - # The original method will be aliased, with the new name being prefixed - # with "original_". This allows the new method to access the original - # value. - # - # Example: - # - # class Person - # - # include ActiveModel::AttributeMethods - # - # cattr_accessor :primary_key - # cattr_accessor :inheritance_column - # - # define_attr_method :primary_key, "sysid" - # define_attr_method( :inheritance_column ) do - # original_inheritance_column + "_id" - # end - # - # end - # - # Provides you with: - # - # Person.primary_key - # # => "sysid" - # Person.inheritance_column = 'address' - # Person.inheritance_column - # # => 'address_id' - def define_attr_method(name, value=nil, &block) - sing = singleton_class - sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 - if method_defined?('original_#{name}') - undef :'original_#{name}' - end - alias_method :'original_#{name}', :'#{name}' - eorb - if block_given? - sing.send :define_method, name, &block - else - # If we can compile the method name, do it. Otherwise use define_method. - # This is an important *optimization*, please don't change it. define_method - # has slower dispatch and consumes more memory. - if name =~ NAME_COMPILABLE_REGEXP - sing.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end - RUBY - else - value = value.to_s if value - sing.send(:define_method, name) { value } - end - end - end - # Declares a method available for all attributes with the given prefix. # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method. # @@ -280,7 +223,7 @@ module ActiveModel unless instance_method_already_implemented?(method_name) generate_method = "define_method_#{matcher.method_missing_target}" - if respond_to?(generate_method) + if respond_to?(generate_method, true) send(generate_method, attr_name) else define_optimized_call generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s @@ -382,14 +325,14 @@ module ActiveModel end @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' - @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/ + @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/ @method_missing_target = "#{@prefix}attribute#{@suffix}" @method_name = "#{prefix}%s#{suffix}" end def match(method_name) if @regex =~ method_name - AttributeMethodMatch.new(method_missing_target, $2, method_name) + AttributeMethodMatch.new(method_missing_target, $1, method_name) else nil end @@ -451,7 +394,7 @@ module ActiveModel protected def attribute_method?(attr_name) - attributes.include?(attr_name) + respond_to_without_attributes?(:attributes) && attributes.include?(attr_name) end private diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 37d0c9a0b9..ebb4b51aa3 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/callbacks' module ActiveModel @@ -41,7 +40,7 @@ module ActiveModel # You can choose not to have all three callbacks by passing a hash to the # define_model_callbacks method. # - # define_model_callbacks :create, :only => :after, :before + # define_model_callbacks :create, :only => [:after, :before] # # Would only create the after_create and before_create callback methods in your # class. @@ -89,11 +88,12 @@ module ActiveModel options = callbacks.extract_options! options = { :terminator => "result == false", + :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name], :only => [:before, :around, :after] }.merge(options) - types = Array.wrap(options.delete(:only)) + types = Array(options.delete(:only)) callbacks.each do |callback| define_callbacks(callback, options) @@ -125,7 +125,7 @@ module ActiveModel def self.after_#{callback}(*args, &block) options = args.extract_options! options[:prepend] = true - options[:if] = Array.wrap(options[:if]) << "!halted && value != false" + options[:if] = Array(options[:if]) << "value != false" set_callback(:#{callback}, :after, *(args << options), &block) end CALLBACK diff --git a/activemodel/lib/active_model/configuration.rb b/activemodel/lib/active_model/configuration.rb new file mode 100644 index 0000000000..ba5a6a2075 --- /dev/null +++ b/activemodel/lib/active_model/configuration.rb @@ -0,0 +1,134 @@ +require 'active_support/concern' +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/class/attribute_accessors' + +module ActiveModel + # This API is for Rails' internal use and is not currently considered 'public', so + # it may change in the future without warning. + # + # It creates configuration attributes that can be inherited from a module down + # to a class that includes the module. E.g. + # + # module MyModel + # extend ActiveModel::Configuration + # config_attribute :awesome + # self.awesome = true + # end + # + # class Post + # include MyModel + # end + # + # Post.awesome # => true + # + # Post.awesome = false + # Post.awesome # => false + # MyModel.awesome # => true + # + # We assume that the module will have a ClassMethods submodule containing methods + # to be transferred to the including class' singleton class. + # + # Config options can also be defined directly on a class: + # + # class Post + # extend ActiveModel::Configuration + # config_attribute :awesome + # end + # + # So this allows us to define a module that doesn't care about whether it is being + # included in a class or a module: + # + # module Awesomeness + # extend ActiveSupport::Concern + # + # included do + # extend ActiveModel::Configuration + # config_attribute :awesome + # self.awesome = true + # end + # end + # + # class Post + # include Awesomeness + # end + # + # module AwesomeModel + # include Awesomeness + # end + module Configuration #:nodoc: + def config_attribute(name, options = {}) + klass = self.is_a?(Class) ? ClassAttribute : ModuleAttribute + klass.new(self, name, options).define + end + + class Attribute + attr_reader :host, :name, :options + + def initialize(host, name, options) + @host, @name, @options = host, name, options + end + + def instance_writer? + options.fetch(:instance_writer, false) + end + end + + class ClassAttribute < Attribute + def define + if options[:global] + host.cattr_accessor name, :instance_writer => instance_writer? + else + host.class_attribute name, :instance_writer => instance_writer? + end + end + end + + class ModuleAttribute < Attribute + def class_methods + @class_methods ||= begin + if host.const_defined?(:ClassMethods, false) + host.const_get(:ClassMethods) + else + host.const_set(:ClassMethods, Module.new) + end + end + end + + def define + host.singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 + attr_accessor :#{name} + def #{name}?; !!#{name}; end + CODE + + name, host = self.name, self.host + + class_methods.class_eval do + define_method(name) { host.send(name) } + define_method("#{name}?") { !!send(name) } + end + + host.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}; defined?(@#{name}) ? @#{name} : self.class.#{name}; end + def #{name}?; !!#{name}; end + CODE + + if options[:global] + class_methods.class_eval do + define_method("#{name}=") { |val| host.send("#{name}=", val) } + end + else + class_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}=(val) + singleton_class.class_eval do + remove_possible_method(:#{name}) + define_method(:#{name}) { val } + end + end + CODE + end + + host.send(:attr_writer, name) if instance_writer? + end + end + end +end diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 80a3ba51c3..d7f30f0920 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -21,7 +21,7 @@ module ActiveModel # cm.to_model == self # => true # cm.to_key # => nil # cm.to_param # => nil - # cm.to_path # => "contact_messages/contact_message" + # cm.to_partial_path # => "contact_messages/contact_message" # module Conversion extend ActiveSupport::Concern @@ -39,11 +39,9 @@ module ActiveModel # Returns an Enumerable of all key attributes if any is set, regardless # if the object is persisted or not. - # - # Note the default implementation uses persisted? just because all objects - # in Ruby 1.8.x responds to <tt>:id</tt>. def to_key - persisted? ? [id] : nil + key = respond_to?(:id) && id + key ? [key] : nil end # Returns a string representing the object's key suitable for use in URLs, @@ -59,7 +57,7 @@ module ActiveModel end module ClassMethods #:nodoc: - # Provide a class level cache for the to_path. This is an + # Provide a class level cache for #to_partial_path. This is an # internal method and should not be accessed directly. def _to_partial_path #:nodoc: @_to_partial_path ||= begin diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 026f077ee7..3d35b5bb6f 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,6 +1,7 @@ require 'active_model/attribute_methods' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' +require 'active_support/core_ext/object/blank' module ActiveModel # == Active Model Dirty @@ -82,6 +83,7 @@ module ActiveModel # in-place attributes. # # person.name_will_change! + # person.name_change # => ['Bill', 'Bill'] # person.name << 'y' # person.name_change # => ['Bill', 'Billy'] module Dirty @@ -98,7 +100,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.any? + changed_attributes.present? end # List of attributes with unsaved changes. @@ -150,13 +152,15 @@ module ActiveModel # Handle <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) + return if attribute_changed?(attr) + begin value = __send__(attr) value = value.duplicable? ? value.clone : value rescue TypeError, NoMethodError end - changed_attributes[attr] = value unless changed_attributes.include?(attr) + changed_attributes[attr] = value end # Handle <tt>reset_*!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 8337b04c0d..aba6618b56 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,16 +1,13 @@ # -*- coding: utf-8 -*- -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/hash/reverse_merge' -require 'active_support/ordered_hash' module ActiveModel # == Active Model Errors # - # Provides a modified +OrderedHash+ that you can include in your object + # Provides a modified +Hash+ that you can include in your object # for handling error messages and interacting with Action Pack helpers. # # A minimal implementation could be: @@ -76,7 +73,12 @@ module ActiveModel # end def initialize(base) @base = base - @messages = ActiveSupport::OrderedHash.new + @messages = {} + end + + def initialize_dup(other) + @messages = other.messages.dup + super end # Clear the messages @@ -100,6 +102,11 @@ module ActiveModel messages[key] = value end + # Delete messages for +key+ + def delete(key) + messages.delete(key) + end + # When passed a symbol or a name of a method, returns an array of errors # for the method. # @@ -114,7 +121,7 @@ module ActiveModel # p.errors[:name] = "must be set" # p.errors[:name] # => ['must be set'] def []=(attribute, error) - self[attribute.to_sym] << error + self[attribute] << error end # Iterates through each error key, value pair in the error messages hash. @@ -122,12 +129,12 @@ module ActiveModel # has more than one error message, yields once for each error message. # # p.errors.add(:name, "can't be blank") - # p.errors.each do |attribute, errors_array| + # p.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # end # # p.errors.add(:name, "must be specified") - # p.errors.each do |attribute, errors_array| + # p.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # # then yield :name and "must be specified" # end @@ -176,8 +183,9 @@ module ActiveModel end # Returns true if no errors are found, false otherwise. + # If the error message is a string it can be empty. def empty? - all? { |k, v| v && v.empty? } + all? { |k, v| v && v.empty? && !v.is_a?(String) } end alias_method :blank?, :empty? @@ -193,28 +201,39 @@ module ActiveModel # # <error>name must be specified</error> # # </errors> def to_xml(options={}) - to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) + to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options)) end - # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object. + # Returns an Hash that can be used as the JSON representation for this object. + # Options: + # * <tt>:full_messages</tt> - determines if json object should contain + # full messages or not. Default: <tt>false</tt>. def as_json(options=nil) - to_hash + to_hash(options && options[:full_messages]) end - def to_hash - messages.dup + def to_hash(full_messages = false) + if full_messages + messages = {} + self.messages.each do |attribute, array| + messages[attribute] = array.map { |message| full_message(attribute, message) } + end + messages + else + self.messages.dup + end end # Adds +message+ to the error messages on +attribute+. More than one error can be added to the same # +attribute+. # If no +message+ is supplied, <tt>:invalid</tt> is assumed. # - # If +message+ is a symbol, it will be translated using the appropriate scope (see +translate_error+). + # If +message+ is a symbol, it will be translated using the appropriate scope (see +generate_message+). # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error. def add(attribute, message = nil, options = {}) message = normalize_message(attribute, message, options) if options[:strict] - raise ActiveModel::StrictValidationFailed, message + raise ActiveModel::StrictValidationFailed, full_message(attribute, message) end self[attribute] << message @@ -266,7 +285,7 @@ module ActiveModel # "Name is invalid" def full_message(attribute, message) return message if attribute == :base - attr_name = attribute.to_s.gsub('.', '_').humanize + attr_name = attribute.to_s.tr('.', '_').humanize attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) I18n.t(:"errors.format", { :default => "%{attribute} %{message}", @@ -327,7 +346,7 @@ module ActiveModel :model => @base.class.model_name.human, :attribute => @base.class.human_attribute_name(attribute), :value => value - }.merge(options) + }.merge!(options) I18n.translate(key, options) end @@ -336,9 +355,10 @@ module ActiveModel def normalize_message(attribute, message, options) message ||= :invalid - if message.is_a?(Symbol) + case message + when Symbol generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) - elsif message.is_a?(Proc) + when Proc message.call else message diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index bfe7ea1869..88b730626c 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -3,9 +3,13 @@ module ActiveModel # == Active Model Lint Tests # # 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. + # 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. + # + # Note an object is not required to implement all APIs in order to work + # with Action Pack. This module only intends to provide guidance in case + # you want all features out of the box. # # These tests do not attempt to determine the semantic correctness of the # returned values. For instance, you could implement valid? to always @@ -19,7 +23,8 @@ module ActiveModel # == Responds to <tt>to_key</tt> # # Returns an Enumerable of all (primary) key attributes - # or nil if model.persisted? is false + # or nil if model.persisted? is false. This is used by + # dom_id to generate unique ids for the object. def test_to_key assert model.respond_to?(:to_key), "The model should respond to to_key" def model.persisted?() false end @@ -53,22 +58,13 @@ module ActiveModel assert_kind_of String, model.to_partial_path end - # == Responds to <tt>valid?</tt> - # - # Returns a boolean that specifies whether the object is in a valid or invalid - # state. - def test_valid? - assert model.respond_to?(:valid?), "The model should respond to valid?" - assert_boolean model.valid?, "valid?" - end - # == Responds to <tt>persisted?</tt> # # Returns a boolean that specifies whether the object has been persisted yet. # This is used when calculating the URL for an object. If the object is - # not persisted, a form for that object, for instance, will be POSTed to the - # collection. If it is persisted, a form for the object will be PUT to the - # URL for the object. + # not persisted, a form for that object, for instance, will route to the + # create action. If it is persisted, a form for the object will routes to + # the update action. def test_persisted? assert model.respond_to?(:persisted?), "The model should respond to persisted?" assert_boolean model.persisted?, "persisted?" @@ -82,33 +78,23 @@ module ActiveModel def test_model_naming assert model.class.respond_to?(:model_name), "The model should respond to model_name" model_name = model.class.model_name - assert_kind_of String, model_name - assert_kind_of String, model_name.human - assert_kind_of String, model_name.singular - assert_kind_of String, model_name.plural + assert model_name.respond_to?(:to_str) + assert model_name.human.respond_to?(:to_str) + assert model_name.singular.respond_to?(:to_str) + assert model_name.plural.respond_to?(:to_str) end # == Errors Testing # - # Returns an object that has :[] and :full_messages defined on it. See below - # for more details. - # - # Returns an Array of Strings that are the errors for the attribute in - # question. If localization is used, the Strings should be localized - # for the current locale. If no error is present, this method should - # return an empty Array. + # Returns an object that implements [](attribute) defined which returns an + # Array of Strings that are the errors for the attribute in question. + # If localization is used, the Strings should be localized for the current + # locale. If no error is present, this method should return an empty Array. def test_errors_aref assert model.respond_to?(:errors), "The model should respond to errors" assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array" end - # Returns an Array of all error messages for the object. Each message - # should contain information about the field, if applicable. - def test_errors_full_messages - assert model.respond_to?(:errors), "The model should respond to errors" - assert model.errors.full_messages.is_a?(Array), "errors#full_messages should return an Array" - end - private def model assert @model.respond_to?(:to_model), "The object should respond_to to_model" diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index 44425b4a28..d17848c861 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -9,7 +9,7 @@ en: inclusion: "is not included in the list" exclusion: "is reserved" invalid: "is invalid" - confirmation: "doesn't match confirmation" + confirmation: "doesn't match %{attribute}" accepted: "must be accepted" empty: "can't be empty" blank: "can't be blank" @@ -23,5 +23,6 @@ en: equal_to: "must be equal to %{count}" less_than: "must be less than %{count}" less_than_or_equal_to: "must be less than or equal to %{count}" + other_than: "must be other than %{count}" odd: "must be odd" even: "must be even" diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 3f9feb7631..5e5405fe27 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/array/wrap' require 'active_model/mass_assignment_security/permission_set' require 'active_model/mass_assignment_security/sanitizer' @@ -10,11 +9,13 @@ module ActiveModel extend ActiveSupport::Concern included do - class_attribute :_accessible_attributes - class_attribute :_protected_attributes - class_attribute :_active_authorizer + extend ActiveModel::Configuration - class_attribute :_mass_assignment_sanitizer + config_attribute :_accessible_attributes + config_attribute :_protected_attributes + config_attribute :_active_authorizer + + config_attribute :_mass_assignment_sanitizer self.mass_assignment_sanitizer = :logger end @@ -56,7 +57,7 @@ module ActiveModel # You can specify your own sanitizer object eg. MySanitizer.new. # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for example implementation. # - # + # module ClassMethods # Attributes named in this macro are protected from mass-assignment # whenever attributes are sanitized before assignment. A role for the @@ -71,10 +72,11 @@ module ActiveModel # class Customer # include ActiveModel::MassAssignmentSecurity # - # attr_accessor :name, :credit_rating + # attr_accessor :name, :email, :logins_count # - # attr_protected :credit_rating, :last_login - # attr_protected :last_login, :as => :admin + # attr_protected :logins_count + # # Suppose that admin can not change email for customer + # attr_protected :logins_count, :email, :as => :admin # # def assign_attributes(values, options = {}) # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| @@ -83,37 +85,38 @@ module ActiveModel # end # end # - # When using the :default role : + # When using the :default role: # # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) + # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5 }, :as => :default) # customer.name # => "David" - # customer.credit_rating # => nil - # customer.last_login # => nil + # customer.email # => "a@b.com" + # customer.logins_count # => nil # - # customer.credit_rating = "Average" - # customer.credit_rating # => "Average" - # - # And using the :admin role : + # And using the :admin role: # # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5}, :as => :admin) # customer.name # => "David" - # customer.credit_rating # => "Excellent" - # customer.last_login # => nil + # customer.email # => nil + # customer.logins_count # => nil + # + # customer.email = "c@d.com" + # customer.email # => "c@d.com" # # To start from an all-closed default and enable attributes as needed, # have a look at +attr_accessible+. # - # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+ - # to sanitize attributes won't provide sufficient protection. + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of + # +attr_protected+ to sanitize attributes provides basically the same + # functionality, but it makes a bit tricky to deal with nested attributes. def attr_protected(*args) options = args.extract_options! role = options[:as] || :default self._protected_attributes = protected_attributes_configs.dup - Array.wrap(role).each do |name| + Array(role).each do |name| self._protected_attributes[name] = self.protected_attributes(name) + args end @@ -150,7 +153,7 @@ module ActiveModel # end # end # - # When using the :default role : + # When using the :default role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) @@ -160,22 +163,23 @@ module ActiveModel # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # - # And using the :admin role : + # And using the :admin role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) # customer.name # => "David" # customer.credit_rating # => "Excellent" # - # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+ - # to sanitize attributes won't provide sufficient protection. + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of + # +attr_accessible+ to sanitize attributes provides basically the same + # functionality, but it makes a bit tricky to deal with nested attributes. def attr_accessible(*args) options = args.extract_options! role = options[:as] || :default self._accessible_attributes = accessible_attributes_configs.dup - Array.wrap(role).each do |name| + Array(role).each do |name| self._accessible_attributes[name] = self.accessible_attributes(name) + args end @@ -224,12 +228,12 @@ module ActiveModel protected - def sanitize_for_mass_assignment(attributes, role = :default) + def sanitize_for_mass_assignment(attributes, role = nil) _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role)) end - def mass_assignment_authorizer(role = :default) - self.class.active_authorizer[role] + def mass_assignment_authorizer(role) + self.class.active_authorizer[role || :default] end end end diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb index a1fcdf1a38..9661349503 100644 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -13,7 +13,7 @@ module ActiveModel end def deny?(key) - raise NotImplementedError, "#deny?(key) suppose to be overwritten" + raise NotImplementedError, "#deny?(key) supposed to be overwritten" end protected diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb index bbdddfb50d..4491e07a72 100644 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -1,36 +1,31 @@ -require 'active_support/core_ext/module/delegation' - module ActiveModel module MassAssignmentSecurity class Sanitizer - def initialize(target=nil) - end - # Returns all attributes not denied by the authorizer. def sanitize(attributes, authorizer) - sanitized_attributes = attributes.reject { |key, value| authorizer.deny?(key) } - debug_protected_attribute_removal(attributes, sanitized_attributes) + rejected = [] + sanitized_attributes = attributes.reject do |key, value| + rejected << key if authorizer.deny?(key) + end + process_removed_attributes(rejected) unless rejected.empty? sanitized_attributes end protected - def debug_protected_attribute_removal(attributes, sanitized_attributes) - removed_keys = attributes.keys - sanitized_attributes.keys - process_removed_attributes(removed_keys) if removed_keys.any? - end - def process_removed_attributes(attrs) raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten" end end class LoggerSanitizer < Sanitizer - delegate :logger, :to => :@target - def initialize(target) @target = target - super + super() + end + + def logger + @target.logger end def logger? @@ -38,14 +33,18 @@ module ActiveModel end def process_removed_attributes(attrs) - logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger? + logger.warn "Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger? end end class StrictSanitizer < Sanitizer + def initialize(target = nil) + super() + end + def process_removed_attributes(attrs) return if (attrs - insensitive_attributes).empty? - raise ActiveModel::MassAssignmentSecurity::Error, "Can't mass-assign protected attributes: #{attrs.join(', ')}" + raise ActiveModel::MassAssignmentSecurity::Error.new(attrs) end def insensitive_attributes @@ -54,6 +53,9 @@ module ActiveModel end class Error < StandardError + def initialize(attrs) + super("Can't mass-assign protected attributes: #{attrs.join(', ')}") + end end end end diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb new file mode 100644 index 0000000000..3af95b09b0 --- /dev/null +++ b/activemodel/lib/active_model/model.rb @@ -0,0 +1,76 @@ +module ActiveModel + + # == Active Model Basic Model + # + # Includes the required interface for an object to interact with <tt>ActionPack</tt>, + # using different <tt>ActiveModel</tt> modules. It includes model name introspections, + # conversions, translations and validations. Besides that, it allows you to + # initialize the object with a hash of attributes, pretty much like + # <tt>ActiveRecord</tt> does. + # + # A minimal implementation could be: + # + # class Person + # include ActiveModel::Model + # attr_accessor :name, :age + # end + # + # person = Person.new(:name => 'bob', :age => '18') + # person.name # => 'bob' + # person.age # => 18 + # + # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt> to + # return <tt>false</tt>, which is the most common case. You may want to override it + # in your class to simulate a different scenario: + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name + # + # def persisted? + # self.id == 1 + # end + # end + # + # person = Person.new(:id => 1, :name => 'bob') + # person.persisted? # => true + # + # Also, if for some reason you need to run code on <tt>initialize</tt>, make sure you + # call super if you want the attributes hash initialization to happen. + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name, :omg + # + # def initialize(attributes={}) + # super + # @omg ||= true + # end + # end + # + # person = Person.new(:id => 1, :name => 'bob') + # person.omg # => true + # + # For more detailed information on other functionalities available, please refer + # to the specific modules included in <tt>ActiveModel::Model</tt> (see below). + module Model + def self.included(base) + base.class_eval do + extend ActiveModel::Naming + extend ActiveModel::Translation + include ActiveModel::Validations + include ActiveModel::Conversion + end + end + + def initialize(params={}) + params.each do |attr, value| + self.public_send("#{attr}=", value) + end if params + end + + def persisted? + false + end + end +end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index f16459ede2..2b5fc57a3a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,30 +1,40 @@ require 'active_support/inflector' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' -require 'active_support/core_ext/module/deprecation' +require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/object/blank' module ActiveModel - class Name < String - attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key, :i18n_key + class Name + include Comparable + + attr_reader :singular, :plural, :element, :collection, + :singular_route_key, :route_key, :param_key, :i18n_key, + :name + alias_method :cache_key, :collection - deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead." + delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, + :to_str, :to => :name def initialize(klass, namespace = nil, name = nil) - name ||= klass.name - super(name) - @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace - - @klass = klass - @singular = _singularize(self).freeze - @plural = ActiveSupport::Inflector.pluralize(@singular).freeze - @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze - @human = ActiveSupport::Inflector.humanize(@element).freeze - @collection = ActiveSupport::Inflector.tableize(self).freeze - @partial_path = "#{@collection}/#{@element}".freeze - @param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze - @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze - @i18n_key = self.underscore.to_sym + @name = name || klass.name + + raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank? + + @unnamespaced = @name.sub(/^#{namespace.name}::/, '') if namespace + @klass = klass + @singular = _singularize(@name) + @plural = ActiveSupport::Inflector.pluralize(@singular) + @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name)) + @human = ActiveSupport::Inflector.humanize(@element) + @collection = ActiveSupport::Inflector.tableize(@name) + @param_key = (namespace ? _singularize(@unnamespaced) : @singular) + @i18n_key = @name.underscore.to_sym + + @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup) + @singular_route_key = ActiveSupport::Inflector.singularize(@route_key) + @route_key << "_index" if @plural == @singular end # Transform the model name into a more humane format, using I18n. By default, @@ -44,7 +54,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 @@ -68,8 +78,8 @@ module ActiveModel # BookCover.model_name # => "BookCover" # BookCover.model_name.human # => "Book cover" # - # BookCover.model_name.i18n_key # => "book_cover" - # BookModule::BookCover.model_name.i18n_key # => "book_module.book_cover" + # BookCover.model_name.i18n_key # => :book_cover + # BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover" # # Providing the functionality that ActiveModel::Naming provides in your object # is required to pass the Active Model Lint test. So either extending the provided @@ -79,7 +89,9 @@ module ActiveModel # used to retrieve all kinds of naming-related information. def model_name @_model_name ||= begin - namespace = self.parents.detect { |n| n.respond_to?(:_railtie) } + namespace = self.parents.detect do |n| + n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming? + end ActiveModel::Name.new(self, namespace) end end @@ -112,10 +124,25 @@ module ActiveModel # namespaced models regarding whether it's inside isolated engine. # # For isolated engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> post + # + # For shared engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> blog_post + def self.singular_route_key(record_or_class) + model_name_from_record_or_class(record_or_class).singular_route_key + end + + # Returns string to use while generating route names. It differs for + # namespaced models regarding whether it's inside isolated engine. + # + # For isolated engine: # ActiveModel::Naming.route_key(Blog::Post) #=> posts # # For shared engine: # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts + # + # The route key also considers if the noun is uncountable and, in + # such cases, automatically appends _index. def self.route_key(record_or_class) model_name_from_record_or_class(record_or_class).route_key end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index cd8eb357de..f5ea285ccb 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -1,10 +1,11 @@ require 'singleton' require 'active_model/observer_array' -require 'active_support/core_ext/array/wrap' 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/deprecation' +require 'active_support/core_ext/object/try' require 'active_support/descendants_tracker' module ActiveModel @@ -64,33 +65,40 @@ module ActiveModel # raises an +ArgumentError+ exception. def add_observer(observer) unless observer.respond_to? :update - raise ArgumentError, "observer needs to respond to `update'" + raise ArgumentError, "observer needs to respond to 'update'" end observer_instances << observer end # Notify list of observers of a change. - def notify_observers(*arg) - observer_instances.each { |observer| observer.update(*arg) } + def notify_observers(*args) + observer_instances.each { |observer| observer.update(*args) } end # Total number of observers. - def count_observers + def observers_count observer_instances.size end + 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.to_s.camelize.constantize.instance - elsif observer.respond_to?(:instance) + 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 an " + - "instance of the class itself) responding to the instance " + - "method. Example: Person.observers = :big_brother # calls " + + "#{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 @@ -102,17 +110,24 @@ module ActiveModel end end - private - # Fires notifications to model's observers - # - # def save - # notify_observers(:before_save) - # ... - # notify_observers(:after_save) - # end - def notify_observers(method) - self.class.notify_observers(method, self) - 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 +custom_notification+, passing as arguments + # the current object and :foo. + # + def notify_observers(method, *extra_args) + self.class.notify_observers(method, self, *extra_args) + end end # == Active Model Observers @@ -187,7 +202,7 @@ module ActiveModel def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } - redefine_method(:observed_classes) { models } + singleton_class.redefine_method(:observed_classes) { models } end # Returns an array of Classes to observe. @@ -200,21 +215,18 @@ module ActiveModel # end # end def observed_classes - Array.wrap(observed_class) + Array(observed_class) end # The class observed by default is inferred from the observer's class name: # assert_equal Person, PersonObserver.observed_class def observed_class - if observed_class_name = name[/(.*)Observer/, 1] - observed_class_name.constantize - else - nil - end + name[/(.*)Observer/, 1].try :constantize end end # Start observing the declared classes and their subclasses. + # Called automatically by the instance method. def initialize observed_classes.each { |klass| add_observer!(klass) } end @@ -225,10 +237,10 @@ module ActiveModel # Send observed_method(object) if the method exists and # the observer is enabled for the given object's class. - def update(observed_method, object, &block) #:nodoc: + def update(observed_method, object, *extra_args, &block) #:nodoc: return unless respond_to?(observed_method) return if disabled_for?(object) - send(observed_method, object, &block) + send(observed_method, object, *extra_args, &block) end # Special method sent by the observed class when it is inherited. @@ -243,6 +255,7 @@ module ActiveModel klass.add_observer(self) end + # Returns true if notifications are disabled for this object. def disabled_for?(object) klass = object.class return false unless klass.respond_to?(:observers) diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index db78864c67..8711b24124 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -29,7 +29,7 @@ module ActiveModel # user.save # => true # user.authenticate("notright") # => false # user.authenticate("mUc3m00RsqyRe") # => user - # User.find_by_name("david").try(:authenticate, "notright") # => nil + # User.find_by_name("david").try(:authenticate, "notright") # => false # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user def has_secure_password # Load bcrypt-ruby only when has_secure_password is used. @@ -55,17 +55,14 @@ module ActiveModel module InstanceMethodsOnActivation # Returns self if the password is correct, otherwise false. def authenticate(unencrypted_password) - if BCrypt::Password.new(password_digest) == unencrypted_password - self - else - false - end + BCrypt::Password.new(password_digest) == unencrypted_password && self end - # Encrypts the password into the password_digest attribute. + # Encrypts the password into the password_digest attribute, only if the + # new password is not blank. def password=(unencrypted_password) - @password = unencrypted_password unless unencrypted_password.blank? + @password = unencrypted_password self.password_digest = BCrypt::Password.create(unencrypted_password) end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index a4b58ab456..4403ef060b 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,7 +1,5 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/array/wrap' - module ActiveModel # == Active Model Serialization @@ -11,15 +9,13 @@ module ActiveModel # A minimal implementation could be: # # class Person - # # include ActiveModel::Serialization # # attr_accessor :name # # def attributes - # {'name' => name} + # {'name' => nil} # end - # # end # # Which would provide you with: @@ -29,27 +25,29 @@ module ActiveModel # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # - # You need to declare some sort of attributes hash which contains the attributes - # you want to serialize and their current value. + # You need to declare an attributes hash which contains the attributes + # you want to serialize. Attributes must be strings, not symbols. + # When called, serializable hash will use + # instance methods that match the name of the attributes hash's keys. + # In order to override this behavior, take a look at the private + # method +read_attribute_for_serialization+. # # Most of the time though, you will want to include the JSON or XML # serializations. Both of these modules automatically include the - # ActiveModel::Serialization module, so there is no need to explicitly + # <tt>ActiveModel::Serialization</tt> module, so there is no need to explicitly # include it. # - # So a minimal implementation including XML and JSON would be: + # A minimal implementation including XML and JSON would be: # # class Person - # # include ActiveModel::Serializers::JSON # include ActiveModel::Serializers::Xml # # attr_accessor :name # # def attributes - # {'name' => name} + # {'name' => nil} # end - # # end # # Which would provide you with: @@ -66,26 +64,30 @@ module ActiveModel # 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> and <tt>:methods</tt> . + # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and <tt>include</tt>. + # The following are all valid examples: + # + # person.serializable_hash(:only => 'name') + # person.serializable_hash(:include => :address) + # person.serializable_hash(:include => { :address => { :only => 'city' }}) module Serialization def serializable_hash(options = nil) options ||= {} attribute_names = attributes.keys.sort if only = options[:only] - attribute_names &= Array.wrap(only).map(&:to_s) + attribute_names &= Array(only).map(&:to_s) elsif except = options[:except] - attribute_names -= Array.wrap(except).map(&:to_s) + attribute_names -= Array(except).map(&:to_s) end hash = {} attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) } - method_names.each { |n| hash[n] = send(n) } + Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } serializable_add_includes(options) do |association, records, opts| - hash[association] = if records.is_a?(Enumerable) + hash[association.to_s] = if records.is_a?(Enumerable) records.map { |a| a.serializable_hash(opts) } else records.serializable_hash(opts) @@ -123,13 +125,13 @@ module ActiveModel # +records+ - the association record(s) to be serialized # +opts+ - options for the association records def serializable_add_includes(options = {}) #:nodoc: - return unless include = options[:include] + return unless includes = options[:include] - unless include.is_a?(Hash) - include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] + unless includes.is_a?(Hash) + includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] end - include.each do |association, opts| + includes.each do |association, opts| if records = send(association) yield association, records, opts end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index c845440120..63ab8e7edc 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,8 +10,9 @@ module ActiveModel included do extend ActiveModel::Naming + extend ActiveModel::Configuration - class_attribute :include_root_in_json + config_attribute :include_root_in_json self.include_root_in_json = true end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index d61d9d7119..5084298210 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/hash/conversions' @@ -56,7 +55,7 @@ module ActiveModel end def serializable_collection - methods = Array.wrap(options[:methods]).map(&:to_s) + methods = Array(options[:methods]).map(&:to_s) serializable_hash.map do |name, value| name = name.to_s if methods.include?(name) @@ -146,7 +145,7 @@ module ActiveModel def add_procs if procs = options.delete(:procs) - Array.wrap(procs).each do |proc| + Array(procs).each do |proc| if proc.arity == 1 proc.call(options) else diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb index 6328807ad7..5004855d56 100644 --- a/activemodel/lib/active_model/test_case.rb +++ b/activemodel/lib/active_model/test_case.rb @@ -1,16 +1,4 @@ module ActiveModel #:nodoc: class TestCase < ActiveSupport::TestCase #:nodoc: - def with_kcode(kcode) - if RUBY_VERSION < '1.9' - orig_kcode, $KCODE = $KCODE, kcode - begin - yield - ensure - $KCODE = orig_kcode - end - else - yield - end - end end end diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 6d64c81b5f..6f0ca92e2a 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/hash/reverse_merge' - module ActiveModel # == Active Model Translation @@ -43,15 +41,28 @@ module ActiveModel # # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) - defaults = lookup_ancestors.map do |klass| - :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}" + options = { :count => 1 }.merge!(options) + parts = attribute.to_s.split(".", 2) + attribute = parts.pop + namespace = parts.pop + attributes_scope = "#{self.i18n_scope}.attributes" + + if namespace + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" + end + defaults << :"#{attributes_scope}.#{namespace}.#{attribute}" + else + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}" + end end defaults << :"attributes.#{attribute}" defaults << options.delete(:default) if options[:default] - defaults << attribute.to_s.humanize + defaults << attribute.humanize - options.reverse_merge! :count => 1, :default => defaults + options[:default] = defaults I18n.translate(defaults.shift, options) end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 8ed392abca..3ed72bae3b 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/except' @@ -34,7 +33,7 @@ module ActiveModel # person.first_name = 'zoolander' # person.valid? # => false # person.invalid? # => true - # person.errors # => #<OrderedHash {:first_name=>["starts with z."]}> + # person.errors # => #<Hash {: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> object, so @@ -42,9 +41,9 @@ module ActiveModel # module Validations extend ActiveSupport::Concern - include ActiveSupport::Callbacks included do + extend ActiveModel::Callbacks extend ActiveModel::Translation extend HelperMethods @@ -53,7 +52,8 @@ module ActiveModel attr_accessor :validation_context define_callbacks :validate, :scope => :name - class_attribute :_validators + extend ActiveModel::Configuration + config_attribute :_validators self._validators = Hash.new { |h,k| h[k] = [] } end @@ -65,7 +65,7 @@ module ActiveModel # # attr_accessor :first_name, :last_name # - # validates_each :first_name, :last_name do |record, attr, value| + # validates_each :first_name, :last_name, :allow_blank => true do |record, attr, value| # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z # end # end @@ -128,11 +128,24 @@ module ActiveModel # end # end # + # Options: + # * <tt>:on</tt> - Specifies the context where this validation is active + # (e.g. <tt>:on => :create</tt> or <tt>:on => :custom_validation_context</tt>) + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. + # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, + # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false value. def validate(*args, &block) options = args.extract_options! if options.key?(:on) options = options.dup - options[:if] = Array.wrap(options[:if]) + options[:if] = Array(options[:if]) options[:if].unshift("validation_context == :#{options[:on]}") end args << options @@ -145,7 +158,7 @@ module ActiveModel _validators.values.flatten.uniq end - # List all validators that being used to validate a specific attribute. + # List all validators that are being used to validate a specific attribute. def validators_on(*attributes) attributes.map do |attribute| _validators[attribute.to_sym] diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 22a77320dc..dbafd0bd1a 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -8,7 +8,8 @@ module ActiveModel # Provides an interface for any class to have <tt>before_validation</tt> and # <tt>after_validation</tt> callbacks. # - # First, extend ActiveModel::Callbacks from the class you are creating: + # First, include ActiveModel::Validations::Callbacks from the class you are + # creating: # # class MyModel # include ActiveModel::Validations::Callbacks @@ -23,14 +24,14 @@ module ActiveModel included do include ActiveSupport::Callbacks - define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name] + define_callbacks :validation, :terminator => "result == false", :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name] end module ClassMethods def before_validation(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] - options[:if] = Array.wrap(options[:if]) + options[:if] = Array(options[:if]) options[:if].unshift("self.validation_context == :#{options[:on]}") end set_callback(:validation, :before, *args, &block) @@ -39,8 +40,7 @@ module ActiveModel def after_validation(*args, &block) options = args.extract_options! options[:prepend] = true - options[:if] = Array.wrap(options[:if]) - options[:if] << "!halted" + options[:if] = Array(options[:if]) options[:if].unshift("self.validation_context == :#{options[:on]}") if options[:on] set_callback(:validation, :after, *(args << options), &block) end diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb new file mode 100644 index 0000000000..b632a2bd6b --- /dev/null +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -0,0 +1,31 @@ +require 'active_support/core_ext/range.rb' + +module ActiveModel + module Validations + module Clusivity + ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << + "and must be supplied as the :in option of the configuration hash" + + def check_validity! + unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } + raise ArgumentError, ERROR_MESSAGE + end + end + + private + + def include?(record, value) + delimiter = options[:in] + exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter + exclusions.send(inclusion_method(exclusions), value) + 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. + def inclusion_method(enumerable) + enumerable.is_a?(Range) ? :cover? : :include? + end + end + end +end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 6573a7d264..69ab74734d 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -5,7 +5,8 @@ module ActiveModel class ConfirmationValidator < EachValidator def validate_each(record, attribute, value) if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) - record.errors.add(attribute, :confirmation, options) + human_attribute_name = record.class.human_attribute_name(attribute) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(:attribute => human_attribute_name)) end end @@ -37,7 +38,7 @@ module ActiveModel # attribute. # # NOTE: This check is performed only if +password_confirmation+ is not - # +nil+, and by default only on save. To require confirmation, make sure + # +nil+. To require confirmation, make sure # to add a presence check for the confirmation attribute: # # validates_presence_of :password_confirmation, :if => :password_changed? diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 644cc814a7..5fedb1978b 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,35 +1,17 @@ -require 'active_support/core_ext/range' +require "active_model/validations/clusivity" module ActiveModel # == Active Model Exclusion Validator module Validations class ExclusionValidator < EachValidator - ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << - "and must be supplied as the :in option of the configuration hash" - - def check_validity! - unless [:include?, :call].any? { |method| options[:in].respond_to?(method) } - raise ArgumentError, ERROR_MESSAGE - end - end + include Clusivity def validate_each(record, attribute, value) - delimiter = options[:in] - exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter - if exclusions.send(inclusion_method(exclusions), value) + if include?(record, value) record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value)) end end - - private - - # 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. - def inclusion_method(enumerable) - enumerable.is_a?(Range) ? :cover? : :include? - end end module HelperMethods @@ -59,7 +41,7 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. + # * <tt>:strict</tt> - Specifies whether validation should be strict. # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 147e2ecb69..15ae7b1959 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,35 +1,17 @@ -require 'active_support/core_ext/range' +require "active_model/validations/clusivity" module ActiveModel # == Active Model Inclusion Validator module Validations class InclusionValidator < EachValidator - ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << - "and must be supplied as the :in option of the configuration hash" - - def check_validity! - unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } - raise ArgumentError, ERROR_MESSAGE - end - end + include Clusivity def validate_each(record, attribute, value) - delimiter = options[:in] - exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter - unless exclusions.send(inclusion_method(exclusions), value) + unless include?(record, value) record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) end end - - private - - # 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. - def inclusion_method(enumerable) - enumerable.is_a?(Range) ? :cover? : :include? - end end module HelperMethods @@ -59,7 +41,7 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. + # * <tt>:strict</tt> - Specifies whether validation should be strict. # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index eb7aac709d..037f8c2db8 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -6,14 +6,12 @@ module ActiveModel MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze - DEFAULT_TOKENIZER = lambda { |value| value.split(//) } RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long] def initialize(options) if range = (options.delete(:in) || options.delete(:within)) raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range) - options[:minimum], options[:maximum] = range.begin, range.end - options[:maximum] -= 1 if range.exclude_end? + options[:minimum], options[:maximum] = range.min, range.max end super @@ -23,27 +21,24 @@ module ActiveModel keys = CHECKS.keys & options.keys if keys.empty? - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' + raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.' end keys.each do |key| value = options[key] - unless value.is_a?(Integer) && value >= 0 - raise ArgumentError, ":#{key} must be a nonnegative Integer" + unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY + raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity" end end end def validate_each(record, attribute, value) - value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String) + value = tokenize(value) + value_length = value.respond_to?(:length) ? value.length : value.to_s.length CHECKS.each do |key, validity_check| next unless check_value = options[key] - - value ||= [] if key == :maximum - - value_length = value.respond_to?(:length) ? value.length : value.to_s.length next if value_length.send(validity_check, check_value) errors_options = options.except(*RESERVED_OPTIONS) @@ -55,6 +50,14 @@ module ActiveModel record.errors.add(attribute, MESSAGES[key], errors_options) end end + + private + + def tokenize(value) + if options[:tokenizer] && value.kind_of?(String) + options[:tokenizer].call(value) + end || value + end end module HelperMethods @@ -96,7 +99,7 @@ module ActiveModel # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to # count words as in above example.) # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. - # * <tt>:strict</tt> - Specifies whether validation should be strict. + # * <tt>:strict</tt> - Specifies whether validation should be strict. # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 34d447a0fa..bb9f9679fc 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -5,7 +5,7 @@ module ActiveModel class NumericalityValidator < EachValidator CHECKS = { :greater_than => :>, :greater_than_or_equal_to => :>=, :equal_to => :==, :less_than => :<, :less_than_or_equal_to => :<=, - :odd => :odd?, :even => :even? }.freeze + :odd => :odd?, :even => :even?, :other_than => :!= }.freeze RESERVED_OPTIONS = CHECKS.keys + [:only_integer] @@ -99,6 +99,7 @@ module ActiveModel # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value. # * <tt>:less_than</tt> - Specifies the value must be less than the supplied value. # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value. + # * <tt>:other_than</tt> - Specifies the value must be other than the supplied value. # * <tt>:odd</tt> - Specifies the value must be an odd number. # * <tt>:even</tt> - Specifies the value must be an even number. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should @@ -107,7 +108,7 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. + # * <tt>:strict</tt> - Specifies whether validation should be strict. # See <tt>ActiveModel::Validation#validates!</tt> for more information # # The following checks can also be supplied with a proc or a symbol which corresponds to a method: diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 35af7152db..9a643a6f5c 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -25,14 +25,14 @@ module ActiveModel # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>. # # Configuration options: - # * <tt>message</tt> - A custom error message (default is: "can't be blank"). + # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. - # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. - # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 8e09f6ac35..d94c4e3f4f 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 @@ -59,7 +58,7 @@ module ActiveModel # # validates :name, :'film/title' => true # - # The validators hash can also handle regular expressions, ranges, + # The validators hash can also handle regular expressions, ranges, # arrays and strings in shortcut form, e.g. # # validates :email, :format => /@/ @@ -70,7 +69,7 @@ module ActiveModel # validator's initializer as +options[:in]+ while other types including # regular expressions and strings are passed as +options[:with]+ # - # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ and +:strict+ + # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ and +:strict+ # can be given to one specific validator, as a hash: # # validates :password, :presence => { :if => :password_required? }, :confirmation => true @@ -80,7 +79,7 @@ module ActiveModel # validates :password, :presence => true, :confirmation => true, :if => :password_required? # def validates(*attributes) - defaults = attributes.extract_options! + defaults = attributes.extract_options!.dup validations = defaults.slice!(*_validates_default_keys) raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? @@ -101,12 +100,12 @@ module ActiveModel end end - # This method is used to define validation that can not be corrected by end user - # and is considered exceptional. - # So each validator defined with bang or <tt>:strict</tt> option set to <tt>true</tt> - # will always raise <tt>ActiveModel::InternalValidationFailed</tt> instead of adding error - # when validation fails - # See <tt>validates</tt> for more information about validation itself. + # This method is used to define validations that cannot be corrected by end + # users and are considered exceptional. So each validator defined with bang + # or <tt>:strict</tt> option set to <tt>true</tt> will always raise + # <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error + # when validation fails. + # See <tt>validates</tt> for more information about the validation itself. def validates!(*attributes) options = attributes.extract_options! options[:strict] = true @@ -118,7 +117,7 @@ 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 - [ :if, :unless, :on, :allow_blank, :allow_nil , :strict] + [:if, :unless, :on, :allow_blank, :allow_nil , :strict] end def _parse_validates_options(options) #:nodoc: diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 93a340eb39..991c5f7b82 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -56,7 +56,7 @@ module ActiveModel # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. - # * <tt>unless</tt> - Specifies a method, proc or string to call to + # * <tt>:unless</tt> - Specifies a method, proc or string to call to # determine if the validation should not occur # (e.g. <tt>:unless => :skip_validation</tt>, or # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). @@ -126,12 +126,12 @@ module ActiveModel # end # # Standard configuration options (:on, :if and :unless), which are - # available on the class version of validates_with, should instead be - # placed on the <tt>validates</tt> method as these are applied and tested + # available on the class version of +validates_with+, should instead be + # placed on the +validates+ method as these are applied and tested # in the callback # # If you pass any additional configuration options, they will be passed - # to the class and available as <tt>options</tt>, please refer to the + # to the class and available as +options+, please refer to the # class version of this method for more information # def validates_with(*args, &block) diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 35ec98c822..2953126c3c 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/array/wrap' require "active_support/core_ext/module/anonymous" require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/inclusion' @@ -137,7 +136,7 @@ module ActiveModel #:nodoc: # +options+ reader, however the <tt>:attributes</tt> option will be removed # and instead be made available through the +attributes+ reader. def initialize(options) - @attributes = Array.wrap(options.delete(:attributes)) + @attributes = Array(options.delete(:attributes)) raise ":attributes cannot be blank" if @attributes.empty? super check_validity! diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index dbda55ca7c..e195c12a4d 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,7 +1,7 @@ module ActiveModel module VERSION #:nodoc: - MAJOR = 3 - MINOR = 2 + MAJOR = 4 + MINOR = 0 TINY = 0 PRE = "beta" |