diff options
Diffstat (limited to 'activemodel/lib/active_model')
29 files changed, 697 insertions, 542 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index a43436e008..be55581c66 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' module ActiveModel class MissingAttributeError < NoMethodError @@ -9,46 +9,46 @@ module ActiveModel # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and suffixes # to your methods as well as handling the creation of Active Record like class methods # such as +table_name+. - # + # # The requirements to implement ActiveModel::AttributeMethods are to: # # * <tt>include ActiveModel::AttributeMethods</tt> in your object - # * Call each Attribute Method module method you want to add, such as + # * Call each Attribute Method module method you want to add, such as # attribute_method_suffix or attribute_method_prefix # * Call <tt>define_attribute_methods</tt> after the other methods are # called. # * Define the various generic +_attribute+ methods that you have declared - # + # # A minimal implementation could be: - # + # # class Person # include ActiveModel::AttributeMethods - # + # # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' # attribute_method_suffix '_contrived?' # attribute_method_prefix 'clear_' # define_attribute_methods ['name'] - # + # # attr_accessor :name - # + # # private - # + # # def attribute_contrived?(attr) # true # end - # + # # def clear_attribute(attr) # send("#{attr}=", nil) # end - # + # # def reset_attribute_to_default!(attr) # send("#{attr}=", "Default Name") # end # end # - # Notice that whenever you include ActiveModel::AttributeMethods in your class, - # it requires you to implement a <tt>attributes</tt> methods which returns a hash - # with each attribute name in your model as hash key and the attribute value as + # Note that whenever you include ActiveModel::AttributeMethods in your class, + # it requires you to implement an <tt>attributes</tt> method which returns a hash + # with each attribute name in your model as hash key and the attribute value as # hash value. # # Hash keys must be strings. @@ -56,35 +56,40 @@ module ActiveModel module AttributeMethods extend ActiveSupport::Concern + included do + class_attribute :attribute_method_matchers, :instance_writer => false + self.attribute_method_matchers = [] + 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 + # 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 + # 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: - # + # # AttributePerson.primary_key # # => "sysid" # AttributePerson.inheritance_column = 'address' @@ -93,19 +98,22 @@ module ActiveModel 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} + if method_defined?('original_#{name}') + undef :'original_#{name}' end - alias_method :original_#{name}, :#{name} + alias_method :'original_#{name}', :'#{name}' eorb if block_given? sing.send :define_method, name, &block else - # use eval instead of a block to work around a memory leak in dev - # mode in fcgi - sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 - def #{name}; #{value.to_s.inspect}; end - eorb + if name =~ /^[a-zA-Z_]\w*[!?=]?$/ + sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 + def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end + eorb + else + value = value.to_s if value + sing.send(:define_method, name) { value } + end end end @@ -118,20 +126,20 @@ module ActiveModel # # #{prefix}attribute(#{attr}, *args, &block) # - # An instance method <tt>#{prefix}attribute</tt> must exist and accept + # An instance method <tt>#{prefix}attribute</tt> must exist and accept # at least the +attr+ argument. # # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_prefix 'clear_' # define_attribute_methods [:name] # # private - # + # # def clear_attribute(attr) # send("#{attr}=", nil) # end @@ -143,7 +151,7 @@ module ActiveModel # person.clear_name # person.name # => nil def attribute_method_prefix(*prefixes) - attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix }) + self.attribute_method_matchers += prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix } undefine_attribute_methods end @@ -162,14 +170,14 @@ module ActiveModel # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_suffix '_short?' # define_attribute_methods [:name] # # private - # + # # def attribute_short?(attr) # send(attr).length < 5 # end @@ -180,7 +188,7 @@ module ActiveModel # person.name # => "Bob" # person.name_short? # => true def attribute_method_suffix(*suffixes) - attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix }) + self.attribute_method_matchers += suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix } undefine_attribute_methods end @@ -200,14 +208,14 @@ module ActiveModel # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' # define_attribute_methods [:name] # # private - # + # # def reset_attribute_to_default!(attr) # ... # end @@ -218,29 +226,27 @@ module ActiveModel # person.reset_name_to_default! # person.name # => 'Gemma' def attribute_method_affix(*affixes) - attribute_method_matchers.concat(affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] }) + self.attribute_method_matchers += affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] } undefine_attribute_methods end def alias_attribute(new_name, old_name) attribute_method_matchers.each do |matcher| - module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{matcher.method_name(new_name)}(*args) - send(:#{matcher.method_name(old_name)}, *args) - end - STR + define_method(matcher.method_name(new_name)) do |*args| + send(matcher.method_name(old_name), *args) + end end end - # Declares a the attributes that should be prefixed and suffixed by + # Declares the attributes that should be prefixed and suffixed by # ActiveModel::AttributeMethods. - # + # # To use, pass in an array of 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. - # + # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name, :age, :address # attribute_method_prefix 'clear_' @@ -251,36 +257,36 @@ module ActiveModel # define_attribute_methods [:name, :age, :address] # # private - # + # # def clear_attribute(attr) # ... # end # end def define_attribute_methods(attr_names) - return if attribute_methods_generated? - attr_names.each do |attr_name| - attribute_method_matchers.each do |matcher| - unless instance_method_already_implemented?(matcher.method_name(attr_name)) - generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" + attr_names.each { |attr_name| define_attribute_method(attr_name) } + end - if respond_to?(generate_method) - send(generate_method, attr_name) - else - method_name = matcher.method_name(attr_name) + def define_attribute_method(attr_name) + attribute_method_matchers.each do |matcher| + unless instance_method_already_implemented?(matcher.method_name(attr_name)) + generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" + + if respond_to?(generate_method) + send(generate_method, attr_name) + else + method_name = matcher.method_name(attr_name) - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - if method_defined?(:#{method_name}) - undef :#{method_name} - end - def #{method_name}(*args) - send(:#{matcher.method_missing_target}, '#{attr_name}', *args) - end - STR - end + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + if method_defined?('#{method_name}') + undef :'#{method_name}' + end + define_method('#{method_name}') do |*args| + send('#{matcher.method_missing_target}', '#{attr_name}', *args) + end + STR end end end - @attribute_methods_generated = true end # Removes all the previously dynamically defined methods from the class @@ -288,7 +294,6 @@ module ActiveModel generated_attribute_methods.module_eval do instance_methods.each { |m| undef_method(m) } end - @attribute_methods_generated = nil end # Returns true if the attribute methods defined have been generated. @@ -300,11 +305,6 @@ module ActiveModel end end - # Returns true if the attribute methods defined have been generated. - def attribute_methods_generated? - @attribute_methods_generated ||= nil - end - protected def instance_method_already_implemented?(method_name) method_defined?(method_name) @@ -312,7 +312,7 @@ module ActiveModel private class AttributeMethodMatcher - attr_reader :prefix, :suffix + attr_reader :prefix, :suffix, :method_missing_target AttributeMethodMatch = Struct.new(:target, :attr_name) @@ -320,40 +320,34 @@ module ActiveModel options.symbolize_keys! @prefix, @suffix = options[:prefix] || '', options[: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 matchdata = @regex.match(method_name) - AttributeMethodMatch.new(method_missing_target, matchdata[2]) + if @regex =~ method_name + AttributeMethodMatch.new(method_missing_target, $2) else nil end end def method_name(attr_name) - "#{prefix}#{attr_name}#{suffix}" - end - - def method_missing_target - :"#{prefix}attribute#{suffix}" + @method_name % attr_name end end - - def attribute_method_matchers #:nodoc: - read_inheritable_attribute(:attribute_method_matchers) || write_inheritable_attribute(:attribute_method_matchers, []) - end end - # Allows access to the object attributes, which are held in the - # <tt>@attributes</tt> hash, as though they were first-class methods. So a - # Person class with a name attribute can use Person#name and Person#name= + # Allows access to the object attributes, which are held in the + # <tt>@attributes</tt> hash, as though they were first-class methods. So a + # Person class with a name attribute can use Person#name and Person#name= # and never directly use the attributes hash -- except for multiple assigns - # with ActiveRecord#attributes=. A Milestone class can also ask - # Milestone#completed? to test that the completed attribute is not +nil+ + # with ActiveRecord#attributes=. A Milestone class can also ask + # Milestone#completed? to test that the completed attribute is not +nil+ # or 0. # - # It's also possible to instantiate related objects, so a Client class - # belonging to the clients table with a +master_id+ foreign key can + # It's also possible to instantiate related objects, so a Client class + # belonging to the clients table with a +master_id+ foreign key can # instantiate master through Client#master. def method_missing(method_id, *args, &block) method_name = method_id.to_s @@ -390,7 +384,7 @@ module ActiveModel # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. def match_attribute_method?(method_name) - self.class.send(:attribute_method_matchers).each do |method| + self.class.attribute_method_matchers.each do |method| if (match = method.match(method_name)) && attribute_method?(match.attr_name) return match end @@ -401,7 +395,7 @@ module ActiveModel # prevent method_missing from calling private methods with #send def guard_private_attribute_method!(method_name, args) if self.class.private_method_defined?(method_name) - raise NoMethodError.new("Attempt to call private method", method_name, args) + raise NoMethodError.new("Attempt to call private method `#{method_name}'", method_name, args) end end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 8c10c54b54..2a1f51a9a7 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -3,49 +3,46 @@ require 'active_support/callbacks' module ActiveModel # == Active Model Callbacks - # + # # Provides an interface for any class to have Active Record like callbacks. - # + # # Like the Active Record methods, the callback chain is aborted as soon as # one of the methods in the chain returns false. # # First, extend ActiveModel::Callbacks from the class you are creating: - # + # # class MyModel # extend ActiveModel::Callbacks # end - # + # # Then define a list of methods that you want callbacks attached to: - # + # # define_model_callbacks :create, :update - # + # # This will provide all three standard callbacks (before, around and after) for - # both the :create and :update methods. To implement, you need to wrap the methods + # both the :create and :update methods. To implement, you need to wrap the methods # you want callbacks on in a block so that the callbacks get a chance to fire: - # + # # def create - # _run_create_callbacks do + # run_callbacks :create do # # Your create action methods here # end # end - # - # The _run_<method_name>_callbacks methods are dynamically created when you extend - # the <tt>ActiveModel::Callbacks</tt> module. - # + # # Then in your class, you can use the +before_create+, +after_create+ and +around_create+ # methods, just as you would in an Active Record module. - # + # # before_create :action_before_create - # + # # def action_before_create # # Your code here # end - # - # You can choose not to have all three callbacks by passing a hash to the + # + # 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 - # + # # Would only create the after_create and before_create callback methods in your # class. module Callbacks @@ -56,50 +53,53 @@ module ActiveModel end # define_model_callbacks accepts the same options define_callbacks does, in case - # you want to overwrite a default. Besides that, it also accepts an :only option, + # you want to overwrite a default. Besides that, it also accepts an :only option, # where you can choose if you want all types (before, around or after) or just some. # # define_model_callbacks :initializer, :only => :after - # + # # Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on # that method call. To get around this you can call the define_model_callbacks # method as many times as you need. - # + # # define_model_callbacks :create, :only => :after # define_model_callbacks :update, :only => :before # define_model_callbacks :destroy, :only => :around - # + # # Would create +after_create+, +before_update+ and +around_destroy+ methods only. - # + # # You can pass in a class to before_<type>, after_<type> and around_<type>, in which # case the callback will call that class's <action>_<type> method passing the object # that the callback is being called on. - # + # # class MyModel # extend ActiveModel::Callbacks # define_model_callbacks :create - # + # # before_create AnotherClass # end - # + # # class AnotherClass # def self.before_create( obj ) # # obj is the MyModel instance that the callback is being called on # end # end - # + # def define_model_callbacks(*callbacks) options = callbacks.extract_options! - options = { :terminator => "result == false", :scope => [:kind, :name] }.merge(options) + options = { + :terminator => "result == false", + :scope => [:kind, :name], + :only => [:before, :around, :after] + }.merge(options) - types = Array.wrap(options.delete(:only)) - types = [:before, :around, :after] if types.empty? + types = Array.wrap(options.delete(:only)) callbacks.each do |callback| define_callbacks(callback, options) types.each do |type| - send(:"_define_#{type}_model_callback", self, callback) + send("_define_#{type}_model_callback", self, callback) end end end diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index d2bd160dc7..e3992e842a 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -3,8 +3,6 @@ module ActiveModel # # Handles default conversions: to_model, to_key and to_param. # - # == Example - # # Let's take for example this non persisted object. # # class ContactMessage @@ -22,19 +20,22 @@ module ActiveModel # cm.to_param # => nil # module Conversion - # If your object is already designed to implement all of the Active Model - # you can use the default to_model implementation, which simply returns + # If your object is already designed to implement all of the Active Model + # you can use the default to_model implementation, which simply returns # self. - # - # If your model does not act like an Active Model object, then you should - # define <tt>:to_model</tt> yourself returning a proxy object that wraps + # + # If your model does not act like an Active Model object, then you should + # define <tt>:to_model</tt> yourself returning a proxy object that wraps # your object with Active Model compliant methods. def to_model self end - # Returns an Enumerable of all (primary) key attributes or nil if - # persisted? is false + # 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 :id. def to_key persisted? ? [id] : nil end @@ -42,7 +43,7 @@ module ActiveModel # Returns a string representing the object's key suitable for use in URLs, # or nil if persisted? is false def to_param - to_key ? to_key.join('-') : nil + persisted? ? to_key.join('-') : nil end end end diff --git a/activemodel/lib/active_model/deprecated_error_methods.rb b/activemodel/lib/active_model/deprecated_error_methods.rb deleted file mode 100644 index adc50773d9..0000000000 --- a/activemodel/lib/active_model/deprecated_error_methods.rb +++ /dev/null @@ -1,33 +0,0 @@ -module ActiveModel - module DeprecatedErrorMethods - def on(attribute) - message = "Errors#on have been deprecated, use Errors#[] instead.\n" - message << "Also note that the behaviour of Errors#[] has changed. Errors#[] now always returns an Array. An empty Array is " - message << "returned when there are no errors on the specified attribute." - ActiveSupport::Deprecation.warn(message) - - errors = self[attribute] - errors.size < 2 ? errors.first : errors - end - - def on_base - ActiveSupport::Deprecation.warn "Errors#on_base have been deprecated, use Errors#[:base] instead" - ActiveSupport::Deprecation.silence { on(:base) } - end - - def add_to_base(msg) - ActiveSupport::Deprecation.warn "Errors#add_to_base(msg) has been deprecated, use Errors#add(:base, msg) instead" - self[:base] << msg - end - - def invalid?(attribute) - ActiveSupport::Deprecation.warn "Errors#invalid?(attribute) has been deprecated, use Errors#[attribute].any? instead" - self[attribute].any? - end - - def each_full - ActiveSupport::Deprecation.warn "Errors#each_full has been deprecated, use Errors#to_a.each instead" - to_a.each { |error| yield error } - end - end -end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 2516377afd..a479795d51 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,53 +1,52 @@ require 'active_model/attribute_methods' -require 'active_support/concern' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' module ActiveModel # == Active Model Dirty # - # Provides a way to track changes in your object in the same way as + # Provides a way to track changes in your object in the same way as # Active Record does. - # + # # The requirements to implement ActiveModel::Dirty are to: # # * <tt>include ActiveModel::Dirty</tt> in your object - # * Call <tt>define_attribute_methods</tt> passing each method you want to + # * Call <tt>define_attribute_methods</tt> passing each method you want to # track - # * Call <tt>attr_name_will_change!</tt> before each change to the tracked + # * Call <tt>attr_name_will_change!</tt> before each change to the tracked # attribute - # - # If you wish to also track previous changes on save or update, you need to + # + # If you wish to also track previous changes on save or update, you need to # add - # + # # @previously_changed = changes - # + # # inside of your save or update method. - # + # # A minimal implementation could be: - # + # # class Person - # + # # include ActiveModel::Dirty - # + # # define_attribute_methods [:name] - # + # # def name # @name # end - # + # # def name=(val) # name_will_change! unless val == @name # @name = val # end - # + # # def save # @previously_changed = changes # @changed_attributes.clear # end - # + # # end - # + # # == Examples: # # A newly instantiated object is unchanged: @@ -79,7 +78,7 @@ module ActiveModel # person.changes # => { 'name' => ['Bill', 'Bob'] } # # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> - # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to + # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to # in-place attributes. # # person.name_will_change! @@ -115,7 +114,7 @@ module ActiveModel # person.name = 'bob' # person.changes # => { 'name' => ['bill', 'bob'] } def changes - changed.inject(HashWithIndifferentAccess.new){ |h, attr| h[attr] = attribute_change(attr); h } + HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] end # Map of attributes that were changed when the model was saved. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 272ddb1554..c2f0228785 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -12,77 +12,96 @@ module ActiveModel # # Provides a modified +OrderedHash+ that you can include in your object # for handling error messages and interacting with Action Pack helpers. - # + # # A minimal implementation could be: - # + # # class Person - # + # # # Required dependency for ActiveModel::Errors # extend ActiveModel::Naming - # + # # def initialize # @errors = ActiveModel::Errors.new(self) # end - # + # # attr_accessor :name # attr_reader :errors - # + # # def validate! # errors.add(:name, "can not be nil") if name == nil # end - # + # # # The following methods are needed to be minimally implemented # # def read_attribute_for_validation(attr) # send(attr) # end - # + # # def Person.human_attribute_name(attr, options = {}) # attr # end - # + # # def Person.lookup_ancestors # [self] # end - # + # # end - # + # # 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::Translations # you will not need to implement the last two. Likewise, using # ActiveModel::Validations will handle the validation related methods # for you. - # + # # The above allows you to do: - # + # # p = Person.new # p.validate! # => ["can not be nil"] # p.errors.full_messages # => ["name can not be nil"] # # etc.. - class Errors < ActiveSupport::OrderedHash - include DeprecatedErrorMethods + class Errors + include Enumerable CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank] + attr_reader :messages + # Pass in the instance of the object that is using the errors object. - # + # # class Person # def initialize # @errors = ActiveModel::Errors.new(self) # end # end def initialize(base) - @base = base - super() + @base = base + @messages = ActiveSupport::OrderedHash.new + end + + # Clear the messages + def clear + messages.clear + end + + # Do the error messages include an error with key +error+? + def include?(error) + messages.include? error + end + + # Get messages for +key+ + def get(key) + messages[key] end - alias_method :get, :[] - alias_method :set, :[]= + # Set messages for +key+ to +value+ + def set(key, value) + messages[key] = value + end - # When passed a symbol or a name of a method, returns an array of errors + # When passed a symbol or a name of a method, returns an array of errors # for the method. - # + # # p.errors[:name] # => ["can not be nil"] # p.errors['name'] # => ["can not be nil"] def [](attribute) @@ -90,7 +109,7 @@ module ActiveModel end # Adds to the supplied attribute the supplied error message. - # + # # p.errors[:name] = "must be set" # p.errors[:name] # => ['must be set'] def []=(attribute, error) @@ -100,25 +119,25 @@ module ActiveModel # Iterates through each error key, value pair in the error messages hash. # Yields the attribute and the error for that attribute. If the attribute # 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| # # Will yield :name and "can't be blank" # end - # + # # p.errors.add(:name, "must be specified") # p.errors.each do |attribute, errors_array| # # Will yield :name and "can't be blank" # # then yield :name and "must be specified" # end def each - each_key do |attribute| + messages.each_key do |attribute| self[attribute].each { |error| yield attribute, error } end end # Returns the number of error messages. - # + # # p.errors.add(:name, "can't be blank") # p.errors.size # => 1 # p.errors.add(:name, "must be specified") @@ -127,8 +146,18 @@ module ActiveModel values.flatten.size end + # Returns all message values + def values + messages.values + end + + # Returns all message keys + def keys + messages.keys + end + # Returns an array of error messages, with the attribute name included - # + # # p.errors.add(:name, "can't be blank") # p.errors.add(:name, "must be specified") # p.errors.to_a # => ["name can't be blank", "name must be specified"] @@ -149,9 +178,9 @@ module ActiveModel def empty? all? { |k, v| v && v.empty? } end - + alias_method :blank?, :empty? # Returns an xml formatted representation of the Errors hash. - # + # # p.errors.add(:name, "can't be blank") # p.errors.add(:name, "must be specified") # p.errors.to_xml @@ -167,14 +196,18 @@ module ActiveModel # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object. def as_json(options=nil) - self + to_hash + end + + def to_hash + messages.dup end # Adds +message+ to the error messages on +attribute+, which will be returned on a call to # <tt>on(attribute)</tt> for the same attribute. More than one error can be added to the same # +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. # 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 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 = {}) @@ -191,13 +224,6 @@ module ActiveModel # Will add an error message to each of the attributes in +attributes+ that is empty. def add_on_empty(attributes, options = {}) - if options && !options.is_a?(Hash) - options = { :message => options } - ActiveSupport::Deprecation.warn \ - "ActiveModel::Errors#add_on_empty(attributes, custom_message) has been deprecated.\n" + - "Instead of passing a custom_message pass an options Hash { :message => custom_message }." - end - [attributes].flatten.each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) is_empty = value.respond_to?(:empty?) ? value.empty? : false @@ -207,13 +233,6 @@ module ActiveModel # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). def add_on_blank(attributes, options = {}) - if options && !options.is_a?(Hash) - options = { :message => options } - ActiveSupport::Deprecation.warn \ - "ActiveModel::Errors#add_on_blank(attributes, custom_message) has been deprecated.\n" + - "Instead of passing a custom_message pass an options Hash { :message => custom_message }." - end - [attributes].flatten.each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) add(attribute, :blank, options) if value.blank? @@ -231,41 +250,35 @@ module ActiveModel # company.errors.full_messages # => # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] def full_messages - full_messages = [] - - each do |attribute, messages| - messages = Array.wrap(messages) - next if messages.empty? - + map { |attribute, message| if attribute == :base - messages.each {|m| full_messages << m } + message else attr_name = attribute.to_s.gsub('.', '_').humanize attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) - options = { :default => "%{attribute} %{message}", :attribute => attr_name } - messages.each do |m| - full_messages << I18n.t(:"errors.format", options.merge(:message => m)) - end + I18n.t(:"errors.format", { + :default => "%{attribute} %{message}", + :attribute => attr_name, + :message => message + }) end - end - - full_messages + } end - # Translates an error message in its default scope + # Translates an error message in its default scope # (<tt>activemodel.errors.messages</tt>). # - # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, - # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not - # there also, it returns the translation of the default message + # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, + # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not + # there also, it returns the translation of the default message # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model name, # translated attribute name and the value are available for interpolation. # # When using inheritance in your models, it will check all the inherited # models too, but only if the model itself hasn't been found. Say you have - # <tt>class Admin < User; end</tt> and you wanted the translation for - # the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+, + # <tt>class Admin < User; end</tt> and you wanted the translation for + # the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+, # it looks for these translations: # # <ol> @@ -281,13 +294,6 @@ module ActiveModel def generate_message(attribute, type = :invalid, options = {}) type = options.delete(:message) if options[:message].is_a?(Symbol) - if options[:default] - ActiveSupport::Deprecation.warn \ - "ActiveModel::Errors#generate_message(attributes, custom_message) has been deprecated.\n" + - "Use ActiveModel::Errors#generate_message(attributes, :message => 'your message') instead." - options[:message] = options.delete(:default) - end - defaults = @base.class.lookup_ancestors.map do |klass| [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{type}", :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{type}" ] diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index fe650053d9..b71ef4b22e 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -1,19 +1,19 @@ -# == 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. -# -# These tests do not attempt to determine the semantic correctness of the -# returned values. For instance, you could implement valid? to always -# return true, and the tests would pass. It is up to you to ensure that -# the values are semantically meaningful. -# -# Objects you pass in are expected to return a compliant object from a -# call to to_model. It is perfectly fine for to_model to return self. module ActiveModel module Lint + # == 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. + # + # These tests do not attempt to determine the semantic correctness of the + # returned values. For instance, you could implement valid? to always + # return true, and the tests would pass. It is up to you to ensure that + # the values are semantically meaningful. + # + # Objects you pass in are expected to return a compliant object from a + # call to to_model. It is perfectly fine for to_model to return self. module Tests # == Responds to <tt>to_key</tt> @@ -23,7 +23,7 @@ module ActiveModel def test_to_key assert model.respond_to?(:to_key), "The model should respond to to_key" def model.persisted?() false end - assert model.to_key.nil? + assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end # == Responds to <tt>to_param</tt> @@ -38,8 +38,9 @@ module ActiveModel # not persisted?, then to_param should always return nil. def test_to_param assert model.respond_to?(:to_param), "The model should respond to to_param" + def model.to_key() [1] end def model.persisted?() false end - assert model.to_param.nil? + assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" end # == Responds to <tt>valid?</tt> @@ -79,7 +80,7 @@ module ActiveModel end # == Errors Testing - # + # # Returns an object that has :[] and :full_messages defined on it. See below # for more details. # diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 66cd9fdde6..be48415739 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -20,32 +20,32 @@ module ActiveModel # For example, a logged in user may need to assign additional attributes depending # on their role: # - # class AccountsController < ApplicationController - # include ActiveModel::MassAssignmentSecurity + # class AccountsController < ApplicationController + # include ActiveModel::MassAssignmentSecurity # - # attr_accessible :first_name, :last_name + # attr_accessible :first_name, :last_name # - # def self.admin_accessible_attributes - # accessible_attributes + [ :plan_id ] - # end + # def self.admin_accessible_attributes + # accessible_attributes + [ :plan_id ] + # end # - # def update - # ... - # @account.update_attributes(account_params) - # ... - # end + # def update + # ... + # @account.update_attributes(account_params) + # ... + # end # - # protected + # protected # - # def account_params - # sanitize_for_mass_assignment(params[:account]) - # end + # def account_params + # sanitize_for_mass_assignment(params[:account]) + # end # - # def mass_assignment_authorizer - # admin ? admin_accessible_attributes : super - # end + # def mass_assignment_authorizer + # admin ? admin_accessible_attributes : super + # end # - # end + # end # module ClassMethods # Attributes named in this macro are protected from mass-assignment @@ -54,9 +54,7 @@ module ActiveModel # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect # sensitive attributes from being overwritten by malicious users - # tampering with URLs or forms. - # - # == Example + # tampering with URLs or forms. Example: # # class Customer # include ActiveModel::MassAssignmentSecurity diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index b74d669f0a..eb9b847509 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,19 +1,25 @@ require 'active_support/inflector' +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/module/introspection' module ActiveModel class Name < String - attr_reader :singular, :plural, :element, :collection, :partial_path + attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key alias_method :cache_key, :collection - def initialize(klass) + def initialize(klass, namespace = nil) super(klass.name) + @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace + @klass = klass - @singular = ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze + @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 end # Transform the model name into a more humane format, using I18n. By default, @@ -30,35 +36,43 @@ module ActiveModel klass.model_name.underscore.to_sym end - defaults << options.delete(:default) if options[:default] + defaults << options[:default] if options[:default] defaults << @human - options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults + options = {:scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults}.merge(options.except(:default)) I18n.translate(defaults.shift, options) end + + private + def _singularize(str) + ActiveSupport::Inflector.underscore(str).tr('/', '_') + end end # == Active Model Naming # # Creates a +model_name+ method on your object. - # + # # To implement, just extend ActiveModel::Naming in your object: - # + # # class BookCover # extend ActiveModel::Naming # end - # + # # BookCover.model_name # => "BookCover" # BookCover.model_name.human # => "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 - # method below, or rolling your own is required.. + # method below, or rolling your own is required. module Naming # Returns an ActiveModel::Name object for module. It can be # used to retrieve all kinds of naming-related information. def model_name - @_model_name ||= ActiveModel::Name.new(self) + @_model_name ||= begin + namespace = self.parents.detect { |n| n.respond_to?(:_railtie) } + ActiveModel::Name.new(self, namespace) + end end # Returns the plural class name of a record or class. Examples: @@ -85,10 +99,38 @@ module ActiveModel plural(record_or_class) == singular(record_or_class) 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 + def self.route_key(record_or_class) + model_name_from_record_or_class(record_or_class).route_key + end + + # Returns string to use for params names. It differs for + # namespaced models regarding whether it's inside isolated engine. + # + # For isolated engine: + # ActiveModel::Naming.param_key(Blog::Post) #=> post + # + # For shared engine: + # ActiveModel::Naming.param_key(Blog::Post) #=> blog_post + def self.param_key(record_or_class) + model_name_from_record_or_class(record_or_class).param_key + end + private def self.model_name_from_record_or_class(record_or_class) - (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name + (record_or_class.is_a?(Class) ? record_or_class : convert_to_model(record_or_class).class).model_name + end + + def self.convert_to_model(object) + object.respond_to?(:to_model) ? object.to_model : object end end - + end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index c6a79acf81..ef36f80bec 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -10,19 +10,23 @@ module ActiveModel module ClassMethods # == Active Model Observers Activation - # + # # Activates the observers assigned. Examples: # + # class ORM + # include ActiveModel::Observing + # end + # # # Calls PersonObserver.instance - # ActiveRecord::Base.observers = :person_observer + # ORM.observers = :person_observer # # # Calls Cacher.instance and GarbageCollector.instance - # ActiveRecord::Base.observers = :cacher, :garbage_collector + # ORM.observers = :cacher, :garbage_collector # # # Same as above, just using explicit class references - # ActiveRecord::Base.observers = Cacher, GarbageCollector + # ORM.observers = Cacher, GarbageCollector # - # Note: Setting this does not instantiate the observers yet. + # Note: Setting this does not instantiate the observers yet. # +instantiate_observers+ is called during startup, and before # each development request. def observers=(*values) @@ -34,36 +38,41 @@ module ActiveModel @observers ||= [] end + # Gets the current observer instances. + def observer_instances + @observer_instances ||= [] + end + # Instantiate the global Active Record observers. def instantiate_observers observers.each { |o| instantiate_observer(o) } end + # Add a new observer to the pool. def add_observer(observer) unless observer.respond_to? :update raise ArgumentError, "observer needs to respond to `update'" end - @observer_instances ||= [] - @observer_instances << observer + observer_instances << observer end + # Notify list of observers of a change. def notify_observers(*arg) - if defined? @observer_instances - for observer in @observer_instances - observer.update(*arg) - end + for observer in observer_instances + observer.update(*arg) end end + # Total number of observers. def count_observers - @observer_instances.size + observer_instances.size end protected def instantiate_observer(observer) #:nodoc: # string/symbol if observer.respond_to?(:to_sym) - observer = observer.to_s.camelize.constantize.instance + observer.to_s.camelize.constantize.instance elsif observer.respond_to?(:instance) observer.instance else @@ -93,7 +102,7 @@ module ActiveModel # == Active Model Observers # - # Observer classes respond to lifecycle callbacks to implement trigger-like + # Observer classes respond to life cycle callbacks to implement trigger-like # behavior outside the original class. This is a great way to reduce the # clutter that normally comes when the model class is burdened with # functionality that doesn't pertain to the core responsibility of the @@ -123,9 +132,9 @@ module ActiveModel # # Observers will by default be mapped to the class with which they share a # name. So CommentObserver will be tied to observing Comment, ProductManagerObserver - # to ProductManager, and so on. If you want to name your observer differently than - # the class you're interested in observing, you can use the Observer.observe class - # method which takes either the concrete class (Product) or a symbol for that + # to ProductManager, and so on. If you want to name your observer differently than + # the class you're interested in observing, you can use the Observer.observe class + # method which takes either the concrete class (Product) or a symbol for that # class (:product): # # class AuditObserver < ActiveModel::Observer @@ -136,7 +145,7 @@ module ActiveModel # end # end # - # If the audit observer needs to watch more than one kind of object, this can be + # If the audit observer needs to watch more than one kind of object, this can be # specified with multiple arguments: # # class AuditObserver < ActiveModel::Observer @@ -147,9 +156,13 @@ module ActiveModel # end # end # - # The AuditObserver will now act on both updates to Account and Balance by treating + # The AuditObserver will now act on both updates to Account and Balance by treating # them both as records. # + # If you're using an Observer in a Rails application with Active Record, be sure to + # read about the necessary configuration in the documentation for + # ActiveRecord::Observer. + # class Observer include Singleton diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb new file mode 100644 index 0000000000..957d0ddaaa --- /dev/null +++ b/activemodel/lib/active_model/secure_password.rb @@ -0,0 +1,66 @@ +require 'bcrypt' + +module ActiveModel + module SecurePassword + extend ActiveSupport::Concern + + module ClassMethods + # Adds methods to set and authenticate against a BCrypt password. + # This mechanism requires you to have a password_digest attribute. + # + # Validations for presence of password, confirmation of password (using + # a "password_confirmation" attribute) are automatically added. + # You can add more validations by hand if need be. + # + # Example using Active Record (which automatically includes ActiveModel::SecurePassword): + # + # # Schema: User(name:string, password_digest:string) + # class User < ActiveRecord::Base + # has_secure_password + # end + # + # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch") + # user.save # => false, password required + # user.password = "mUc3m00RsqyRe" + # user.save # => false, confirmation doesn't match + # user.password_confirmation = "mUc3m00RsqyRe" + # user.save # => true + # user.authenticate("notright") # => false + # user.authenticate("mUc3m00RsqyRe") # => user + # User.find_by_name("david").try(:authenticate, "notright") # => nil + # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user + def has_secure_password + attr_reader :password + attr_accessor :password_confirmation + + validates_confirmation_of :password + validates_presence_of :password_digest + + include InstanceMethodsOnActivation + + if respond_to?(:attributes_protected_by_default) + def self.attributes_protected_by_default + super + ['password_digest'] + end + end + end + end + + 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 + end + + # Encrypts the password into the password_digest attribute. + def password=(unencrypted_password) + @password = unencrypted_password + self.password_digest = BCrypt::Password.create(unencrypted_password) + end + end + end +end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index e675937f4d..caf44a2ee0 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -3,25 +3,25 @@ require 'active_support/core_ext/hash/slice' module ActiveModel # == Active Model Serialization - # + # # Provides a basic serialization to a serializable_hash for your object. - # + # # A minimal implementation could be: - # + # # class Person - # + # # include ActiveModel::Serialization - # + # # attr_accessor :name - # + # # def attributes - # @attributes ||= {'name' => 'nil'} + # {'name' => name} # end - # + # # end - # + # # Which would provide you with: - # + # # person = Person.new # person.serializable_hash # => {"name"=>nil} # person.name = "Bob" @@ -29,63 +29,58 @@ module ActiveModel # # You need to declare some sort of attributes hash which contains the attributes # you want to serialize and their current value. - # - # Most of the time though, you will want to include the JSON or XML - # serializations. Both of these modules automatically include the + # + # Most of the time though, 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 # include it. - # + # # So a minimal implementation including XML and JSON would be: - # + # # class Person - # + # # include ActiveModel::Serializers::JSON # include ActiveModel::Serializers::Xml - # + # # attr_accessor :name - # + # # def attributes - # @attributes ||= {'name' => 'nil'} + # {'name' => name} # end - # + # # end - # + # # Which would provide you with: - # + # # person = Person.new # 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> and <tt>:methods</tt> . + # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> . module Serialization def serializable_hash(options = nil) options ||= {} - options[:only] = Array.wrap(options[:only]).map { |n| n.to_s } - options[:except] = Array.wrap(options[:except]).map { |n| n.to_s } + only = Array.wrap(options[:only]).map(&:to_s) + except = Array.wrap(options[:except]).map(&:to_s) attribute_names = attributes.keys.sort - if options[:only].any? - attribute_names &= options[:only] - elsif options[:except].any? - attribute_names -= options[:except] - end - - method_names = Array.wrap(options[:methods]).inject([]) do |methods, name| - methods << name if respond_to?(name.to_s) - methods + if only.any? + attribute_names &= only + elsif except.any? + attribute_names -= except end - (attribute_names + method_names).inject({}) { |hash, name| - hash[name] = send(name) - hash - } + method_names = Array.wrap(options[:methods]).map { |n| n if respond_to?(n.to_s) }.compact + Hash[(attribute_names + method_names).map { |n| [n, send(n)] }] end end end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index e1dbc522de..0bfbf2aa06 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -18,17 +18,17 @@ module ActiveModel # Returns a JSON string representing the model. Some configuration can be # passed through +options+. # - # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the - # top-level behavior of +to_json+. If true (the default) +to_json+ will - # emit a single root node named after the object's type. For example: + # The option <tt>include_root_in_json</tt> controls the top-level behavior + # of +as_json+. If true (the default) +as_json+ will emit a single root + # node named after the object's type. For example: # # konata = User.find(1) - # konata.to_json + # konata.as_json # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} } # # ActiveRecord::Base.include_root_in_json = false - # konata.to_json + # konata.as_json # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # @@ -39,29 +39,29 @@ module ActiveModel # attributes. For example: # # konata = User.find(1) - # konata.to_json + # konata.as_json # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes # included, and work similar to the +attributes+ method. For example: # - # konata.to_json(:only => [ :id, :name ]) + # konata.as_json(:only => [ :id, :name ]) # # => {"id": 1, "name": "Konata Izumi"} # - # konata.to_json(:except => [ :id, :created_at, :age ]) + # konata.as_json(:except => [ :id, :created_at, :age ]) # # => {"name": "Konata Izumi", "awesome": true} # # To include the result of some method calls on the model use <tt>:methods</tt>: # - # konata.to_json(:methods => :permalink) + # konata.as_json(:methods => :permalink) # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true, # "permalink": "1-konata-izumi"} # # To include associations use <tt>:include</tt>: # - # konata.to_json(:include => :posts) + # konata.as_json(:include => :posts) # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true, # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, @@ -69,7 +69,7 @@ module ActiveModel # # Second level and higher order associations work as well: # - # konata.to_json(:include => { :posts => { + # konata.as_json(:include => { :posts => { # :include => { :comments => { # :only => :body } }, # :only => :title } }) @@ -79,18 +79,16 @@ module ActiveModel # "title": "Welcome to the weblog"}, # {"comments": [{"body": "Don't think too hard"}], # "title": "So I was thinking"}]} - def encode_json(encoder) - hash = serializable_hash(encoder.options) + + def as_json(options = nil) + hash = serializable_hash(options) + if include_root_in_json - custom_root = encoder.options && encoder.options[:root] + custom_root = options && options[:root] hash = { custom_root || self.class.model_name.element => hash } end - ActiveSupport::JSON.encode(hash) - end - - def as_json(options = nil) - self + hash end def from_json(json) diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index ed64434b8f..d4295e6afe 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -17,7 +17,8 @@ module ActiveModel def initialize(name, serializable, raw_value=nil) @name, @serializable = name, serializable - @value = value || @serializable.send(name) + raw_value = raw_value.in_time_zone if raw_value.respond_to?(:in_time_zone) + @value = raw_value || @serializable.send(name) @type = compute_type end @@ -52,7 +53,7 @@ module ActiveModel @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s } end - # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> + # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> # takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set # for a N level model but is set for the N+1 level models, # then because <tt>:except</tt> is set to a default value, the second @@ -76,10 +77,9 @@ module ActiveModel end def serializable_methods - Array.wrap(options[:methods]).inject([]) do |methods, name| - methods << self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) - methods - end + Array.wrap(options[:methods]).map do |name| + self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) + end.compact end def serialize @@ -134,6 +134,29 @@ module ActiveModel # 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. For example: + # + # konata = User.find(1) + # konata.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 activerecord/lib/active_record/serializers/xml_serializer.xml. def to_xml(options = {}, &block) Serializer.new(self, options).serialize(&block) end diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 0facbd6ce1..dbb76244e4 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -3,19 +3,19 @@ require 'active_support/core_ext/hash/reverse_merge' module ActiveModel # == Active Model Translation - # + # # Provides integration between your object and the Rails internationalization # (i18n) framework. - # + # # A minimal implementation could be: - # + # # class TranslatedPerson # extend ActiveModel::Translation # end - # + # # TranslatedPerson.human_attribute_name('my_attribute') # # => "My attribute" - # + # # This also provides the required class methods for hooking into the # Rails internationalization API, including being able to define a # class based i18n_scope and lookup_ancestors to find translations in @@ -28,9 +28,9 @@ module ActiveModel :activemodel end - # When localizing a string, it goes through the lookup returned by this + # When localizing a string, it goes through the lookup returned by this # method, which is used in ActiveModel::Name#human, - # ActiveModel::Errors#full_messages and + # ActiveModel::Errors#full_messages and # ActiveModel::Translation#human_attribute_name. def lookup_ancestors self.ancestors.select { |x| x.respond_to?(:model_name) } @@ -54,11 +54,5 @@ module ActiveModel options.reverse_merge! :count => 1, :default => defaults I18n.translate(defaults.shift, options) end - - # Model.human_name is deprecated. Use Model.model_name.human instead. - def human_name(*args) - ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,5]) - model_name.human(*args) - end end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 3407c59e7a..d968609e67 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -2,30 +2,31 @@ 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' require 'active_model/errors' require 'active_model/validations/callbacks' module ActiveModel # == Active Model Validations - # + # # Provides a full validation framework to your objects. - # + # # A minimal implementation could be: - # + # # class Person # include ActiveModel::Validations - # + # # attr_accessor :first_name, :last_name # # validates_each :first_name, :last_name do |record, attr, value| # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z # end # end - # + # # Which provides you with the full standard validation stack that you # know from Active Record: - # + # # person = Person.new # person.valid? # => true # person.invalid? # => false @@ -34,11 +35,11 @@ module ActiveModel # person.valid? # => false # person.invalid? # => true # person.errors # => #<OrderedHash {:first_name=>["starts with z."]}> - # - # Note that ActiveModel::Validations automatically adds an +errors+ method - # to your instances initialized with a new ActiveModel::Errors object, so + # + # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+ method + # to your instances initialized with a new <tt>ActiveModel::Errors</tt> object, so # there is no need for you to do this manually. - # + # module Validations extend ActiveSupport::Concern include ActiveSupport::Callbacks @@ -61,7 +62,7 @@ module ActiveModel # # class Person # include ActiveModel::Validations - # + # # attr_accessor :first_name, :last_name # # validates_each :first_name, :last_name do |record, attr, value| @@ -70,8 +71,8 @@ module ActiveModel # end # # Options: - # * <tt>:on</tt> - Specifies when this validation is active (default is - # <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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 @@ -95,15 +96,15 @@ module ActiveModel # # class Comment # include ActiveModel::Validations - # + # # validate :must_be_friends # # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) # end # end # - # Or with a block which is passed with the current record to be validated: + # With a block which is passed with the current record to be validated: # # class Comment # include ActiveModel::Validations @@ -113,7 +114,17 @@ module ActiveModel # end # # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # end + # end + # + # Or with a block where self points to the current record to be validated: + # + # class Comment + # include ActiveModel::Validations + # + # validate do + # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) # end # end # @@ -128,15 +139,17 @@ module ActiveModel set_callback(:validate, *args, &block) end - # List all validators that are being used to validate the model using + # List all validators that are being used to validate the model using # +validates_with+ method. def validators _validators.values.flatten.uniq end # List all validators that being used to validate a specific attribute. - def validators_on(attribute) - _validators[attribute.to_sym] + def validators_on(*attributes) + attributes.map do |attribute| + _validators[attribute.to_sym] + end.flatten end # Check if method is an attribute method or not. @@ -152,7 +165,7 @@ module ActiveModel end end - # Returns the Errors object that holds all information about attribute error messages. + # Returns the +Errors+ object that holds all information about attribute error messages. def errors @errors ||= Errors.new(self) end @@ -168,14 +181,14 @@ module ActiveModel self.validation_context = current_context end - # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, + # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, # false otherwise. def invalid?(context = nil) !valid?(context) end - # Hook method defining how an attribute value should be retrieved. By default - # this is assumed to be an instance named after the attribute. Override this + # Hook method defining how an attribute value should be retrieved. By default + # this is assumed to be an instance named after the attribute. Override this # method in subclasses should you need to retrieve the value for a given # attribute differently: # @@ -194,9 +207,9 @@ module ActiveModel alias :read_attribute_for_validation :send protected - + def run_validations! - _run_validate_callbacks + run_callbacks :validate errors.empty? end end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 99b8966def..4f390613aa 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -14,8 +14,6 @@ module ActiveModel end def setup(klass) - # Note: instance_methods.map(&:to_s) is important for 1.9 compatibility - # as instance_methods returns symbols unlike 1.8 which returns strings. attr_readers = attributes.reject { |name| klass.attribute_method?(name) } attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } klass.send(:attr_reader, *attr_readers) @@ -24,7 +22,7 @@ module ActiveModel end module HelperMethods - # Encapsulates the pattern of wanting to validate the acceptance of a + # Encapsulates the pattern of wanting to validate the acceptance of a # terms of service check box (or similar agreement). Example: # # class Person < ActiveRecord::Base @@ -32,33 +30,33 @@ module ActiveModel # validates_acceptance_of :eula, :message => "must be abided" # end # - # If the database column does not exist, the +terms_of_service+ attribute + # If the database column does not exist, the +terms_of_service+ attribute # is entirely virtual. This check is performed only if +terms_of_service+ # is not +nil+ and by default on save. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "must be + # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <tt>:on</tt> - Specifies when this validation is active (default is - # <tt>:save</tt>, other options are <tt>:create</tt> and - # <tt>:update</tt>). + # * <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>:allow_nil</tt> - Skip validation if attribute is +nil+ (default # is true). - # * <tt>:accept</tt> - Specifies value that is considered 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 + # an HTML checkbox. This should be set to +true+ if you are validating # a database column, since the attribute is typecast from "1" to +true+ # before validation. # * <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 + # 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 (for example, - # <tt>:unless => :skip_validation</tt>, or + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should not occur (for example, + # <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 + # The method, proc or string should return or evaluate to a true or # false value. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index afd65d3dd5..adc2867ad0 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -14,7 +14,7 @@ module ActiveModel # include ActiveModel::Validations::Callbacks # # before_validation :do_stuff_before_validation - # after_validation :do_tuff_after_validation + # after_validation :do_stuff_after_validation # end # # Like other before_* callbacks if <tt>before_validation</tt> returns false @@ -40,7 +40,7 @@ module ActiveModel options = args.extract_options! options[:prepend] = true options[:if] = Array.wrap(options[:if]) - options[:if] << "!halted && value != false" + options[:if] << "!halted" options[:if] << "self.validation_context == :#{options[:on]}" if options[:on] set_callback(:validation, :after, *(args << options), &block) end @@ -50,7 +50,7 @@ module ActiveModel # Overwrite run validations to include callbacks. def run_validations! - _run_validation_callbacks { super } + run_callbacks(:validation) { super } end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 3a80893866..e6d10cfff8 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -4,24 +4,26 @@ module ActiveModel module Validations class ConfirmationValidator < EachValidator def validate_each(record, attribute, value) - confirmed = record.send(:"#{attribute}_confirmation") - return if confirmed.nil? || value == confirmed - record.errors.add(attribute, :confirmation, options) + if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) + record.errors.add(attribute, :confirmation, options) + end end def setup(klass) - klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" }) + klass.send(:attr_accessor, *attributes.map do |attribute| + :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation") + end.compact) end end module HelperMethods - # Encapsulates the pattern of wanting to validate a password or email + # Encapsulates the pattern of wanting to validate a password or email # address field with a confirmation. For example: # # Model: # class Person < ActiveRecord::Base # validates_confirmation_of :user_name, :password - # validates_confirmation_of :email_address, + # validates_confirmation_of :email_address, # :message => "should match confirmation" # end # @@ -29,12 +31,12 @@ module ActiveModel # <%= password_field "person", "password" %> # <%= password_field "person", "password_confirmation" %> # - # The added +password_confirmation+ attribute is virtual; it exists only + # The added +password_confirmation+ attribute is virtual; it exists only # as an in-memory attribute for validating the password. To achieve this, - # the validation adds accessors to the model for the confirmation + # the validation adds accessors to the model for the confirmation # attribute. - # - # NOTE: This check is performed only if +password_confirmation+ is not + # + # NOTE: This check is performed only if +password_confirmation+ is not # +nil+, and by default only on save. To require confirmation, make sure # to add a presence check for the confirmation attribute: # @@ -43,16 +45,17 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "doesn't match # confirmation"). - # * <tt>:on</tt> - Specifies when this validation is active (default is - # <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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 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 + # 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</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 validates_confirmation_of(*attr_names) diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 4138892786..e38e565d09 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -29,6 +29,9 @@ module ActiveModel # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # * <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 # 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. diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 104f403492..541f53a834 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -51,7 +51,9 @@ module ActiveModel # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation. # * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation. - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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 # 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. diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 049b093618..92ac940f36 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/range.rb' + module ActiveModel # == Active Model Inclusion Validator @@ -9,9 +11,14 @@ module ActiveModel end def validate_each(record, attribute, value) - unless options[:in].include?(value) - record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) - end + record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) unless options[:in].send(include?, 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 include? + options[:in].is_a?(Range) ? :cover? : :include? end end @@ -26,9 +33,14 @@ module ActiveModel # # Configuration options: # * <tt>:in</tt> - An enumerable object of available items. + # If the enumerable is a range the test is performed with <tt>Range#cover?</tt> + # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # * <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 # 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. diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index a7af4f2b4d..72735cfb89 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -34,20 +34,17 @@ module ActiveModel end end end - + def validate_each(record, attribute, value) value = options[:tokenizer].call(value) if value.kind_of?(String) CHECKS.each do |key, validity_check| next unless check_value = options[key] - valid_value = if key == :maximum - value.nil? || value.size.send(validity_check, check_value) - else - value && value.size.send(validity_check, check_value) - end + value ||= [] if key == :maximum - next if valid_value + 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) errors_options[:count] = check_value @@ -87,7 +84,9 @@ module ActiveModel # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min 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>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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 # 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. diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index b6aff7aa6b..ae576462e6 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -24,7 +24,7 @@ module ActiveModel def validate_each(record, attr_name, value) before_type_cast = "#{attr_name}_before_type_cast" - raw_value = record.send("#{attr_name}_before_type_cast") if record.respond_to?(before_type_cast.to_sym) + raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast.to_sym) raw_value ||= value return if options[:allow_nil] && raw_value.nil? @@ -93,7 +93,9 @@ module ActiveModel # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is not a number"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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>:only_integer</tt> - Specifies whether the value has to be an integer, e.g. an integral value (default is +false+). # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is +false+). Notice that for fixnum and float columns empty strings are converted to +nil+. # * <tt>:greater_than</tt> - Specifies the value must be greater than the supplied value. diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 28c4640b17..cfb4c33dcc 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -26,8 +26,9 @@ module ActiveModel # # Configuration options: # * <tt>message</tt> - A custom error message (default is: "can't be blank"). - # * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, - # <tt>:update</tt>). + # * <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 # 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. diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 3260e6bc5a..7ff42de00b 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -9,7 +9,7 @@ module ActiveModel # validator classes ending in 'Validator'. Note that Rails default # validators can be overridden inside specific classes by creating # custom validator classes in their place such as PresenceValidator. - # + # # Examples of using the default rails validators: # # validates :terms, :acceptance => true @@ -21,25 +21,25 @@ module ActiveModel # validates :age, :numericality => true # validates :username, :presence => true # validates :username, :uniqueness => true - # + # # The power of the +validates+ method comes when using custom validators # and default validators in one call for a given attribute e.g. # # class EmailValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) # record.errors[attribute] << (options[:message] || "is not an email") unless - # value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i + # value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i # end # end - # + # # class Person # include ActiveModel::Validations # attr_accessor :name, :email - # + # # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } # validates :email, :presence => true, :email => true # end - # + # # Validator classes may also exist within the class being validated # allowing custom modules of validators to be included as needed e.g. # @@ -48,21 +48,30 @@ module ActiveModel # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) - # record.errors[attribute] << "must start with 'the'" unless =~ /^the/i + # record.errors[attribute] << "must start with 'the'" unless value =~ /\Athe/i # end # end # # validates :name, :title => true # end # - # The validators hash can also handle regular expressions, ranges and arrays: + # Additionally validator classes may be in another namespace and still used within any class. + # + # validates :name, :'file/title' => true + # + # The validators hash can also handle regular expressions, ranges, + # arrays and strings in shortcut form, e.g. # # validates :email, :format => /@/ # validates :gender, :inclusion => %w(male female) # validates :password, :length => 6..20 # - # Finally, the options :if, :unless, :on, :allow_blank and :allow_nil can be given - # to one specific validator: + # When using shortcut form, ranges and arrays are passed to your + # 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+ and +:allow_nil+ can be given + # to one specific validator, as a hash: # # validates :password, :presence => { :if => :password_required? }, :confirmation => true # @@ -72,17 +81,18 @@ module ActiveModel # def validates(*attributes) defaults = attributes.extract_options! - validations = defaults.slice!(:if, :unless, :on, :allow_blank, :allow_nil) + validations = defaults.slice!(*_validates_default_keys) raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? - raise ArgumentError, "Attribute names must be symbols" if attributes.any?{ |attribute| !attribute.is_a?(Symbol) } raise ArgumentError, "You need to supply at least one validation" if validations.empty? defaults.merge!(:attributes => attributes) validations.each do |key, options| + key = "#{key.to_s.camelize}Validator" + begin - validator = const_get("#{key.to_s.camelize}Validator") + validator = key.include?('::') ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end @@ -93,18 +103,24 @@ module ActiveModel protected + # 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 ] + end + def _parse_validates_options(options) #:nodoc: case options when TrueClass {} when Hash options - when Regexp - { :with => options } when Range, Array { :in => options } + else + { :with => options } end end end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 200efd4eb5..65ae18a769 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -8,6 +8,18 @@ module ActiveModel end end + class WithValidator < EachValidator + def validate_each(record, attr, val) + method_name = options[:with] + + if record.method(method_name).arity == 0 + record.send method_name + else + record.send method_name, attr + end + end + end + module ClassMethods # Passes the record off to the class or classes specified and allows them # to add errors based on more complex conditions. @@ -38,9 +50,9 @@ module ActiveModel # end # # Configuration options: - # * <tt>on</tt> - Specifies when this validation is active + # * <tt>:on</tt> - Specifies when this validation is active # (<tt>:create</tt> or <tt>:update</tt> - # * <tt>if</tt> - Specifies a method, proc or string to call to determine + # * <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. diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 163124d531..c5ed8d22d3 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -5,9 +5,9 @@ require 'active_support/core_ext/object/blank' module ActiveModel #:nodoc: # == Active Model Validator - # - # A simple base class that can be used along with - # +ActiveModel::Validations::ClassMethods.validates_with+ + # + # A simple base class that can be used along with + # ActiveModel::Validations::ClassMethods.validates_with # # class Person # include ActiveModel::Validations @@ -42,7 +42,7 @@ module ActiveModel #:nodoc: # end # end # - # To cause a validation error, you must add to the <tt>record<tt>'s errors directly + # To cause a validation error, you must add to the <tt>record</tt>'s errors directly # from within the validators message # # class MyValidator < ActiveModel::Validator @@ -61,31 +61,31 @@ module ActiveModel #:nodoc: # @my_custom_field = options[:field_name] || :first_name # end # end - # + # # The easiest way to add custom validators for validating individual attributes - # is with the convenient ActiveModel::EachValidator for example: - # + # is with the convenient <tt>ActiveModel::EachValidator</tt>. For example: + # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value) # end # end - # + # # This can now be used in combination with the +validates+ method - # (see ActiveModel::Validations::ClassMethods.validates for more on this) - # + # (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this) + # # class Person # include ActiveModel::Validations # attr_accessor :title - # - # validates :title, :presence => true, :title => true + # + # validates :title, :presence => true # end - # + # # Validator may also define a +setup+ instance method which will get called - # with the class that using that validator as it's argument. This can be - # useful when there are prerequisites such as an attr_accessor being present + # with the class that using that validator as its argument. This can be + # useful when there are prerequisites such as an +attr_accessor+ being present # for example: - # + # # class MyValidator < ActiveModel::Validator # def setup(klass) # klass.send :attr_accessor, :custom_attribute @@ -94,13 +94,11 @@ module ActiveModel #:nodoc: # # This setup method is only called when used with validation macros or the # class level <tt>validates_with</tt> method. - # + # class Validator attr_reader :options - # Returns the kind of the validator. - # - # == Examples + # Returns the kind of the validator. Examples: # # PresenceValidator.kind # => :presence # UniquenessValidator.kind # => :uniqueness @@ -122,18 +120,18 @@ module ActiveModel #:nodoc: # Override this method in subclasses with validation logic, adding errors # to the records +errors+ array where necessary. def validate(record) - raise NotImplementedError + raise NotImplementedError, "Subclasses must implement a validate(record) method." end end - # EachValidator is a validator which iterates through the attributes given - # in the options hash invoking the validate_each method passing in the + # +EachValidator+ is a validator which iterates through the attributes given + # in the options hash invoking the <tt>validate_each</tt> method passing in the # record, attribute and value. # - # All Active Model validations are built on top of this Validator. + # All Active Model validations are built on top of this validator. class EachValidator < Validator attr_reader :attributes - + # Returns a new validator instance. All options will be available via the # +options+ reader, however the <tt>:attributes</tt> option will be removed # and instead be made available through the +attributes+ reader. @@ -158,19 +156,18 @@ module ActiveModel #:nodoc: # Override this method in subclasses with the validation logic, adding # errors to the records +errors+ array where necessary. def validate_each(record, attribute, value) - raise NotImplementedError + raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method" end # Hook method that gets called by the initializer allowing verification # that the arguments supplied are valid. You could for example raise an - # ArgumentError when invalid options are supplied. + # +ArgumentError+ when invalid options are supplied. def check_validity! end end - # BlockValidator is a special EachValidator which receives a block on initialization - # and call this block for each attribute being validated. +validates_each+ uses this - # Validator. + # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization + # and call this block for each attribute being validated. +validates_each+ uses this validator. class BlockValidator < EachValidator def initialize(options, &block) @block = block diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index f2f4b15520..23ba42bf35 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,10 +1,10 @@ module ActiveModel module VERSION #:nodoc: MAJOR = 3 - MINOR = 0 + MINOR = 1 TINY = 0 - BUILD = "rc" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end |