diff options
Diffstat (limited to 'activemodel')
78 files changed, 2464 insertions, 2067 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index e4779c90d8..2c966943ee 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,120 +1,81 @@ -* `AM::Errors#to_json`: support `:full_messages` parameter *Bogdan Gusiev* - -* Trim down Active Model API by removing `valid?` and `errors.full_messages` *José Valim* - -## Rails 3.2.0 (January 20, 2012) ## - -* Deprecated `define_attr_method` in `ActiveModel::AttributeMethods`, because this only existed to - support methods like `set_table_name` in Active Record, which are themselves being deprecated. - - *Jon Leighton* - -* Add ActiveModel::Errors#added? to check if a specific error has been added *Martin Svalin* - -* Add ability to define strict validation(with :strict => true option) that always raises exception when fails *Bogdan Gusiev* - -* Deprecate "Model.model_name.partial_path" in favor of "model.to_partial_path" *Grant Hutchins, Peter Jaros* - -* Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior *Bogdan Gusiev* - -## Rails 3.1.0 (August 30, 2011) ## - -* Alternate I18n namespace lookup is no longer supported. - Instead of "activerecord.models.admins.post", do "activerecord.models.admins/post" instead *José Valim* - -* attr_accessible and friends now accepts :as as option to specify a role *Josh Kalderimis* - -* Add support for proc or lambda as an option for InclusionValidator, - ExclusionValidator, and FormatValidator *Prem Sichanugrist* - - You can now supply Proc, lambda, or anything that respond to #call in those - validations, and it will be called with current record as an argument. - That given proc or lambda must returns an object which respond to #include? for - InclusionValidator and ExclusionValidator, and returns a regular expression - object for FormatValidator. - -* Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting *DHH* +## Rails 4.0.0 (unreleased) ## -* ActiveModel::AttributeMethods allows attributes to be defined on demand *Alexander Uvarov* +* Add `ActiveModel::ForbiddenAttributesProtection`, a simple module to + protect attributes from mass assignment when non-permitted attributes are passed. -* Add support for selectively enabling/disabling observers *Myron Marston* + *DHH + Guillermo Iguaran* +* `ActiveModel::MassAssignmentSecurity` has been extracted from Active Model and the + `protected_attributes` gem should be added to Gemfile in order to use + `attr_accessible` and `attr_protected` macros in your models. -## Rails 3.0.7 (April 18, 2011) ## + *Guillermo Iguaran* -* No changes. +* Due to a change in builder, nil values and empty strings now generates + closed tags, so instead of this: + <pseudonyms nil=\"true\"></pseudonyms> -* Rails 3.0.6 (April 5, 2011) + It generates this: -* Fix when database column name has some symbolic characters (e.g. Oracle CASE# VARCHAR2(20)) #5818 #6850 *Robert Pankowecki, Santiago Pastorino* + <pseudonyms nil=\"true\"/> -* Fix length validation for fixnums #6556 *Andriy Tyurnikov* + *Carlos Antonio da Silva* -* Fix i18n key collision with namespaced models #6448 *yves.senn* +* Changed inclusion and exclusion validators to accept a symbol for `:in` option. + This allows to use dynamic inclusion/exclusion values using methods, besides the current lambda/proc support. -## Rails 3.0.5 (February 26, 2011) ## + *Gabriel Sobrinho* -* No changes. +* `AM::Validation#validates` ability to pass custom exception to `:strict` option. + *Bogdan Gusiev* -## Rails 3.0.4 (February 8, 2011) ## +* Changed `ActiveModel::Serializers::Xml::Serializer#add_associations` to by default + propagate `:skip_types, :dasherize, :camelize` keys to included associations. + It can be overriden on each association by explicitly specifying the option on one + or more associations -* No changes. + *Anthony Alberto* +* Changed `AM::Serializers::JSON.include_root_in_json' default value to false. + Now, AM Serializers and AR objects have the same default behaviour. Fixes #6578. -## Rails 3.0.3 (November 16, 2010) ## + class User < ActiveRecord::Base; end -* No changes. + class Person + include ActiveModel::Model + include ActiveModel::AttributeMethods + include ActiveModel::Serializers::JSON + attr_accessor :name, :age -## Rails 3.0.2 (November 15, 2010) ## - -* No changes - - -## Rails 3.0.1 (October 15, 2010) ## - -* No Changes, just a version bump. - - -## Rails 3.0.0 (August 29, 2010) ## - -* Added ActiveModel::MassAssignmentSecurity *Eric Chapweske, Josh Kalderimis* - -* JSON supports a custom root option: to_json(:root => 'custom') #4515 *Jatinder Singh* - -* #new_record? and #destroyed? were removed from ActiveModel::Lint. Use - persisted? instead. A model is persisted if it's not a new_record? and it was - not destroyed? *MG* - -* Added validations reflection in ActiveModel::Validations *JV* + def attributes + instance_values + end + end - Model.validators - Model.validators_on(:field) + user.as_json + => {"id"=>1, "name"=>"Konata Izumi", "age"=>16, "awesome"=>true} + # root is not included -* #to_key was added to ActiveModel::Lint so we can generate DOM IDs for - AMo objects with composite keys *MG* + person.as_json + => {"name"=>"Francesco", "age"=>22} + # root is not included -* ActiveModel::Observer#add_observer! + *Francesco Rodriguez* - It has a custom hook to define after_find that should really be in a - ActiveRecord::Observer subclass: +* Passing false hash values to `validates` will no longer enable the corresponding validators *Steve Purcell* - def add_observer!(klass) - klass.add_observer(self) - klass.class_eval 'def after_find() end' unless klass.respond_to?(:after_find) - end +* `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute` *Brian Cardarella* -* Change the ActiveModel::Base.include_root_in_json default to true for Rails 3 *DHH* +* Added ActiveModel::Model, a mixin to make Ruby objects work with AP out of box *Guillermo Iguaran* -* Add validates_format_of :without => /regexp/ option. #430 *Elliot Winkler, Peer Allan* - - Example : +* `AM::Errors#to_json`: support `:full_messages` parameter *Bogdan Gusiev* - validates_format_of :subdomain, :without => /www|admin|mail/ +* Trim down Active Model API by removing `valid?` and `errors.full_messages` *José Valim* -* Introduce validates_with to encapsulate attribute validations in a class. #2630 *Jeff Dean* +* When `^` or `$` are used in the regular expression provided to `validates_format_of` and the :multiline option is not set to true, an exception will be raised. This is to prevent security vulnerabilities when using `validates_format_of`. The problem is described in detail in the Rails security guide. -* Extracted from Active Record and Active Resource. +Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activemodel/CHANGELOG.md) for previous changes. diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index a7ba27ba73..b4565b5881 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -13,6 +13,25 @@ in code duplication and fragile applications that broke on upgrades. Active Model solves this by defining an explicit API. You can read more about the API in ActiveModel::Lint::Tests. +Active Model provides a default module that implements the basic API required +to integrate with Action Pack out of the box: <tt>ActiveModel::Model</tt>. + + class Person + include ActiveModel::Model + + attr_accessor :name, :age + validates_presence_of :name + end + + person = Person.new(:name => 'bob', :age => '18') + person.name # => 'bob' + person.age # => '18' + person.valid? # => true + +It includes model name introspections, conversions, translations and +validations, resulting in a class suitable to be used with Action Pack. +See <tt>ActiveModel::Model</tt> for more examples. + Active Model also provides the following functionality to have ORM-like behavior out of the box: @@ -22,7 +41,7 @@ behavior out of the box: include ActiveModel::AttributeMethods attribute_method_prefix 'clear_' - define_attribute_methods [:name, :age] + define_attribute_methods :name, :age attr_accessor :name, :age @@ -52,7 +71,7 @@ behavior out of the box: This generates +before_create+, +around_create+ and +after_create+ class methods that wrap your create method. - {Learn more}[link:classes/ActiveModel/CallBacks.html] + {Learn more}[link:classes/ActiveModel/Callbacks.html] * Tracking value changes @@ -97,9 +116,6 @@ behavior out of the box: person.errors.full_messages # => ["Name can not be nil"] - person.errors.full_messages - # => ["Name can not be nil"] - {Learn more}[link:classes/ActiveModel/Errors.html] * Model name introspection @@ -119,6 +135,16 @@ behavior out of the box: pattern in a Rails App and take advantage of all the standard observer functions. + class PersonObserver < ActiveModel::Observer + def after_create(person) + person.logger.info("New person added!") + end + + def after_destroy(person) + person.logger.warn("Person with an id of #{person.id} was destroyed!") + end + end + {Learn more}[link:classes/ActiveModel/Observer.html] * Making objects serializable diff --git a/activemodel/Rakefile b/activemodel/Rakefile index fc5aaf9f8f..fc5aaf9f8f 100755..100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 60c1d16934..be5d5d3ca8 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -5,9 +5,10 @@ Gem::Specification.new do |s| s.name = 'activemodel' s.version = version s.summary = 'A toolkit for building modeling frameworks (part of Rails).' - s.description = 'A toolkit for building modeling frameworks like Active Record and Active Resource. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.' + s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' @@ -17,5 +18,5 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.add_dependency('activesupport', version) - s.add_dependency('builder', '~> 3.0.0') + s.add_dependency('builder', '~> 3.1.0') end diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 85514e63fd..f757ba9843 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -21,9 +21,8 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) -$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require 'active_support' +require 'active_support/rails' require 'active_model/version' module ActiveModel @@ -32,13 +31,13 @@ module ActiveModel autoload :AttributeMethods autoload :BlockValidator, 'active_model/validator' autoload :Callbacks - autoload :Configuration autoload :Conversion autoload :Dirty autoload :EachValidator, 'active_model/validator' - autoload :Errors + autoload :ForbiddenAttributesProtection autoload :Lint - autoload :MassAssignmentSecurity + autoload :Model + autoload :DeprecatedMassAssignmentSecurity autoload :Name, 'active_model/naming' autoload :Naming autoload :Observer, 'active_model/observing' @@ -50,13 +49,25 @@ module ActiveModel autoload :Validations autoload :Validator + eager_autoload do + autoload :Errors + end + module Serializers extend ActiveSupport::Autoload - autoload :JSON - autoload :Xml + eager_autoload do + autoload :JSON + autoload :Xml + end + end + + def eager_load! + super + ActiveModel::Serializer.eager_load! end end -require 'active_support/i18n' -I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' +end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 97a83e58af..ef04f1fa49 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,34 +1,39 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/class/attribute' -require 'active_support/deprecation' module ActiveModel + # Raised when an attribute is not defined. + # + # class User < ActiveRecord::Base + # has_many :pets + # end + # + # user = User.first + # user.pets.select(:id).first.user_id + # # => ActiveModel::MissingAttributeError: missing attribute: user_id class MissingAttributeError < NoMethodError end # == Active Model Attribute Methods # - # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and suffixes - # to your methods as well as handling the creation of Active Record like class methods - # such as +table_name+. + # <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 + # * <tt>include ActiveModel::AttributeMethods</tt> in your object. # * Call each Attribute Method module method you want to add, such as - # attribute_method_suffix or attribute_method_prefix - # * Call <tt>define_attribute_methods</tt> after the other methods are - # called. - # * Define the various generic +_attribute+ methods that you have declared + # +attribute_method_suffix+ or +attribute_method_prefix+. + # * Call +define_attribute_methods+ after the other methods are called. + # * Define the various generic +_attribute+ methods that you have declared. # # A minimal implementation could be: # # class Person # include ActiveModel::AttributeMethods # - # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' + # attribute_method_affix prefix: 'reset_', suffix: '_to_default!' # attribute_method_suffix '_contrived?' # attribute_method_prefix 'clear_' - # define_attribute_methods ['name'] + # define_attribute_methods :name # # attr_accessor :name # @@ -43,17 +48,16 @@ module ActiveModel # end # # def reset_attribute_to_default!(attr) - # send("#{attr}=", "Default Name") + # send("#{attr}=", 'Default Name') # end # end # # Note that whenever you include ActiveModel::AttributeMethods in your class, - # it requires you to implement an <tt>attributes</tt> method which returns a hash + # it requires you to implement an +attributes+ method which returns a hash # with each attribute name in your model as hash key and the attribute value as # hash value. # # Hash keys must be strings. - # module AttributeMethods extend ActiveSupport::Concern @@ -61,8 +65,8 @@ module ActiveModel CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do - extend ActiveModel::Configuration - config_attribute :attribute_method_matchers + class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false + self.attribute_aliases = {} self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] end @@ -79,14 +83,12 @@ module ActiveModel # 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] + # define_attribute_methods :name # # private # @@ -96,12 +98,12 @@ module ActiveModel # end # # person = Person.new - # person.name = "Bob" + # person.name = 'Bob' # person.name # => "Bob" # person.clear_name # person.name # => nil def attribute_method_prefix(*prefixes) - self.attribute_method_matchers += prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix } + self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix } undefine_attribute_methods end @@ -114,17 +116,15 @@ module ActiveModel # # attribute#{suffix}(#{attr}, *args, &block) # - # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least - # the +attr+ argument. - # - # For example: + # An <tt>attribute#{suffix}</tt> instance method must exist and accept at + # least the +attr+ argument. # # class Person - # # include ActiveModel::AttributeMethods + # # attr_accessor :name # attribute_method_suffix '_short?' - # define_attribute_methods [:name] + # define_attribute_methods :name # # private # @@ -134,11 +134,11 @@ module ActiveModel # end # # person = Person.new - # person.name = "Bob" + # person.name = 'Bob' # person.name # => "Bob" # person.name_short? # => true def attribute_method_suffix(*suffixes) - self.attribute_method_matchers += suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix } + self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix } undefine_attribute_methods end @@ -155,14 +155,12 @@ module ActiveModel # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and # accept at least the +attr+ argument. # - # For example: - # # class Person - # # include ActiveModel::AttributeMethods + # # attr_accessor :name - # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' - # define_attribute_methods [:name] + # attribute_method_affix prefix: 'reset_', suffix: '_to_default!' + # define_attribute_methods :name # # private # @@ -176,35 +174,61 @@ module ActiveModel # person.reset_name_to_default! # person.name # => 'Gemma' def attribute_method_affix(*affixes) - self.attribute_method_matchers += 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 + + # Allows you to make aliases for attributes. + # + # class Person + # include ActiveModel::AttributeMethods + # + # attr_accessor :name + # attribute_method_suffix '_short?' + # define_attribute_methods :name + # + # alias_attribute :nickname, :name + # + # private + # + # def attribute_short?(attr) + # send(attr).length < 5 + # end + # end + # + # person = Person.new + # person.name = 'Bob' + # person.name # => "Bob" + # person.nickname # => "Bob" + # person.name_short? # => true + # person.nickname_short? # => true def alias_attribute(new_name, old_name) + self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s) attribute_method_matchers.each do |matcher| matcher_new = matcher.method_name(new_name).to_s matcher_old = matcher.method_name(old_name).to_s - define_optimized_call self, matcher_new, matcher_old + define_proxy_call false, self, matcher_new, matcher_old end end # 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. + # To use, pass attribute names (as strings or symbols), be sure to declare + # +define_attribute_methods+ after you define any prefix, suffix or affix + # methods, or they will not hook in. # # class Person - # # include ActiveModel::AttributeMethods + # # attr_accessor :name, :age, :address # attribute_method_prefix 'clear_' # # # Call to define_attribute_methods must appear after the # # attribute_method_prefix, attribute_method_suffix or # # attribute_method_affix declares. - # define_attribute_methods [:name, :age, :address] + # define_attribute_methods :name, :age, :address # # private # @@ -212,10 +236,39 @@ module ActiveModel # ... # end # end - def define_attribute_methods(attr_names) - attr_names.each { |attr_name| define_attribute_method(attr_name) } + def define_attribute_methods(*attr_names) + attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) } end + # Declares an attribute that should be prefixed and suffixed by + # ActiveModel::AttributeMethods. + # + # To use, pass an attribute name (as string or symbol), be sure to declare + # +define_attribute_method+ after you define any prefix, suffix or affix + # method, or they will not hook in. + # + # class Person + # include ActiveModel::AttributeMethods + # + # attr_accessor :name + # attribute_method_suffix '_short?' + # + # # Call to define_attribute_method must appear after the + # # attribute_method_prefix, attribute_method_suffix or + # # attribute_method_affix declares. + # define_attribute_method :name + # + # private + # + # def attribute_short?(attr) + # send(attr).length < 5 + # end + # end + # + # person = Person.new + # person.name = 'Bob' + # person.name # => "Bob" + # person.name_short? # => true def define_attribute_method(attr_name) attribute_method_matchers.each do |matcher| method_name = matcher.method_name(attr_name) @@ -226,14 +279,36 @@ module ActiveModel if respond_to?(generate_method, true) send(generate_method, attr_name) else - define_optimized_call generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s + define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end end end attribute_method_matchers_cache.clear end - # Removes all the previously dynamically defined methods from the class + # Removes all the previously dynamically defined methods from the class. + # + # class Person + # include ActiveModel::AttributeMethods + # + # attr_accessor :name + # attribute_method_suffix '_short?' + # define_attribute_method :name + # + # private + # + # def attribute_short?(attr) + # send(attr).length < 5 + # end + # end + # + # person = Person.new + # person.name = 'Bob' + # person.name_short? # => true + # + # Person.undefine_attribute_methods + # + # person.name_short? # => NoMethodError def undefine_attribute_methods generated_attribute_methods.module_eval do instance_methods.each { |m| undef_method(m) } @@ -243,15 +318,11 @@ module ActiveModel # Returns true if the attribute methods defined have been generated. def generated_attribute_methods #:nodoc: - @generated_attribute_methods ||= begin - mod = Module.new - include mod - mod - end + @generated_attribute_methods ||= Module.new.tap { |mod| include mod } end protected - def instance_method_already_implemented?(method_name) + def instance_method_already_implemented?(method_name) #:nodoc: generated_attribute_methods.method_defined?(method_name) end @@ -270,34 +341,32 @@ module ActiveModel end def attribute_method_matcher(method_name) #:nodoc: - if attribute_method_matchers_cache.key?(method_name) - attribute_method_matchers_cache[method_name] - else + attribute_method_matchers_cache.fetch(method_name) do |name| # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix # will match every time. matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1) match = nil - matchers.detect { |method| match = method.match(method_name) } - attribute_method_matchers_cache[method_name] = match + matchers.detect { |method| match = method.match(name) } + attribute_method_matchers_cache[name] = match end end # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This fallbacks `define_method` # and `send` if the given names cannot be compiled. - def define_optimized_call(mod, name, send, *extra) #:nodoc: - if name =~ NAME_COMPILABLE_REGEXP - defn = "def #{name}(*args)" + def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: + defn = if name =~ NAME_COMPILABLE_REGEXP + "def #{name}(*args)" else - defn = "define_method(:'#{name}') do |*args|" + "define_method(:'#{name}') do |*args|" end - extra = (extra.map(&:inspect) << "*args").join(", ") + extra = (extra.map!(&:inspect) << "*args").join(", ") - if send =~ CALL_COMPILABLE_REGEXP - target = "#{send}(#{extra})" + target = if send =~ CALL_COMPILABLE_REGEXP + "#{"self." unless include_private}#{send}(#{extra})" else - target = "send(:'#{send}', #{extra})" + "send(:'#{send}', #{extra})" end mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -307,14 +376,12 @@ module ActiveModel RUBY end - class AttributeMethodMatcher + class AttributeMethodMatcher #:nodoc: attr_reader :prefix, :suffix, :method_missing_target AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) def initialize(options = {}) - options.symbolize_keys! - if options[:prefix] == '' || options[:suffix] == '' ActiveSupport::Deprecation.warn( "Specifying an empty prefix/suffix for an attribute method is no longer " \ @@ -324,7 +391,7 @@ module ActiveModel ) end - @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' + @prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '') @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/ @method_missing_target = "#{@prefix}attribute#{@suffix}" @method_name = "#{prefix}%s#{suffix}" @@ -333,8 +400,6 @@ module ActiveModel def match(method_name) if @regex =~ method_name AttributeMethodMatch.new(method_missing_target, $1, method_name) - else - nil end end @@ -393,7 +458,7 @@ module ActiveModel end protected - def attribute_method?(attr_name) + def attribute_method?(attr_name) #:nodoc: respond_to_without_attributes?(:attributes) && attributes.include?(attr_name) end @@ -402,7 +467,7 @@ module ActiveModel # The struct's attributes are prefix, base and suffix. def match_attribute_method?(method_name) match = self.class.send(:attribute_method_matcher, method_name) - match && attribute_method?(match.attr_name) ? match : nil + match if match && attribute_method?(match.attr_name) end def missing_attribute(attr_name, stack) diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index ebb4b51aa3..e442455a53 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -6,7 +6,7 @@ module ActiveModel # 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. + # one of the methods in the chain returns +false+. # # First, extend ActiveModel::Callbacks from the class you are creating: # @@ -18,9 +18,10 @@ module ActiveModel # # 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 - # you want callbacks on in a block so that the callbacks get a chance to fire: + # This will provide all three standard callbacks (before, around and after) + # for both the <tt>:create</tt> and <tt>:update</tt> 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_callbacks :create do @@ -28,8 +29,8 @@ module ActiveModel # end # end # - # Then in your class, you can use the +before_create+, +after_create+ and +around_create+ - # methods, just as you would in an Active Record module. + # 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 # @@ -37,39 +38,52 @@ module ActiveModel # # Your code here # end # + # When defining an around callback remember to yield to the block, otherwise + # it won't be executed: + # + # around_create :log_status + # + # def log_status + # puts 'going to call the block...' + # yield + # puts 'block successfully called.' + # end + # # You can choose not to have all three callbacks by passing a hash to the - # define_model_callbacks method. + # +define_model_callbacks+ method. # - # define_model_callbacks :create, :only => [:after, :before] + # define_model_callbacks :create, only: [:after, :before] # - # Would only create the after_create and before_create callback methods in your - # class. + # Would only create the +after_create+ and +before_create+ callback methods in + # your class. module Callbacks - def self.extended(base) + def self.extended(base) #:nodoc: base.class_eval do include ActiveSupport::Callbacks end 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, - # where you can choose if you want all types (before, around or after) or just some. + # define_model_callbacks accepts the same options +define_callbacks+ does, + # in case you want to overwrite a default. Besides that, it also accepts an + # <tt>:only</tt> option, where you can choose if you want all types (before, + # around or after) or just some. # - # define_model_callbacks :initializer, :only => :after + # 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 + # 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 + # 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. + # 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. + # 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 @@ -83,15 +97,14 @@ module ActiveModel # # 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", - :skip_after_callbacks_if_terminated => true, - :scope => [:kind, :name], - :only => [:before, :around, :after] - }.merge(options) + :terminator => "result == false", + :skip_after_callbacks_if_terminated => true, + :scope => [:kind, :name], + :only => [:before, :around, :after] + }.merge!(options) types = Array(options.delete(:only)) @@ -104,6 +117,8 @@ module ActiveModel end end + private + def _define_before_model_callback(klass, callback) #:nodoc: klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1 def self.before_#{callback}(*args, &block) diff --git a/activemodel/lib/active_model/configuration.rb b/activemodel/lib/active_model/configuration.rb deleted file mode 100644 index 1757c12ebf..0000000000 --- a/activemodel/lib/active_model/configuration.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/class/attribute_accessors' - -module ActiveModel - # This API is for Rails' internal use and is not currently considered 'public', so - # it may change in the future without warning. - # - # It creates configuration attributes that can be inherited from a module down - # to a class that includes the module. E.g. - # - # module MyModel - # extend ActiveModel::Configuration - # config_attribute :awesome - # self.awesome = true - # end - # - # class Post - # include MyModel - # end - # - # Post.awesome # => true - # - # Post.awesome = false - # Post.awesome # => false - # MyModel.awesome # => true - # - # We assume that the module will have a ClassMethods submodule containing methods - # to be transferred to the including class' singleton class. - # - # Config options can also be defined directly on a class: - # - # class Post - # extend ActiveModel::Configuration - # config_attribute :awesome - # end - # - # So this allows us to define a module that doesn't care about whether it is being - # included in a class or a module: - # - # module Awesomeness - # extend ActiveSupport::Concern - # - # included do - # extend ActiveModel::Configuration - # config_attribute :awesome - # self.awesome = true - # end - # end - # - # class Post - # include Awesomeness - # end - # - # module AwesomeModel - # include Awesomeness - # end - module Configuration #:nodoc: - def config_attribute(name, options = {}) - klass = self.is_a?(Class) ? ClassAttribute : ModuleAttribute - klass.new(self, name, options).define - end - - class Attribute - attr_reader :host, :name, :options - - def initialize(host, name, options) - @host, @name, @options = host, name, options - end - - def instance_writer? - options.fetch(:instance_writer, false) - end - end - - class ClassAttribute < Attribute - def define - if options[:global] - host.cattr_accessor name, :instance_writer => instance_writer? - else - host.class_attribute name, :instance_writer => instance_writer? - end - end - end - - class ModuleAttribute < Attribute - def class_methods - @class_methods ||= begin - if host.const_defined?(:ClassMethods, false) - host.const_get(:ClassMethods) - else - host.const_set(:ClassMethods, Module.new) - end - end - end - - def define - host.singleton_class.class_eval <<-CODE, __FILE__, __LINE__ - attr_accessor :#{name} - def #{name}?; !!#{name}; end - CODE - - name, host = self.name, self.host - - class_methods.class_eval do - define_method(name) { host.send(name) } - define_method("#{name}?") { !!send(name) } - end - - host.class_eval <<-CODE - def #{name}; defined?(@#{name}) ? @#{name} : self.class.#{name}; end - def #{name}?; !!#{name}; end - CODE - - if options[:global] - class_methods.class_eval do - define_method("#{name}=") { |val| host.send("#{name}=", val) } - end - else - class_methods.class_eval <<-CODE, __FILE__, __LINE__ - def #{name}=(val) - singleton_class.class_eval do - remove_possible_method(:#{name}) - define_method(:#{name}) { val } - end - end - CODE - end - - host.send(:attr_writer, name) if instance_writer? - end - end - end -end diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index c7c805f1a2..48c53f0789 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' require 'active_support/inflector' module ActiveModel @@ -18,17 +17,23 @@ module ActiveModel # end # # cm = ContactMessage.new - # cm.to_model == self # => true - # cm.to_key # => nil - # cm.to_param # => nil - # cm.to_path # => "contact_messages/contact_message" - # + # cm.to_model == cm # => true + # cm.to_key # => nil + # cm.to_param # => nil + # cm.to_partial_path # => "contact_messages/contact_message" module Conversion extend ActiveSupport::Concern # If your object is already designed to implement all of the Active Model # you can use the default <tt>:to_model</tt> implementation, which simply - # returns self. + # returns +self+. + # + # class Person + # include ActiveModel::Conversion + # end + # + # person = Person.new + # person.to_model == person # => true # # 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 @@ -37,27 +42,46 @@ module ActiveModel self end - # Returns an Enumerable of all key attributes if any is set, regardless - # if the object is persisted or not. + # Returns an Enumerable of all key attributes if any is set, regardless if + # the object is persisted or not. If there no key attributes, returns +nil+. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.create + # person.to_key # => [1] def to_key key = respond_to?(:id) && id key ? [key] : nil end - # Returns a string representing the object's key suitable for use in URLs, - # or nil if <tt>persisted?</tt> is false. + # Returns a +string+ representing the object's key suitable for use in URLs, + # or +nil+ if <tt>persisted?</tt> is +false+. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.create + # person.to_param # => "1" def to_param persisted? ? to_key.join('-') : nil end - # Returns a string identifying the path associated with the object. + # Returns a +string+ identifying the path associated with the object. # ActionPack uses this to find a suitable partial to represent the object. + # + # class Person + # include ActiveModel::Conversion + # end + # + # person = Person.new + # person.to_partial_path # => "people/person" def to_partial_path self.class._to_partial_path end module ClassMethods #:nodoc: - # Provide a class level cache for the to_path. This is an + # Provide a class level cache for #to_partial_path. This is an # internal method and should not be accessed directly. def _to_partial_path #:nodoc: @_to_partial_path ||= begin diff --git a/activemodel/lib/active_model/deprecated_mass_assignment_security.rb b/activemodel/lib/active_model/deprecated_mass_assignment_security.rb new file mode 100644 index 0000000000..2ea69991fc --- /dev/null +++ b/activemodel/lib/active_model/deprecated_mass_assignment_security.rb @@ -0,0 +1,19 @@ +module ActiveModel + module DeprecatedMassAssignmentSecurity # :nodoc: + extend ActiveSupport::Concern + + module ClassMethods # :nodoc: + def attr_protected(*args) + raise "`attr_protected` is extracted out of Rails into a gem. " \ + "Please use new recommended protection model for params " \ + "or add `protected_attributes` to your Gemfile to use old one." + end + + def attr_accessible(*args) + raise "`attr_accessible` is extracted out of Rails into a gem. " \ + "Please use new recommended protection model for params " \ + "or add `protected_attributes` to your Gemfile to use old one." + end + end + end +end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 026f077ee7..c0b268fa4d 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -10,14 +10,14 @@ module ActiveModel # # The requirements for implementing ActiveModel::Dirty are: # - # * <tt>include ActiveModel::Dirty</tt> in your object + # * <tt>include ActiveModel::Dirty</tt> in your object. # * Call <tt>define_attribute_methods</tt> passing each method you want to - # track + # track. # * Call <tt>attr_name_will_change!</tt> before each change to the tracked - # attribute + # attribute. # # If you wish to also track previous changes on save or update, you need to - # add + # add: # # @previously_changed = changes # @@ -26,10 +26,9 @@ module ActiveModel # A minimal implementation could be: # # class Person - # # include ActiveModel::Dirty # - # define_attribute_methods [:name] + # define_attribute_methods :name # # def name # @name @@ -44,46 +43,49 @@ module ActiveModel # @previously_changed = changes # @changed_attributes.clear # end - # # end # - # == Examples: - # # A newly instantiated object is unchanged: + # # person = Person.find_by_name('Uncle Bob') # person.changed? # => false # # Change the name: + # # person.name = 'Bob' # person.changed? # => true # person.name_changed? # => true - # person.name_was # => 'Uncle Bob' - # person.name_change # => ['Uncle Bob', 'Bob'] + # person.name_was # => "Uncle Bob" + # person.name_change # => ["Uncle Bob", "Bob"] # person.name = 'Bill' - # person.name_change # => ['Uncle Bob', 'Bill'] + # person.name_change # => ["Uncle Bob", "Bill"] # # Save the changes: + # # person.save # person.changed? # => false # person.name_changed? # => false # # Assigning the same value leaves the attribute unchanged: + # # person.name = 'Bill' # person.name_changed? # => false # person.name_change # => nil # # Which attributes have changed? + # # person.name = 'Bob' - # person.changed # => ['name'] - # person.changes # => { 'name' => ['Bill', 'Bob'] } + # person.changed # => ["name"] + # person.changes # => {"name" => ["Bill", "Bob"]} # # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> - # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to - # in-place attributes. + # to mark that the attribute is changing. Otherwise ActiveModel can't track + # changes to in-place attributes. # # person.name_will_change! + # person.name_change # => ["Bill", "Bill"] # person.name << 'y' - # person.name_change # => ['Bill', 'Billy'] + # person.name_change # => ["Bill", "Billy"] module Dirty extend ActiveSupport::Concern include ActiveModel::AttributeMethods @@ -93,40 +95,50 @@ module ActiveModel attribute_method_affix :prefix => 'reset_', :suffix => '!' end - # Returns true if any attribute have unsaved changes, false otherwise. + # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. + # # person.changed? # => false # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.any? + changed_attributes.present? end - # List of attributes with unsaved changes. + # Returns an array with the name of the attributes with unsaved changes. + # # person.changed # => [] # person.name = 'bob' - # person.changed # => ['name'] + # person.changed # => ["name"] def changed changed_attributes.keys end - # Map of changed attrs => [original value, new value]. + # Returns a hash of changed attributes indicating their original + # and new values like <tt>attr => [original value, new value]</tt>. + # # person.changes # => {} # person.name = 'bob' - # person.changes # => { 'name' => ['bill', 'bob'] } + # person.changes # => { "name" => ["bill", "bob"] } def changes HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] end - # Map of attributes that were changed when the model was saved. - # person.name # => 'bob' + # Returns a hash of attributes that were changed before the model was saved. + # + # person.name # => "bob" # person.name = 'robert' # person.save - # person.previous_changes # => {'name' => ['bob, 'robert']} + # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes @previously_changed end - # Map of change <tt>attr => original value</tt>. + # Returns a hash of the attributes with unsaved changes indicating their original + # values like <tt>attr => original value</tt>. + # + # person.name # => "bob" + # person.name = 'robert' + # person.changed_attributes # => {"name" => "bob"} def changed_attributes @changed_attributes ||= {} end @@ -150,13 +162,15 @@ module ActiveModel # Handle <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) + return if attribute_changed?(attr) + begin value = __send__(attr) value = value.duplicable? ? value.clone : value rescue TypeError, NoMethodError end - changed_attributes[attr] = value unless changed_attributes.include?(attr) + changed_attributes[attr] = value end # Handle <tt>reset_*!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 042f9cd7e2..b3b9ba8e56 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -2,8 +2,6 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/hash/reverse_merge' module ActiveModel # == Active Model Errors @@ -55,8 +53,8 @@ module ActiveModel # The above allows you to do: # # p = Person.new - # p.validate! # => ["can not be nil"] - # p.errors.full_messages # => ["name can not be nil"] + # person.validate! # => ["can not be nil"] + # person.errors.full_messages # => ["name can not be nil"] # # etc.. class Errors include Enumerable @@ -77,33 +75,55 @@ module ActiveModel @messages = {} end - def initialize_dup(other) + def initialize_dup(other) #:nodoc: @messages = other.messages.dup super end - # Clear the messages + # Clear the error messages. + # + # person.errors.full_messages # => ["name can not be nil"] + # person.errors.clear + # person.errors.full_messages # => [] def clear messages.clear end - # Do the error messages include an error with key +error+? - def include?(error) - (v = messages[error]) && v.any? + # Returns +true+ if the error messages include an error for the given key + # +attribute+, +false+ otherwise. + # + # person.errors.messages # => { :name => ["can not be nil"] } + # person.errors.include?(:name) # => true + # person.errors.include?(:age) # => false + def include?(attribute) + (v = messages[attribute]) && v.any? end + # aliases include? alias :has_key? :include? - # Get messages for +key+ + # Get messages for +key+. + # + # person.errors.messages # => { :name => ["can not be nil"] } + # person.errors.get(:name) # => ["can not be nil"] + # person.errors.get(:age) # => nil def get(key) messages[key] end - # Set messages for +key+ to +value+ + # Set messages for +key+ to +value+. + # + # person.errors.get(:name) # => ["can not be nil"] + # person.errors.set(:name, ["can't be nil"]) + # person.errors.get(:name) # => ["can't be nil"] def set(key, value) messages[key] = value end - # Delete messages for +key+ + # Delete messages for +key+. Returns the deleted messages. + # + # person.errors.get(:name) # => ["can not be nil"] + # person.errors.delete(:name) # => ["can not be nil"] + # person.errors.get(:name) # => nil def delete(key) messages.delete(key) end @@ -111,16 +131,16 @@ module ActiveModel # 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"] + # person.errors[:name] # => ["can not be nil"] + # person.errors['name'] # => ["can not be nil"] def [](attribute) get(attribute.to_sym) || set(attribute.to_sym, []) end # Adds to the supplied attribute the supplied error message. # - # p.errors[:name] = "must be set" - # p.errors[:name] # => ['must be set'] + # person.errors[:name] = "must be set" + # person.errors[:name] # => ['must be set'] def []=(attribute, error) self[attribute] << error end @@ -129,13 +149,13 @@ module ActiveModel # 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| + # person.errors.add(:name, "can't be blank") + # person.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # end # - # p.errors.add(:name, "must be specified") - # p.errors.each do |attribute, errors_array| + # person.errors.add(:name, "must be specified") + # person.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # # then yield :name and "must be specified" # end @@ -147,54 +167,65 @@ module ActiveModel # 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") - # p.errors.size # => 2 + # person.errors.add(:name, "can't be blank") + # person.errors.size # => 1 + # person.errors.add(:name, "must be specified") + # person.errors.size # => 2 def size values.flatten.size end - # Returns all message values + # Returns all message values. + # + # person.errors.messages # => { :name => ["can not be nil", "must be specified"] } + # person.errors.values # => [["can not be nil", "must be specified"]] def values messages.values end - # Returns all message keys + # Returns all message keys. + # + # person.errors.messages # => { :name => ["can not be nil", "must be specified"] } + # person.errors.keys # => [:name] def keys messages.keys end - # Returns an array of error messages, with the attribute name included + # 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"] + # person.errors.add(:name, "can't be blank") + # person.errors.add(:name, "must be specified") + # person.errors.to_a # => ["name can't be blank", "name must be specified"] def to_a full_messages end # Returns the number of error messages. - # p.errors.add(:name, "can't be blank") - # p.errors.count # => 1 - # p.errors.add(:name, "must be specified") - # p.errors.count # => 2 + # + # person.errors.add(:name, "can't be blank") + # person.errors.count # => 1 + # person.errors.add(:name, "must be specified") + # person.errors.count # => 2 def count to_a.size end - # Returns true if no errors are found, false otherwise. + # Returns +true+ if no errors are found, +false+ otherwise. # If the error message is a string it can be empty. + # + # person.errors.full_messages # => ["name can not be nil"] + # person.errors.empty? # => false def empty? all? { |k, v| v && v.empty? && !v.is_a?(String) } end + # aliases empty? 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 + # person.errors.add(:name, "can't be blank") + # person.errors.add(:name, "must be specified") + # person.errors.to_xml # # => # # <?xml version=\"1.0\" encoding=\"UTF-8\"?> # # <errors> @@ -202,22 +233,29 @@ module ActiveModel # # <error>name must be specified</error> # # </errors> def to_xml(options={}) - to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) + to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options)) end - # Returns an Hash that can be used as the JSON representation for this object. - # Options: - # * <tt>:full_messages</tt> - determines if json object should contain - # full messages or not. Default: <tt>false</tt>. + # Returns a Hash that can be used as the JSON representation for this + # object. You can pass the <tt>:full_messages</tt> option. This determines + # if the json object should contain full messages or not (false by default). + # + # person.as_json # => { :name => ["can not be nil"] } + # person.as_json(full_messages: true) # => { :name => ["name can not be nil"] } def as_json(options=nil) to_hash(options && options[:full_messages]) end + # Returns a Hash of attributes with their error messages. If +full_messages+ + # is +true+, it will contain full messages (see +full_message+). + # + # person.to_hash # => { :name => ["can not be nil"] } + # person.to_hash(true) # => { :name => ["name can not be nil"] } def to_hash(full_messages = false) if full_messages messages = {} self.messages.each do |attribute, array| - messages[attribute] = array.map{|message| full_message(attribute, message) } + messages[attribute] = array.map { |message| full_message(attribute, message) } end messages else @@ -225,22 +263,50 @@ module ActiveModel end end - # Adds +message+ to the error messages on +attribute+. More than one error can be added to the same - # +attribute+. - # If no +message+ is supplied, <tt>:invalid</tt> is assumed. + # Adds +message+ to the error messages on +attribute+. More than one error + # can be added to the same +attribute+. If no +message+ is supplied, + # <tt>:invalid</tt> is assumed. + # + # person.errors.add(:name) + # # => ["is invalid"] + # person.errors.add(:name, 'must be implemented') + # # => ["is invalid", "must be implemented"] + # + # person.errors.messages + # # => { :name => ["must be implemented", "is invalid"] } + # + # If +message+ is a symbol, it will be translated using the appropriate + # scope (see +generate_message+). + # + # If +message+ is a proc, it will be called, allowing for things like + # <tt>Time.now</tt> to be used within an error. # - # If +message+ is a symbol, it will be translated using the appropriate scope (see +generate_message+). - # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error. + # If the <tt>:strict</tt> option is set to true will raise + # ActiveModel::StrictValidationFailed instead of adding the error. + # <tt>:strict</tt> option can also be set to any other exception. + # + # person.errors.add(:name, nil, strict: true) + # # => ActiveModel::StrictValidationFailed: name is invalid + # person.errors.add(:name, nil, strict: NameIsInvalid) + # # => NameIsInvalid: name is invalid + # + # person.errors.messages # => {} def add(attribute, message = nil, options = {}) message = normalize_message(attribute, message, options) - if options[:strict] - raise ActiveModel::StrictValidationFailed, full_message(attribute, message) + if exception = options[:strict] + exception = ActiveModel::StrictValidationFailed if exception == true + raise exception, full_message(attribute, message) end self[attribute] << message end - # Will add an error message to each of the attributes in +attributes+ that is empty. + # Will add an error message to each of the attributes in +attributes+ + # that is empty. + # + # person.errors.add_on_empty(:name) + # person.errors.messages + # # => { :name => ["can't be empty"] } def add_on_empty(attributes, options = {}) [attributes].flatten.each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) @@ -249,7 +315,12 @@ module ActiveModel end end - # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). + # Will add an error message to each of the attributes in +attributes+ that + # is blank (using Object#blank?). + # + # person.errors.add_on_blank(:name) + # person.errors.messages + # # => { :name => ["can't be blank"] } def add_on_blank(attributes, options = {}) [attributes].flatten.each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) @@ -257,10 +328,11 @@ module ActiveModel end end - # Returns true if an error on the attribute with the given message is present, false otherwise. - # +message+ is treated the same as for +add+. - # p.errors.add :name, :blank - # p.errors.added? :name, :blank # => true + # Returns +true+ if an error on the attribute with the given message is + # present, +false+ otherwise. +message+ is treated the same as for +add+. + # + # person.errors.add :name, :blank + # person.errors.added? :name, :blank # => true def added?(attribute, message = nil, options = {}) message = normalize_message(attribute, message, options) self[attribute].include? message @@ -268,25 +340,24 @@ module ActiveModel # Returns all the full error messages in an array. # - # class Company + # class Person # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 + # validates_length_of :name, in: 5..30 # end # - # company = Company.create(:address => '123 First St.') - # company.errors.full_messages # => - # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"] + # person = Person.create(address: '123 First St.') + # person.errors.full_messages + # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"] def full_messages map { |attribute, message| full_message(attribute, message) } end # Returns a full message for a given attribute. # - # company.errors.full_message(:name, "is invalid") # => - # "Name is invalid" + # person.errors.full_message(:name, 'is invalid') # => "Name is invalid" def full_message(attribute, message) return message if attribute == :base - attr_name = attribute.to_s.gsub('.', '_').humanize + attr_name = attribute.to_s.tr('.', '_').humanize attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) I18n.t(:"errors.format", { :default => "%{attribute} %{message}", @@ -299,10 +370,11 @@ module ActiveModel # (<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 - # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model name, - # translated attribute name and the value are available for interpolation. + # 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 @@ -318,7 +390,6 @@ module ActiveModel # * <tt>activemodel.errors.messages.blank</tt> # * <tt>errors.attributes.title.blank</tt> # * <tt>errors.messages.blank</tt> - # def generate_message(attribute, type = :invalid, options = {}) type = options.delete(:message) if options[:message].is_a?(Symbol) @@ -347,7 +418,7 @@ module ActiveModel :model => @base.class.model_name.human, :attribute => @base.class.human_attribute_name(attribute), :value => value - }.merge(options) + }.merge!(options) I18n.translate(key, options) end @@ -356,9 +427,10 @@ module ActiveModel def normalize_message(attribute, message, options) message ||= :invalid - if message.is_a?(Symbol) + case message + when Symbol generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) - elsif message.is_a?(Proc) + when Proc message.call else message @@ -366,6 +438,21 @@ module ActiveModel end end + # Raised when a validation cannot be corrected by end users and are considered + # exceptional. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # + # validates_presence_of :name, strict: true + # end + # + # person = Person.new + # person.name = nil + # person.valid? + # # => ActiveModel::StrictValidationFailed: Name can't be blank class StrictValidationFailed < StandardError end end diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb new file mode 100644 index 0000000000..4c05b19cba --- /dev/null +++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb @@ -0,0 +1,27 @@ +module ActiveModel + # Raised when forbidden attributes are used for mass assignment. + # + # class Person < ActiveRecord::Base + # end + # + # params = ActionController::Parameters.new(name: 'Bob') + # Person.new(params) + # # => ActiveModel::ForbiddenAttributesError + # + # params.permit! + # Person.new(params) + # # => #<Person id: nil, name: "Bob"> + class ForbiddenAttributesError < StandardError + end + + module ForbiddenAttributesProtection # :nodoc: + protected + def sanitize_for_mass_assignment(attributes, options = {}) + if attributes.respond_to?(:permitted?) && !attributes.permitted? + raise ActiveModel::ForbiddenAttributesError + else + attributes + end + end + end +end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index a10fdefd1a..550fa474ea 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -12,19 +12,20 @@ module ActiveModel # you want all features out of the box. # # These tests do not attempt to determine the semantic correctness of the - # returned values. For instance, you could implement valid? to always - # return true, and the tests would pass. It is up to you to ensure that - # the values are semantically meaningful. + # returned values. For instance, you could implement <tt>valid?</tt> to + # always return true, and the tests would pass. It is up to you to ensure + # 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. + # Objects you pass in are expected to return a compliant object from a call + # to <tt>to_model</tt>. It is perfectly fine for <tt>to_model</tt> to return + # +self+. module Tests # == Responds to <tt>to_key</tt> # # Returns an Enumerable of all (primary) key attributes - # or nil if model.persisted? is false. This is used by - # dom_id to generate unique ids for the object. + # or nil if <tt>model.persisted?</tt> is false. This is used by + # <tt>dom_id</tt> to generate unique ids for the object. def test_to_key assert model.respond_to?(:to_key), "The model should respond to to_key" def model.persisted?() false end @@ -34,13 +35,14 @@ module ActiveModel # == Responds to <tt>to_param</tt> # # Returns a string representing the object's key suitable for use in URLs - # or nil if model.persisted? is false. + # or +nil+ if <tt>model.persisted?</tt> is +false+. # - # Implementers can decide to either raise an exception or provide a default - # in case the record uses a composite primary key. There are no tests for this - # behavior in lint because it doesn't make sense to force any of the possible - # implementation strategies on the implementer. However, if the resource is - # not persisted?, then to_param should always return nil. + # Implementers can decide to either raise an exception or provide a + # default in case the record uses a composite primary key. There are no + # tests for this behavior in lint because it doesn't make sense to force + # any of the possible implementation strategies on the implementer. + # However, if the resource is not persisted?, then <tt>to_param</tt> + # 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 @@ -50,9 +52,8 @@ module ActiveModel # == Responds to <tt>to_partial_path</tt> # - # Returns a string giving a relative path. This is used for looking up + # Returns a string giving a relative path. This is used for looking up # partials. For example, a BlogPost model might return "blog_posts/blog_post" - # def test_to_partial_path assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path" assert_kind_of String, model.to_partial_path @@ -60,11 +61,11 @@ module ActiveModel # == Responds to <tt>persisted?</tt> # - # Returns a boolean that specifies whether the object has been persisted yet. - # This is used when calculating the URL for an object. If the object is - # not persisted, a form for that object, for instance, will route to the - # create action. If it is persisted, a form for the object will routes to - # the update action. + # Returns a boolean that specifies whether the object has been persisted + # yet. This is used when calculating the URL for an object. If the object + # is not persisted, a form for that object, for instance, will route to + # the create action. If it is persisted, a form for the object will routes + # to the update action. def test_persisted? assert model.respond_to?(:persisted?), "The model should respond to persisted?" assert_boolean model.persisted?, "persisted?" @@ -73,15 +74,15 @@ module ActiveModel # == Naming # # Model.model_name must return a string with some convenience methods: - # :human, :singular, and :plural. Check ActiveModel::Naming for more information. - # + # <tt>:human</tt>, <tt>:singular</tt> and <tt>:plural</tt>. Check + # ActiveModel::Naming for more information. def test_model_naming assert model.class.respond_to?(:model_name), "The model should respond to model_name" model_name = model.class.model_name - assert_kind_of String, model_name - assert_kind_of String, model_name.human - assert_kind_of String, model_name.singular - assert_kind_of String, model_name.plural + assert model_name.respond_to?(:to_str) + assert model_name.human.respond_to?(:to_str) + assert model_name.singular.respond_to?(:to_str) + assert model_name.plural.respond_to?(:to_str) end # == Errors Testing diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index ba49c6beaa..d17848c861 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -9,7 +9,7 @@ en: inclusion: "is not included in the list" exclusion: "is reserved" invalid: "is invalid" - confirmation: "doesn't match confirmation" + confirmation: "doesn't match %{attribute}" accepted: "must be accepted" empty: "can't be empty" blank: "can't be blank" diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb deleted file mode 100644 index 95de039676..0000000000 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ /dev/null @@ -1,237 +0,0 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/string/inflections' -require 'active_model/mass_assignment_security/permission_set' -require 'active_model/mass_assignment_security/sanitizer' - -module ActiveModel - # = Active Model Mass-Assignment Security - module MassAssignmentSecurity - extend ActiveSupport::Concern - - included do - extend ActiveModel::Configuration - - config_attribute :_accessible_attributes - config_attribute :_protected_attributes - config_attribute :_active_authorizer - - config_attribute :_mass_assignment_sanitizer - self.mass_assignment_sanitizer = :logger - end - - # Mass assignment security provides an interface for protecting attributes - # from end-user assignment. For more complex permissions, mass assignment security - # may be handled outside the model by extending a non-ActiveRecord class, - # such as a controller, with this behavior. - # - # For example, a logged in user may need to assign additional attributes depending - # on their role: - # - # class AccountsController < ApplicationController - # include ActiveModel::MassAssignmentSecurity - # - # attr_accessible :first_name, :last_name - # attr_accessible :first_name, :last_name, :plan_id, :as => :admin - # - # def update - # ... - # @account.update_attributes(account_params) - # ... - # end - # - # protected - # - # def account_params - # role = admin ? :admin : :default - # sanitize_for_mass_assignment(params[:account], role) - # end - # - # end - # - # = Configuration options - # - # * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible values are: - # * <tt>:logger</tt> (default) - writes filtered attributes to logger - # * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt> on any protected attribute update - # - # You can specify your own sanitizer object eg. MySanitizer.new. - # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for example implementation. - # - # - module ClassMethods - # Attributes named in this macro are protected from mass-assignment - # whenever attributes are sanitized before assignment. A role for the - # attributes is optional, if no role is provided then :default is used. - # A role can be defined by using the :as option. - # - # 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: - # - # class Customer - # include ActiveModel::MassAssignmentSecurity - # - # attr_accessor :name, :email, :logins_count - # - # attr_protected :logins_count - # # Suppose that admin can not change email for customer - # attr_protected :logins_count, :email, :as => :admin - # - # def assign_attributes(values, options = {}) - # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| - # send("#{k}=", v) - # end - # end - # end - # - # When using the :default role : - # - # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5 }, :as => :default) - # customer.name # => "David" - # customer.email # => "a@b.com" - # customer.logins_count # => nil - # - # And using the :admin role : - # - # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5}, :as => :admin) - # customer.name # => "David" - # customer.email # => nil - # customer.logins_count # => nil - # - # customer.email = "c@d.com" - # customer.email # => "c@d.com" - # - # To start from an all-closed default and enable attributes as needed, - # have a look at +attr_accessible+. - # - # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+ - # to sanitize attributes won't provide sufficient protection. - def attr_protected(*args) - options = args.extract_options! - role = options[:as] || :default - - self._protected_attributes = protected_attributes_configs.dup - - Array(role).each do |name| - self._protected_attributes[name] = self.protected_attributes(name) + args - end - - self._active_authorizer = self._protected_attributes - end - - # Specifies a white list of model attributes that can be set via - # mass-assignment. - # - # Like +attr_protected+, a role for the attributes is optional, - # if no role is provided then :default is used. A role can be defined by - # using the :as option. - # - # This is the opposite of the +attr_protected+ macro: Mass-assignment - # will only set attributes in this list, to assign to the rest of - # attributes you can use direct writer methods. This is meant to protect - # sensitive attributes from being overwritten by malicious users - # tampering with URLs or forms. If you'd rather start from an all-open - # default and restrict attributes as needed, have a look at - # +attr_protected+. - # - # class Customer - # include ActiveModel::MassAssignmentSecurity - # - # attr_accessor :name, :credit_rating - # - # attr_accessible :name - # attr_accessible :name, :credit_rating, :as => :admin - # - # def assign_attributes(values, options = {}) - # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| - # send("#{k}=", v) - # end - # end - # end - # - # When using the :default role : - # - # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) - # customer.name # => "David" - # customer.credit_rating # => nil - # - # customer.credit_rating = "Average" - # customer.credit_rating # => "Average" - # - # And using the :admin role : - # - # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) - # customer.name # => "David" - # customer.credit_rating # => "Excellent" - # - # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+ - # to sanitize attributes won't provide sufficient protection. - def attr_accessible(*args) - options = args.extract_options! - role = options[:as] || :default - - self._accessible_attributes = accessible_attributes_configs.dup - - Array(role).each do |name| - self._accessible_attributes[name] = self.accessible_attributes(name) + args - end - - self._active_authorizer = self._accessible_attributes - end - - def protected_attributes(role = :default) - protected_attributes_configs[role] - end - - def accessible_attributes(role = :default) - accessible_attributes_configs[role] - end - - def active_authorizers - self._active_authorizer ||= protected_attributes_configs - end - alias active_authorizer active_authorizers - - def attributes_protected_by_default - [] - end - - def mass_assignment_sanitizer=(value) - self._mass_assignment_sanitizer = if value.is_a?(Symbol) - const_get(:"#{value.to_s.camelize}Sanitizer").new(self) - else - value - end - end - - private - - def protected_attributes_configs - self._protected_attributes ||= begin - Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) } - end - end - - def accessible_attributes_configs - self._accessible_attributes ||= begin - Hash.new { |h,k| h[k] = WhiteList.new } - end - end - end - - protected - - def sanitize_for_mass_assignment(attributes, role = nil) - _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role)) - end - - def mass_assignment_authorizer(role) - self.class.active_authorizer[role || :default] - end - end -end diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb deleted file mode 100644 index 9661349503..0000000000 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'set' - -module ActiveModel - module MassAssignmentSecurity - class PermissionSet < Set - - def +(values) - super(values.map(&:to_s)) - end - - def include?(key) - super(remove_multiparameter_id(key)) - end - - def deny?(key) - raise NotImplementedError, "#deny?(key) supposed to be overwritten" - end - - protected - - def remove_multiparameter_id(key) - key.to_s.gsub(/\(.+/, '') - end - end - - class WhiteList < PermissionSet - - def deny?(key) - !include?(key) - end - end - - class BlackList < PermissionSet - - def deny?(key) - include?(key) - end - end - end -end diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb deleted file mode 100644 index cfeb4aa7cd..0000000000 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ /dev/null @@ -1,63 +0,0 @@ -module ActiveModel - module MassAssignmentSecurity - class Sanitizer - # Returns all attributes not denied by the authorizer. - def sanitize(attributes, authorizer) - sanitized_attributes = attributes.reject { |key, value| authorizer.deny?(key) } - debug_protected_attribute_removal(attributes, sanitized_attributes) - sanitized_attributes - end - - protected - - def debug_protected_attribute_removal(attributes, sanitized_attributes) - removed_keys = attributes.keys - sanitized_attributes.keys - process_removed_attributes(removed_keys) if removed_keys.any? - end - - def process_removed_attributes(attrs) - raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten" - end - end - - class LoggerSanitizer < Sanitizer - def initialize(target) - @target = target - super() - end - - def logger - @target.logger - end - - def logger? - @target.respond_to?(:logger) && @target.logger - end - - def process_removed_attributes(attrs) - logger.warn "Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger? - end - end - - class StrictSanitizer < Sanitizer - def initialize(target = nil) - super() - end - - def process_removed_attributes(attrs) - return if (attrs - insensitive_attributes).empty? - raise ActiveModel::MassAssignmentSecurity::Error.new(attrs) - end - - def insensitive_attributes - ['id'] - end - end - - class Error < StandardError - def initialize(attrs) - super("Can't mass-assign protected attributes: #{attrs.join(', ')}") - end - end - end -end diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb new file mode 100644 index 0000000000..33a530e6bd --- /dev/null +++ b/activemodel/lib/active_model/model.rb @@ -0,0 +1,97 @@ +module ActiveModel + + # == Active Model Basic Model + # + # Includes the required interface for an object to interact with + # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules. + # It includes model name introspections, conversions, translations and + # validations. Besides that, it allows you to initialize the object with a + # hash of attributes, pretty much like <tt>ActiveRecord</tt> does. + # + # A minimal implementation could be: + # + # class Person + # include ActiveModel::Model + # attr_accessor :name, :age + # end + # + # person = Person.new(name: 'bob', age: '18') + # person.name # => 'bob' + # person.age # => 18 + # + # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt> + # to return +false+, which is the most common case. You may want to override + # it in your class to simulate a different scenario: + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name + # + # def persisted? + # self.id == 1 + # end + # end + # + # person = Person.new(id: 1, name: 'bob') + # person.persisted? # => true + # + # Also, if for some reason you need to run code on <tt>initialize</tt>, make + # sure you call +super+ if you want the attributes hash initialization to + # happen. + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name, :omg + # + # def initialize(attributes={}) + # super + # @omg ||= true + # end + # end + # + # person = Person.new(id: 1, name: 'bob') + # person.omg # => true + # + # For more detailed information on other functionalities available, please + # refer to the specific modules included in <tt>ActiveModel::Model</tt> + # (see below). + module Model + def self.included(base) #:nodoc: + base.class_eval do + extend ActiveModel::Naming + extend ActiveModel::Translation + include ActiveModel::Validations + include ActiveModel::Conversion + end + end + + # Initializes a new model with the given +params+. + # + # class Person + # include ActiveModel::Model + # attr_accessor :name, :age + # end + # + # person = Person.new(name: 'bob', age: '18') + # person.name # => "bob" + # person.age # => 18 + def initialize(params={}) + params.each do |attr, value| + self.public_send("#{attr}=", value) + end if params + end + + # Indicates if the model is persisted. Default is +false+. + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name + # end + # + # person = Person.new(id: 1, name: 'bob') + # person.persisted? # => false + def persisted? + false + end + end +end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 755e54efcd..c0d93e5d53 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,43 +1,174 @@ require 'active_support/inflector' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' -require 'active_support/core_ext/module/deprecation' module ActiveModel - class Name < String - attr_reader :singular, :plural, :element, :collection, :partial_path, - :singular_route_key, :route_key, :param_key, :i18n_key + class Name + include Comparable + + attr_reader :singular, :plural, :element, :collection, + :singular_route_key, :route_key, :param_key, :i18n_key, + :name alias_method :cache_key, :collection - deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead." + ## + # :method: == + # + # :call-seq: + # ==(other) + # + # Equivalent to <tt>String#==</tt>. Returns +true+ if the class name and + # +other+ are equal, otherwise +false+. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name == 'BlogPost' # => true + # BlogPost.model_name == 'Blog Post' # => false - def initialize(klass, namespace = nil, name = nil) - name ||= klass.name + ## + # :method: === + # + # :call-seq: + # ===(other) + # + # Equivalent to <tt>#==</tt>. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name === 'BlogPost' # => true + # BlogPost.model_name === 'Blog Post' # => false - raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if name.blank? + ## + # :method: <=> + # + # :call-seq: + # ==(other) + # + # Equivalent to <tt>String#<=></tt>. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name <=> 'BlogPost' # => 0 + # BlogPost.model_name <=> 'Blog' # => 1 + # BlogPost.model_name <=> 'BlogPosts' # => -1 + + ## + # :method: =~ + # + # :call-seq: + # =~(regexp) + # + # Equivalent to <tt>String#=~</tt>. Match the class name against the given + # regexp. Returns the position where the match starts or +nil+ if there is + # no match. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name =~ /Post/ # => 4 + # BlogPost.model_name =~ /\d/ # => nil - super(name) + ## + # :method: !~ + # + # :call-seq: + # !~(regexp) + # + # Equivalent to <tt>String#!~</tt>. Match the class name against the given + # regexp. Returns +true+ if there is no match, otherwise +false+. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name !~ /Post/ # => false + # BlogPost.model_name !~ /\d/ # => true - @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace + ## + # :method: eql? + # + # :call-seq: + # eql?(other) + # + # Equivalent to <tt>String#eql?</tt>. Returns +true+ if the class name and + # +other+ have the same length and content, otherwise +false+. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name.eql?('BlogPost') # => true + # BlogPost.model_name.eql?('Blog Post') # => false + + ## + # :method: to_s + # + # :call-seq: + # to_s() + # + # Returns the class name. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name.to_s # => "BlogPost" + + ## + # :method: to_str + # + # :call-seq: + # to_str() + # + # Equivalent to +to_s+. + delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, + :to_str, :to => :name + + # Returns a new ActiveModel::Name instance. By default, the +namespace+ + # and +name+ option will take the namespace and name of the given class + # respectively. + # + # module Foo + # class Bar + # end + # end + # + # ActiveModel::Name.new(Foo::Bar).to_s + # # => "Foo::Bar" + def initialize(klass, namespace = nil, name = nil) + @name = name || klass.name + + raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank? + + @unnamespaced = @name.sub(/^#{namespace.name}::/, '') if namespace @klass = klass - @singular = _singularize(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 - @i18n_key = self.underscore.to_sym + @singular = _singularize(@name) + @plural = ActiveSupport::Inflector.pluralize(@singular) + @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name)) + @human = ActiveSupport::Inflector.humanize(@element) + @collection = ActiveSupport::Inflector.tableize(@name) + @param_key = (namespace ? _singularize(@unnamespaced) : @singular) + @i18n_key = @name.underscore.to_sym @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup) - @singular_route_key = ActiveSupport::Inflector.singularize(@route_key).freeze + @singular_route_key = ActiveSupport::Inflector.singularize(@route_key) @route_key << "_index" if @plural == @singular - @route_key.freeze end # Transform the model name into a more humane format, using I18n. By default, - # it will underscore then humanize the class name + # it will underscore then humanize the class name. + # + # class BlogPost + # extend ActiveModel::Naming + # end # # BlogPost.model_name.human # => "Blog post" # @@ -53,7 +184,7 @@ module ActiveModel defaults << options[:default] if options[:default] defaults << @human - options = {:scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults}.merge(options.except(:default)) + options = { :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults }.merge!(options.except(:default)) I18n.translate(defaults.shift, options) end @@ -81,11 +212,20 @@ module ActiveModel # BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover" # # Providing the functionality that ActiveModel::Naming provides in your object - # is required to pass the Active Model Lint test. So either extending the provided - # method below, or rolling your own is required. + # is required to pass the Active Model Lint test. So either extending the + # provided 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. + # used to retrieve all kinds of naming-related information + # (See ActiveModel::Name for more information). + # + # class Person < ActiveModel::Model + # end + # + # Person.model_name # => Person + # Person.model_name.class # => ActiveModel::Name + # Person.model_name.singular # => "person" + # Person.model_name.plural # => "people" def model_name @_model_name ||= begin namespace = self.parents.detect do |n| @@ -95,7 +235,7 @@ module ActiveModel end end - # Returns the plural class name of a record or class. Examples: + # Returns the plural class name of a record or class. # # ActiveModel::Naming.plural(post) # => "posts" # ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people" @@ -103,7 +243,7 @@ module ActiveModel model_name_from_record_or_class(record_or_class).plural end - # Returns the singular class name of a record or class. Examples: + # Returns the singular class name of a record or class. # # ActiveModel::Naming.singular(post) # => "post" # ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person" @@ -111,10 +251,10 @@ module ActiveModel model_name_from_record_or_class(record_or_class).singular end - # Identifies whether the class name of a record or class is uncountable. Examples: + # Identifies whether the class name of a record or class is uncountable. # # ActiveModel::Naming.uncountable?(Sheep) # => true - # ActiveModel::Naming.uncountable?(Post) => false + # ActiveModel::Naming.uncountable?(Post) # => false def self.uncountable?(record_or_class) plural(record_or_class) == singular(record_or_class) end @@ -122,11 +262,11 @@ module ActiveModel # 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) #=> post + # # For isolated engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> post # - # For shared engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> blog_post + # # For shared engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> blog_post def self.singular_route_key(record_or_class) model_name_from_record_or_class(record_or_class).singular_route_key end @@ -134,11 +274,11 @@ module ActiveModel # 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 isolated engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> posts # - # For shared engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts + # # For shared engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts # # The route key also considers if the noun is uncountable and, in # such cases, automatically appends _index. @@ -149,23 +289,24 @@ module ActiveModel # 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 isolated engine: + # ActiveModel::Naming.param_key(Blog::Post) #=> post # - # For shared engine: - # ActiveModel::Naming.param_key(Blog::Post) #=> blog_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 : 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 + def self.model_name_from_record_or_class(record_or_class) #:nodoc: + if record_or_class.respond_to?(:model_name) + record_or_class.model_name + elsif record_or_class.respond_to?(:to_model) + record_or_class.to_model.class.model_name + else + record_or_class.class.model_name end + end + private_class_method :model_name_from_record_or_class end - end diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb index 3d463885be..77bc0f71e3 100644 --- a/activemodel/lib/active_model/observer_array.rb +++ b/activemodel/lib/active_model/observer_array.rb @@ -5,18 +5,24 @@ module ActiveModel # a particular model class. class ObserverArray < Array attr_reader :model_class - def initialize(model_class, *args) + def initialize(model_class, *args) #:nodoc: @model_class = model_class super(*args) end - # Returns true if the given observer is disabled for the model class. - def disabled_for?(observer) + # Returns +true+ if the given observer is disabled for the model class, + # +false+ otherwise. + def disabled_for?(observer) #:nodoc: disabled_observers.include?(observer.class) end # Disables one or more observers. This supports multiple forms: # + # ORM.observers.disable :all + # # => disables all observers for all models subclassed from + # # an ORM base class that includes ActiveModel::Observing + # # e.g. ActiveRecord::Base + # # ORM.observers.disable :user_observer # # => disables the UserObserver # @@ -27,9 +33,6 @@ module ActiveModel # ORM.observers.disable :observer_1, :observer_2 # # => disables Observer1 and Observer2 for all models. # - # ORM.observers.disable :all - # # => disables all observers for all models. - # # User.observers.disable :all do # # all user observers are disabled for # # just the duration of the block @@ -40,6 +43,11 @@ module ActiveModel # Enables one or more observers. This supports multiple forms: # + # ORM.observers.enable :all + # # => enables all observers for all models subclassed from + # # an ORM base class that includes ActiveModel::Observing + # # e.g. ActiveRecord::Base + # # ORM.observers.enable :user_observer # # => enables the UserObserver # @@ -51,9 +59,6 @@ module ActiveModel # ORM.observers.enable :observer_1, :observer_2 # # => enables Observer1 and Observer2 for all models. # - # ORM.observers.enable :all - # # => enables all observers for all models. - # # User.observers.enable :all do # # all user observers are enabled for # # just the duration of the block @@ -67,11 +72,11 @@ module ActiveModel protected - def disabled_observers + def disabled_observers #:nodoc: @disabled_observers ||= Set.new end - def observer_class_for(observer) + def observer_class_for(observer) #:nodoc: return observer if observer.is_a?(Class) if observer.respond_to?(:to_sym) # string/symbol @@ -82,25 +87,25 @@ module ActiveModel end end - def start_transaction + def start_transaction #:nodoc: disabled_observer_stack.push(disabled_observers.dup) each_subclass_array do |array| array.start_transaction end end - def disabled_observer_stack + def disabled_observer_stack #:nodoc: @disabled_observer_stack ||= [] end - def end_transaction + def end_transaction #:nodoc: @disabled_observers = disabled_observer_stack.pop each_subclass_array do |array| array.end_transaction end end - def transaction + def transaction #:nodoc: start_transaction begin @@ -110,13 +115,13 @@ module ActiveModel end end - def each_subclass_array + def each_subclass_array #:nodoc: model_class.descendants.each do |subclass| yield subclass.observers end end - def set_enablement(enabled, observers) + def set_enablement(enabled, observers) #:nodoc: if block_given? transaction do set_enablement(enabled, observers) diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 32f2aa46bd..9db7639ea3 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -4,9 +4,11 @@ require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/object/try' require 'active_support/descendants_tracker' module ActiveModel + # == Active Model Observers Activation module Observing extend ActiveSupport::Concern @@ -15,9 +17,7 @@ module ActiveModel end module ClassMethods - # == Active Model Observers Activation - # - # Activates the observers assigned. Examples: + # Activates the observers assigned. # # class ORM # include ActiveModel::Observing @@ -33,34 +33,95 @@ module ActiveModel # ORM.observers = Cacher, GarbageCollector # # Note: Setting this does not instantiate the observers yet. - # +instantiate_observers+ is called during startup, and before + # <tt>instantiate_observers</tt> is called during startup, and before # each development request. def observers=(*values) observers.replace(values.flatten) end - # Gets an array of observers observing this model. - # The array also provides +enable+ and +disable+ methods - # that allow you to selectively enable and disable observers. - # (see <tt>ActiveModel::ObserverArray.enable</tt> and - # <tt>ActiveModel::ObserverArray.disable</tt> for more on this) + # Gets an array of observers observing this model. The array also provides + # +enable+ and +disable+ methods that allow you to selectively enable and + # disable observers (see ActiveModel::ObserverArray.enable and + # ActiveModel::ObserverArray.disable for more on this). + # + # class ORM + # include ActiveModel::Observing + # end + # + # ORM.observers = :cacher, :garbage_collector + # ORM.observers # => [:cacher, :garbage_collector] + # ORM.observers.class # => ActiveModel::ObserverArray def observers @observers ||= ObserverArray.new(self) end - # Gets the current observer instances. + # Returns the current observer instances. + # + # class Foo + # include ActiveModel::Observing + # + # attr_accessor :status + # end + # + # class FooObserver < ActiveModel::Observer + # def on_spec(record, *args) + # record.status = true + # end + # end + # + # Foo.observers = FooObserver + # Foo.instantiate_observers + # + # Foo.observer_instances # => [#<FooObserver:0x007fc212c40820>] def observer_instances @observer_instances ||= [] end # Instantiate the global observers. + # + # class Foo + # include ActiveModel::Observing + # + # attr_accessor :status + # end + # + # class FooObserver < ActiveModel::Observer + # def on_spec(record, *args) + # record.status = true + # end + # end + # + # Foo.observers = FooObserver + # + # foo = Foo.new + # foo.status = false + # foo.notify_observers(:on_spec) + # foo.status # => false + # + # Foo.instantiate_observers # => [FooObserver] + # + # foo = Foo.new + # foo.status = false + # foo.notify_observers(:on_spec) + # foo.status # => true def instantiate_observers observers.each { |o| instantiate_observer(o) } end - # Add a new observer to the pool. - # The new observer needs to respond to 'update', otherwise it - # raises an +ArgumentError+ exception. + # Add a new observer to the pool. The new observer needs to respond to + # <tt>update</tt>, otherwise it raises an +ArgumentError+ exception. + # + # class Foo + # include ActiveModel::Observing + # end + # + # class FooObserver < ActiveModel::Observer + # end + # + # Foo.add_observer(FooObserver.instance) + # + # Foo.observers_instance + # # => [#<FooObserver:0x007fccf55d9390>] def add_observer(observer) unless observer.respond_to? :update raise ArgumentError, "observer needs to respond to 'update'" @@ -68,50 +129,104 @@ module ActiveModel observer_instances << observer end - # Notify list of observers of a change. - def notify_observers(*arg) - observer_instances.each { |observer| observer.update(*arg) } + # Fires notifications to model's observers. + # + # def save + # notify_observers(:before_save) + # ... + # notify_observers(:after_save) + # end + # + # Custom notifications can be sent in a similar fashion: + # + # notify_observers(:custom_notification, :foo) + # + # This will call <tt>custom_notification</tt>, passing as arguments + # the current object and <tt>:foo</tt>. + def notify_observers(*args) + observer_instances.each { |observer| observer.update(*args) } end - # Total number of observers. - def count_observers + # Returns the total number of instantiated observers. + # + # class Foo + # include ActiveModel::Observing + # + # attr_accessor :status + # end + # + # class FooObserver < ActiveModel::Observer + # def on_spec(record, *args) + # record.status = true + # end + # end + # + # Foo.observers = FooObserver + # Foo.observers_count # => 0 + # Foo.instantiate_observers + # Foo.observers_count # => 1 + def observers_count observer_instances.size end + # <tt>count_observers</tt> is deprecated. Use #observers_count. + def count_observers + msg = "count_observers is deprecated in favor of observers_count" + ActiveSupport::Deprecation.warn(msg) + observers_count + end + protected def instantiate_observer(observer) #:nodoc: # string/symbol if observer.respond_to?(:to_sym) - observer.to_s.camelize.constantize.instance - elsif observer.respond_to?(:instance) + observer = observer.to_s.camelize.constantize + end + if observer.respond_to?(:instance) observer.instance else raise ArgumentError, - "#{observer} must be a lowercase, underscored class name (or an " + - "instance of the class itself) responding to the instance " + - "method. Example: Person.observers = :big_brother # calls " + + "#{observer} must be a lowercase, underscored class name (or " + + "the class itself) responding to the method :instance. " + + "Example: Person.observers = :big_brother # calls " + "BigBrother.instance" end end # Notify observers when the observed class is subclassed. - def inherited(subclass) + def inherited(subclass) #:nodoc: super notify_observers :observed_class_inherited, subclass end end - private - # Fires notifications to model's observers - # - # def save - # notify_observers(:before_save) - # ... - # notify_observers(:after_save) - # end - def notify_observers(method) - self.class.notify_observers(method, self) - end + # Notify a change to the list of observers. + # + # class Foo + # include ActiveModel::Observing + # + # attr_accessor :status + # end + # + # class FooObserver < ActiveModel::Observer + # def on_spec(record, *args) + # record.status = true + # end + # end + # + # Foo.observers = FooObserver + # Foo.instantiate_observers # => [FooObserver] + # + # foo = Foo.new + # foo.status = false + # foo.notify_observers(:on_spec) + # foo.status # => true + # + # See ActiveModel::Observing::ClassMethods.notify_observers for more + # information. + def notify_observers(method, *extra_args) + self.class.notify_observers(method, self, *extra_args) + end end # == Active Model Observers @@ -120,15 +235,15 @@ module ActiveModel # behavior outside the original class. This is a great way to reduce the # clutter that normally comes when the model class is burdened with # functionality that doesn't pertain to the core responsibility of the - # class. Example: + # class. # # class CommentObserver < ActiveModel::Observer # def after_save(comment) - # Notifications.comment("admin@do.com", "New comment was posted", comment).deliver + # Notifications.comment('admin@do.com', 'New comment was posted', comment).deliver # end # end # - # This Observer sends an email when a Comment#save is finished. + # This Observer sends an email when a <tt>Comment#save</tt> is finished. # # class ContactObserver < ActiveModel::Observer # def after_create(contact) @@ -145,52 +260,60 @@ module ActiveModel # == Observing a class that can't be inferred # # 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 <tt>Observer.observe</tt> - # class method which takes either the concrete class (Product) or a symbol for that - # class (:product): + # name. So <tt>CommentObserver</tt> will be tied to observing <tt>Comment</tt>, + # <tt>ProductManagerObserver</tt> to <tt>ProductManager</tt>, and so on. If + # you want to name your observer differently than the class you're interested + # in observing, you can use the <tt>Observer.observe</tt> class method which + # takes either the concrete class (<tt>Product</tt>) or a symbol for that + # class (<tt>:product</tt>): # # class AuditObserver < ActiveModel::Observer # observe :account # # def after_update(account) - # AuditTrail.new(account, "UPDATED") + # AuditTrail.new(account, 'UPDATED') # end # end # - # If the audit observer needs to watch more than one kind of object, this can be - # specified with multiple arguments: + # If the audit observer needs to watch more than one kind of object, this can + # be specified with multiple arguments: # # class AuditObserver < ActiveModel::Observer # observe :account, :balance # # def after_update(record) - # AuditTrail.new(record, "UPDATED") + # AuditTrail.new(record, 'UPDATED') # end # end # - # The AuditObserver will now act on both updates to Account and Balance by treating - # them both as records. + # The <tt>AuditObserver</tt> will now act on both updates to <tt>Account</tt> + # and <tt>Balance</tt> by treating them both as records. # - # If you're using an Observer in a Rails application with Active Record, be sure to - # read about the necessary configuration in the documentation for + # If you're using an Observer in a Rails application with Active Record, be + # sure to read about the necessary configuration in the documentation for # ActiveRecord::Observer. - # class Observer include Singleton extend ActiveSupport::DescendantsTracker class << self # Attaches the observer to the supplied model classes. + # + # class AuditObserver < ActiveModel::Observer + # observe :account, :balance + # end + # + # AuditObserver.observed_classes # => [Account, Balance] def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } - redefine_method(:observed_classes) { models } + singleton_class.redefine_method(:observed_classes) { models } end # Returns an array of Classes to observe. # + # AccountObserver.observed_classes # => [Account] + # # You can override this instead of using the +observe+ helper. # # class AuditObserver < ActiveModel::Observer @@ -202,19 +325,19 @@ module ActiveModel Array(observed_class) end - # The class observed by default is inferred from the observer's class name: - # assert_equal Person, PersonObserver.observed_class + # Returns the class observed by default. It's inferred from the observer's + # class name. + # + # PersonObserver.observed_class # => Person + # AccountObserver.observed_class # => Account def observed_class - if observed_class_name = name[/(.*)Observer/, 1] - observed_class_name.constantize - else - nil - end + name[/(.*)Observer/, 1].try :constantize end end # Start observing the declared classes and their subclasses. - def initialize + # Called automatically by the instance method. + def initialize #:nodoc: observed_classes.each { |klass| add_observer!(klass) } end @@ -224,10 +347,9 @@ module ActiveModel # Send observed_method(object) if the method exists and # the observer is enabled for the given object's class. - def update(observed_method, object, &block) #:nodoc: - return unless respond_to?(observed_method) - return if disabled_for?(object) - send(observed_method, object, &block) + def update(observed_method, object, *extra_args, &block) #:nodoc: + return if !respond_to?(observed_method) || disabled_for?(object) + send(observed_method, object, *extra_args, &block) end # Special method sent by the observed class when it is inherited. @@ -242,7 +364,8 @@ module ActiveModel klass.add_observer(self) end - def disabled_for?(object) + # Returns true if notifications are disabled for this object. + def disabled_for?(object) #:nodoc: klass = object.class return false unless klass.respond_to?(:observers) klass.observers.disabled_for?(self) diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb index 63ffe5db63..f239758b35 100644 --- a/activemodel/lib/active_model/railtie.rb +++ b/activemodel/lib/active_model/railtie.rb @@ -1,2 +1,8 @@ require "active_model" -require "rails"
\ No newline at end of file +require "rails" + +module ActiveModel + class Railtie < Rails::Railtie + config.eager_load_namespaces << ActiveModel + end +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index e7a57cf691..d011402081 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -6,11 +6,12 @@ module ActiveModel # 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. + # Validations for presence of password on create, confirmation of password + # (using a +password_confirmation+ attribute) are automatically added. If + # you wish to turn off validations, pass <tt>validations: false</tt> as an + # argument. You can add more validations by hand if need be. # - # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use has_secure_password: + # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password: # # gem 'bcrypt-ruby', '~> 3.0.0' # @@ -21,31 +22,36 @@ module ActiveModel # has_secure_password # end # - # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch") + # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch') # user.save # => false, password required - # user.password = "mUc3m00RsqyRe" + # user.password = 'mUc3m00RsqyRe' # user.save # => false, confirmation doesn't match - # user.password_confirmation = "mUc3m00RsqyRe" + # user.password_confirmation = 'mUc3m00RsqyRe' # user.save # => true - # user.authenticate("notright") # => false - # user.authenticate("mUc3m00RsqyRe") # => user - # User.find_by_name("david").try(:authenticate, "notright") # => false - # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user - def has_secure_password + # user.authenticate('notright') # => false + # user.authenticate('mUc3m00RsqyRe') # => user + # User.find_by_name('david').try(:authenticate, 'notright') # => false + # User.find_by_name('david').try(:authenticate, 'mUc3m00RsqyRe') # => user + def has_secure_password(options = {}) # Load bcrypt-ruby only when has_secure_password is used. - # This is to avoid ActiveModel (and by extension the entire framework) being dependent on a binary library. + # This is to avoid ActiveModel (and by extension the entire framework) + # being dependent on a binary library. gem 'bcrypt-ruby', '~> 3.0.0' require 'bcrypt' attr_reader :password - validates_confirmation_of :password - validates_presence_of :password_digest + if options.fetch(:validations, true) + validates_confirmation_of :password + validates_presence_of :password, :on => :create + + before_create { raise "Password digest missing on new record" if password_digest.blank? } + end include InstanceMethodsOnActivation if respond_to?(:attributes_protected_by_default) - def self.attributes_protected_by_default + def self.attributes_protected_by_default #:nodoc: super + ['password_digest'] end end @@ -53,19 +59,35 @@ module ActiveModel end module InstanceMethodsOnActivation - # Returns self if the password is correct, otherwise false. + # Returns +self+ if the password is correct, otherwise +false+. + # + # class User < ActiveRecord::Base + # has_secure_password validations: false + # end + # + # user = User.new(name: 'david', password: 'mUc3m00RsqyRe') + # user.save + # user.authenticate('notright') # => false + # user.authenticate('mUc3m00RsqyRe') # => user def authenticate(unencrypted_password) - if BCrypt::Password.new(password_digest) == unencrypted_password - self - else - false - end + BCrypt::Password.new(password_digest) == unencrypted_password && self end - # Encrypts the password into the password_digest attribute. + # Encrypts the password into the +password_digest+ attribute, only if the + # new password is not blank. + # + # class User < ActiveRecord::Base + # has_secure_password validations: false + # end + # + # user = User.new + # user.password = nil + # user.password_digest # => nil + # user.password = 'mUc3m00RsqyRe' + # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4." def password=(unencrypted_password) - @password = unencrypted_password unless unencrypted_password.blank? + @password = unencrypted_password self.password_digest = BCrypt::Password.create(unencrypted_password) end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 51f078e662..8a63014ffb 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,7 +1,5 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/array/wrap' - module ActiveModel # == Active Model Serialization @@ -11,7 +9,6 @@ module ActiveModel # A minimal implementation could be: # # class Person - # # include ActiveModel::Serialization # # attr_accessor :name @@ -19,7 +16,6 @@ module ActiveModel # def attributes # {'name' => nil} # end - # # end # # Which would provide you with: @@ -29,21 +25,20 @@ module ActiveModel # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # - # You need to declare an attributes hash which contains the attributes - # you want to serialize. When called, serializable hash will use - # instance methods that match the name of the attributes hash's keys. - # In order to override this behavior, take a look at the private - # method read_attribute_for_serialization. + # You need to declare an attributes hash which contains the attributes you + # want to serialize. Attributes must be strings, not symbols. When called, + # serializable hash will use instance methods that match the name of the + # attributes hash's keys. In order to override this behavior, take a look at + # the private method +read_attribute_for_serialization+. # # Most of the time though, you will want to include the JSON or XML # serializations. Both of these modules automatically include the - # ActiveModel::Serialization module, so there is no need to explicitly - # include it. + # <tt>ActiveModel::Serialization</tt> module, so there is no need to + # explicitly include it. # - # So a minimal implementation including XML and JSON would be: + # A minimal implementation including XML and JSON would be: # # class Person - # # include ActiveModel::Serializers::JSON # include ActiveModel::Serializers::Xml # @@ -52,7 +47,6 @@ module ActiveModel # def attributes # {'name' => nil} # end - # # end # # Which would provide you with: @@ -69,12 +63,41 @@ module ActiveModel # person.to_json # => "{\"name\":\"Bob\"}" # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # - # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> . + # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and + # <tt>:include</tt>. The following are all valid examples: + # + # person.serializable_hash(only: 'name') + # person.serializable_hash(include: :address) + # person.serializable_hash(include: { address: { only: 'city' }}) module Serialization + # Returns a serialized hash of your object. + # + # class Person + # include ActiveModel::Serialization + # + # attr_accessor :name, :age + # + # def attributes + # {'name' => nil, 'age' => nil} + # end + # + # def capitalized_name + # name.capitalize + # end + # end + # + # person = Person.new + # person.name = 'bob' + # person.age = 22 + # person.serializable_hash # => {"name"=>"bob", "age"=>22} + # person.serializable_hash(only: :name) # => {"name"=>"bob"} + # person.serializable_hash(except: :name) # => {"age"=>22} + # person.serializable_hash(methods: :capitalized_name) + # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"} def serializable_hash(options = nil) options ||= {} - attribute_names = attributes.keys.sort + attribute_names = attributes.keys if only = options[:only] attribute_names &= Array(only).map(&:to_s) elsif except = options[:except] @@ -84,12 +107,11 @@ module ActiveModel hash = {} attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - method_names = Array(options[:methods]).select { |n| respond_to?(n) } - method_names.each { |n| hash[n.to_s] = send(n) } + Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } serializable_add_includes(options) do |association, records, opts| - hash[association] = if records.is_a?(Enumerable) - records.map { |a| a.serializable_hash(opts) } + hash[association.to_s] = if records.respond_to?(:to_ary) + records.to_ary.map { |a| a.serializable_hash(opts) } else records.serializable_hash(opts) end @@ -116,7 +138,6 @@ module ActiveModel # @data[key] # end # end - # alias :read_attribute_for_serialization :send # Add associations specified via the <tt>:include</tt> option. @@ -126,13 +147,13 @@ module ActiveModel # +records+ - the association record(s) to be serialized # +opts+ - options for the association records def serializable_add_includes(options = {}) #:nodoc: - return unless include = options[:include] + return unless includes = options[:include] - unless include.is_a?(Hash) - include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] + unless includes.is_a?(Hash) + includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] end - include.each do |association, opts| + includes.each do |association, opts| if records = send(association) yield association, records, opts end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 63ab8e7edc..a4252b995d 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -1,5 +1,4 @@ require 'active_support/json' -require 'active_support/core_ext/class/attribute' module ActiveModel # == Active Model JSON Serializer @@ -10,86 +9,89 @@ module ActiveModel included do extend ActiveModel::Naming - extend ActiveModel::Configuration - config_attribute :include_root_in_json - self.include_root_in_json = true + class_attribute :include_root_in_json + self.include_root_in_json = false end # Returns a hash representing the model. Some configuration can be # passed through +options+. # # 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: + # of +as_json+. If +true+, +as_json+ will emit a single root node named + # after the object's type. The default value for <tt>include_root_in_json</tt> + # option is +false+. # # user = User.find(1) # user.as_json - # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} } + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true} + # + # ActiveRecord::Base.include_root_in_json = true # - # ActiveRecord::Base.include_root_in_json = false # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} + # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true } } # - # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in: + # This behavior can also be achieved by setting the <tt>:root</tt> option + # to +true+ as in: # # user = User.find(1) - # user.as_json(root: false) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The remainder of the examples in this section assume include_root_in_json is set to - # <tt>false</tt>. + # user.as_json(root: true) + # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true } } # # Without any +options+, the returned Hash will include all the model's - # attributes. For example: + # attributes. # # user = User.find(1) # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} + # # => { "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: + # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit + # the attributes included, and work similar to the +attributes+ method. # - # user.as_json(:only => [ :id, :name ]) - # # => {"id": 1, "name": "Konata Izumi"} + # user.as_json(only: [:id, :name]) + # # => { "id" => 1, "name" => "Konata Izumi" } # - # user.as_json(:except => [ :id, :created_at, :age ]) - # # => {"name": "Konata Izumi", "awesome": true} + # user.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>: # - # user.as_json(:methods => :permalink) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "permalink": "1-konata-izumi"} + # user.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>: # - # user.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"}, - # {"id": 2, author_id: 1, "title": "So I was thinking"}]} + # user.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" }, + # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] } # # Second level and higher order associations work as well: # - # user.as_json(:include => { :posts => { - # :include => { :comments => { - # :only => :body } }, - # :only => :title } }) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}], - # "title": "Welcome to the weblog"}, - # {"comments": [{"body": "Don't think too hard"}], - # "title": "So I was thinking"}]} + # user.as_json(include: { posts: { + # include: { comments: { + # only: :body } }, + # only: :title } }) + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true, + # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ], + # # "title" => "Welcome to the weblog" }, + # # { "comments" => [ { "body" => "Don't think too hard" } ], + # # "title" => "So I was thinking" } ] } def as_json(options = nil) - root = include_root_in_json - root = options[:root] if options.try(:key?, :root) + root = if options && options.key?(:root) + options[:root] + else + include_root_in_json + end + if root root = self.class.model_name.element if root == true { root => serializable_hash(options) } @@ -98,6 +100,40 @@ module ActiveModel end end + # Sets the model +attributes+ from a JSON string. Returns +self+. + # + # class Person + # include ActiveModel::Serializers::JSON + # + # attr_accessor :name, :age, :awesome + # + # def attributes=(hash) + # hash.each do |key, value| + # instance_variable_set("@#{key}", value) + # end + # end + # + # def attributes + # instance_values + # end + # end + # + # json = { name: 'bob', age: 22, awesome:true }.to_json + # person = Person.new + # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob"> + # person.name # => "bob" + # person.age # => 22 + # person.awesome # => true + # + # The default value for +include_root+ is +false+. You can change it to + # +true+ if the given JSON string includes a single root node. + # + # json = { person: { name: 'bob', age: 22, awesome:true } }.to_json + # person = Person.new + # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob"> + # person.name # => "bob" + # person.age # => 22 + # person.awesome # => true def from_json(json, include_root=include_root_in_json) hash = ActiveSupport::JSON.decode(json) hash = hash.values.first if include_root diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 5084298210..cf742d0569 100644..100755 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -110,12 +110,18 @@ module ActiveModel end end - # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. + # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. def add_associations(association, records, opts) merged_options = opts.merge(options.slice(:builder, :indent)) merged_options[:skip_instruct] = true - if records.is_a?(Enumerable) + [:skip_types, :dasherize, :camelize].each do |key| + merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil? + end + + if records.respond_to?(:to_ary) + records = records.to_ary + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) type = options[:skip_types] ? { } : {:type => "array"} association_name = association.to_s.singularize @@ -159,8 +165,8 @@ 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: + # Without any +options+, the returned XML string will include all the + # model's attributes. # # user = User.find(1) # user.to_xml @@ -170,21 +176,45 @@ module ActiveModel # <id type="integer">1</id> # <name>David</name> # <age type="integer">16</age> - # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> + # <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. + # 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. + # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt> def to_xml(options = {}, &block) Serializer.new(self, options).serialize(&block) end + # Sets the model +attributes+ from a JSON string. Returns +self+. + # + # class Person + # include ActiveModel::Serializers::Xml + # + # attr_accessor :name, :age, :awesome + # + # def attributes=(hash) + # hash.each do |key, value| + # instance_variable_set("@#{key}", value) + # end + # end + # + # def attributes + # instance_values + # end + # end + # + # xml = { name: 'bob', age: 22, awesome:true }.to_xml + # person = Person.new + # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob"> + # person.name # => "bob" + # person.age # => 22 + # person.awesome # => true def from_xml(xml) self.attributes = Hash.from_xml(xml).values.first self diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 02b7c54d61..7a86701f73 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/hash/reverse_merge' - module ActiveModel # == Active Model Translation @@ -43,19 +41,20 @@ module ActiveModel # # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) - defaults = [] - parts = attribute.to_s.split(".", 2) + options = { :count => 1 }.merge!(options) + parts = attribute.to_s.split(".") attribute = parts.pop - namespace = parts.pop + namespace = parts.join("/") unless parts.empty? + attributes_scope = "#{self.i18n_scope}.attributes" if namespace - lookup_ancestors.each do |klass| - defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" end - defaults << :"#{self.i18n_scope}.attributes.#{namespace}.#{attribute}" + defaults << :"#{attributes_scope}.#{namespace}.#{attribute}" else - lookup_ancestors.each do |klass| - defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}" + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}" end end @@ -63,7 +62,7 @@ module ActiveModel defaults << options.delete(:default) if options[:default] defaults << attribute.humanize - options.reverse_merge! :count => 1, :default => defaults + options[:default] = defaults I18n.translate(defaults.shift, options) end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 0e15155b85..4762f39044 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/except' require 'active_model/errors' @@ -33,12 +32,11 @@ module ActiveModel # person.first_name = 'zoolander' # person.valid? # => false # person.invalid? # => true - # person.errors # => #<Hash {:first_name=>["starts with z."]}> - # - # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+ method - # to your instances initialized with a new <tt>ActiveModel::Errors</tt> object, so - # there is no need for you to do this manually. + # person.errors.messages # => {:first_name=>["starts with z."]} # + # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+ + # method to your instances initialized with a new <tt>ActiveModel::Errors</tt> + # object, so there is no need for you to do this manually. module Validations extend ActiveSupport::Concern @@ -52,8 +50,7 @@ module ActiveModel attr_accessor :validation_context define_callbacks :validate, :scope => :name - extend ActiveModel::Configuration - config_attribute :_validators + class_attribute :_validators self._validators = Hash.new { |h,k| h[k] = [] } end @@ -65,27 +62,27 @@ module ActiveModel # # attr_accessor :first_name, :last_name # - # validates_each :first_name, :last_name do |record, attr, value| + # validates_each :first_name, :last_name, allow_blank: true do |record, attr, value| # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z # end # end # # Options: # * <tt>:on</tt> - Specifies the context where this validation is active - # (e.g. <tt>:on => :create</tt> or <tt>:on => :custom_validation_context</tt>) + # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>) # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine - # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, - # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, - # proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or - # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. + # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, + # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, + # proc or string should return or evaluate to a +true+ or +false+ value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, + # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a +true+ or +false+ + # value. def validates_each(*attr_names, &block) - options = attr_names.extract_options!.symbolize_keys - validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block + validates_with BlockValidator, _merge_attributes(attr_names), &block end # Adds a validation method or block to the class. This is useful when @@ -100,7 +97,7 @@ module ActiveModel # validate :must_be_friends # # def must_be_friends - # errors.add(: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 # @@ -114,7 +111,7 @@ module ActiveModel # end # # def must_be_friends - # errors.add(: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 # @@ -124,10 +121,24 @@ module ActiveModel # include ActiveModel::Validations # # validate do - # errors.add(: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 # + # Options: + # * <tt>:on</tt> - Specifies the context where this validation is active + # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>) + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. + # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, + # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, + # proc or string should return or evaluate to a +true+ or +false+ value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, + # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a +true+ or +false+ + # value. def validate(*args, &block) options = args.extract_options! if options.key?(:on) @@ -141,38 +152,121 @@ module ActiveModel # List all validators that are being used to validate the model using # +validates_with+ method. + # + # class Person + # include ActiveModel::Validations + # + # validates_with MyValidator + # validates_with OtherValidator, on: :create + # validates_with StrictValidator, strict: true + # end + # + # Person.validators + # # => [ + # # #<MyValidator:0x007fbff403e808 @options={}>, + # # #<OtherValidator:0x007fbff403d930 @options={:on=>:create}>, + # # #<StrictValidator:0x007fbff3204a30 @options={:strict=>true}> + # # ] def validators _validators.values.flatten.uniq end - # List all validators that being used to validate a specific attribute. + # List all validators that are being used to validate a specific attribute. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name , :age + # + # validates_presence_of :name + # validates_inclusion_of :age, in: 0..99 + # end + # + # Person.validators_on(:name) + # # => [ + # # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>, + # # #<ActiveModel::Validations::InclusionValidator:0x007fe603bb8780 @attributes=[:age], @options={:in=>0..99}> + # # ] def validators_on(*attributes) attributes.map do |attribute| _validators[attribute.to_sym] end.flatten end - # Check if method is an attribute method or not. + # Returns +true+ if +attribute+ is an attribute method, +false+ otherwise. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # end + # + # User.attribute_method?(:name) # => true + # User.attribute_method?(:age) # => false def attribute_method?(attribute) method_defined?(attribute) end # Copy validators on inheritance. - def inherited(base) + def inherited(base) #:nodoc: dup = _validators.dup base._validators = dup.each { |k, v| dup[k] = v.dup } super end end - # Returns the +Errors+ object that holds all information about attribute error messages. + # Clean the +Errors+ object if instance is duped. + def initialize_dup(other) #:nodoc: + @errors = nil + super + end + + # Returns the +Errors+ object that holds all information about attribute + # error messages. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name + # end + # + # person = Person.new + # person.valid? # => false + # person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={:name=>["can't be blank"]}> def errors @errors ||= Errors.new(self) end - # Runs all the specified validations and returns true if no errors were added - # otherwise false. Context can optionally be supplied to define which callbacks - # to test against (the context is defined on the validations using :on). + # Runs all the specified validations and returns +true+ if no errors were + # added otherwise +false+. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name + # end + # + # person = Person.new + # person.name = '' + # person.valid? # => false + # person.name = 'david' + # person.valid? # => true + # + # Context can optionally be supplied to define which callbacks to test + # against (the context is defined on the validations using <tt>:on</tt>). + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name, on: :new + # end + # + # person = Person.new + # person.valid? # => true + # person.valid?(:new) # => false def valid?(context = nil) current_context, self.validation_context = validation_context, context errors.clear @@ -181,8 +275,35 @@ module ActiveModel self.validation_context = current_context end - # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, - # false otherwise. + # Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were + # added, +false+ otherwise. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name + # end + # + # person = Person.new + # person.name = '' + # person.invalid? # => true + # person.name = 'david' + # person.invalid? # => false + # + # Context can optionally be supplied to define which callbacks to test + # against (the context is defined on the validations using <tt>:on</tt>). + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name, on: :new + # end + # + # person = Person.new + # person.invalid? # => false + # person.invalid?(:new) # => true def invalid?(context = nil) !valid?(context) end @@ -203,12 +324,11 @@ module ActiveModel # @data[key] # end # end - # alias :read_attribute_for_validation :send protected - def run_validations! + def run_validations! #:nodoc: run_callbacks :validate errors.empty? end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index e628c6f306..8d5ebf527f 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -2,9 +2,9 @@ module ActiveModel # == Active Model Acceptance Validator module Validations - class AcceptanceValidator < EachValidator + class AcceptanceValidator < EachValidator #:nodoc: def initialize(options) - super(options.reverse_merge(:allow_nil => true, :accept => "1")) + super({ :allow_nil => true, :accept => "1" }.merge!(options)) end def validate_each(record, attribute, value) @@ -23,11 +23,11 @@ module ActiveModel module HelperMethods # Encapsulates the pattern of wanting to validate the acceptance of a - # terms of service check box (or similar agreement). Example: + # terms of service check box (or similar agreement). # # class Person < ActiveRecord::Base # validates_acceptance_of :terms_of_service - # validates_acceptance_of :eula, :message => "must be abided" + # validates_acceptance_of :eula, message: "must be abided" # end # # If the database column does not exist, the +terms_of_service+ attribute @@ -37,29 +37,17 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <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). + # is +true+). # * <tt>:accept</tt> - Specifies value that is considered accepted. # The default value is a string "1", which makes it easy to relate to # an HTML checkbox. This should be set to +true+ if you are validating # 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 - # 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 => Proc.new { |user| user.signup_step <= 2 }</tt>). - # The method, proc or string should return or evaluate to a true or - # false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # + # There is also a list of default options supported by every validator: + # +:if+, +:unless+, +:on+ and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index c39c85e1af..bf3fe7ff04 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -2,23 +2,24 @@ require 'active_support/callbacks' module ActiveModel module Validations + # == Active Model Validation callbacks + # + # Provides an interface for any class to have +before_validation+ and + # +after_validation+ callbacks. + # + # First, include ActiveModel::Validations::Callbacks from the class you are + # creating: + # + # class MyModel + # include ActiveModel::Validations::Callbacks + # + # before_validation :do_stuff_before_validation + # after_validation :do_stuff_after_validation + # end + # + # Like other <tt>before_*</tt> callbacks if +before_validation+ returns + # +false+ then <tt>valid?</tt> will not be called. module Callbacks - # == Active Model Validation callbacks - # - # Provides an interface for any class to have <tt>before_validation</tt> and - # <tt>after_validation</tt> callbacks. - # - # First, extend ActiveModel::Callbacks from the class you are creating: - # - # class MyModel - # include ActiveModel::Validations::Callbacks - # - # before_validation :do_stuff_before_validation - # after_validation :do_stuff_after_validation - # end - # - # Like other before_* callbacks if <tt>before_validation</tt> returns false - # then <tt>valid?</tt> will not be called. extend ActiveSupport::Concern included do @@ -27,6 +28,30 @@ module ActiveModel end module ClassMethods + # Defines a callback that will get called right before validation + # happens. + # + # class Person + # include ActiveModel::Validations + # include ActiveModel::Validations::Callbacks + # + # attr_accessor :name + # + # validates_length_of :name, maximum: 6 + # + # before_validation :remove_whitespaces + # + # private + # + # def remove_whitespaces + # name.strip! + # end + # end + # + # person = Person.new + # person.name = ' bob ' + # person.valid? # => true + # person.name # => "bob" def before_validation(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] @@ -36,6 +61,33 @@ module ActiveModel set_callback(:validation, :before, *args, &block) end + # Defines a callback that will get called right after validation + # happens. + # + # class Person + # include ActiveModel::Validations + # include ActiveModel::Validations::Callbacks + # + # attr_accessor :name, :status + # + # validates_presence_of :name + # + # after_validation :set_status + # + # private + # + # def set_status + # self.status = errors.empty? + # end + # end + # + # person = Person.new + # person.name = '' + # person.valid? # => false + # person.status # => false + # person.name = 'bob' + # person.valid? # => true + # person.status # => true def after_validation(*args, &block) options = args.extract_options! options[:prepend] = true @@ -48,7 +100,7 @@ module ActiveModel protected # Overwrite run validations to include callbacks. - def run_validations! + def run_validations! #:nodoc: run_callbacks(:validation) { super } end end diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb new file mode 100644 index 0000000000..3d7067fbcb --- /dev/null +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -0,0 +1,41 @@ +require 'active_support/core_ext/range' + +module ActiveModel + module Validations + module Clusivity #:nodoc: + ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " << + "and must be supplied as the :in (or :within) option of the configuration hash" + + def check_validity! + unless delimiter.respond_to?(:include?) || delimiter.respond_to?(:call) || delimiter.respond_to?(:to_sym) + raise ArgumentError, ERROR_MESSAGE + end + end + + private + + def include?(record, value) + exclusions = if delimiter.respond_to?(:call) + delimiter.call(record) + elsif delimiter.respond_to?(:to_sym) + record.send(delimiter) + else + delimiter + end + + exclusions.send(inclusion_method(exclusions), value) + end + + def delimiter + @delimiter ||= options[:in] || options[:within] + end + + # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the + # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> + # uses the previous logic of comparing a value with the range endpoints. + def inclusion_method(enumerable) + enumerable.is_a?(Range) ? :cover? : :include? + end + end + end +end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index e8526303e2..baa034eca6 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -2,10 +2,11 @@ module ActiveModel # == Active Model Confirmation Validator module Validations - class ConfirmationValidator < EachValidator + class ConfirmationValidator < EachValidator #:nodoc: def validate_each(record, attribute, value) if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) - record.errors.add(attribute, :confirmation, options) + human_attribute_name = record.class.human_attribute_name(attribute) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(:attribute => human_attribute_name)) end end @@ -18,13 +19,13 @@ module ActiveModel module HelperMethods # Encapsulates the pattern of wanting to validate a password or email - # address field with a confirmation. For example: + # address field with a confirmation. # # Model: # class Person < ActiveRecord::Base # validates_confirmation_of :user_name, :password # validates_confirmation_of :email_address, - # :message => "should match confirmation" + # message: 'should match confirmation' # end # # View: @@ -37,29 +38,18 @@ module ActiveModel # attribute. # # NOTE: This check is performed only if +password_confirmation+ is not - # +nil+. To require confirmation, make sure - # to add a presence check for the confirmation attribute: + # +nil+. To require confirmation, make sure to add a presence check for + # the confirmation attribute: # - # validates_presence_of :password_confirmation, :if => :password_changed? + # validates_presence_of :password_confirmation, if: :password_changed? # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "doesn't match # confirmation"). - # * <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. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to - # determine if the validation should not occur (e.g. - # <tt>:unless => :skip_validation</tt>, or - # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # + # There is also a list of default options supported by every validator: + # +:if+, +:unless+, +:on+ and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 644cc814a7..3ec552c372 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,66 +1,48 @@ -require 'active_support/core_ext/range' +require "active_model/validations/clusivity" module ActiveModel # == Active Model Exclusion Validator module Validations - class ExclusionValidator < EachValidator - ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << - "and must be supplied as the :in option of the configuration hash" - - def check_validity! - unless [:include?, :call].any? { |method| options[:in].respond_to?(method) } - raise ArgumentError, ERROR_MESSAGE - end - end + class ExclusionValidator < EachValidator #:nodoc: + include Clusivity def validate_each(record, attribute, value) - delimiter = options[:in] - exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter - if exclusions.send(inclusion_method(exclusions), value) - record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value)) + if include?(record, value) + record.errors.add(attribute, :exclusion, options.except(:in, :within).merge!(:value => value)) end end - - private - - # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the - # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> - # uses the previous logic of comparing a value with the range endpoints. - def inclusion_method(enumerable) - enumerable.is_a?(Range) ? :cover? : :include? - end end module HelperMethods - # Validates that the value of the specified attribute is not in a particular enumerable object. + # Validates that the value of the specified attribute is not in a + # particular enumerable object. # # class Person < ActiveRecord::Base - # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" - # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" - # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %{value} is not allowed" - # validates_exclusion_of :password, :in => lambda { |p| [p.username, p.first_name] }, :message => "should not be the same as your username or first name" + # validates_exclusion_of :username, in: %w( admin superuser ), message: "You don't belong here" + # validates_exclusion_of :age, in: 30..60, message: 'This site is only for under 30 and over 60' + # validates_exclusion_of :format, in: %w( mov avi ), message: "extension %{value} is not allowed" + # validates_exclusion_of :password, in: ->(person) { [person.username, person.first_name] }, + # message: 'should not be the same as your username or first name' + # validates_exclusion_of :karma, in: :reserved_karmas # end # # Configuration options: - # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of. - # This can be supplied as a proc or lambda which returns an enumerable. 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 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. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # * <tt>:in</tt> - An enumerable object of items that the value shouldn't + # be part of. This can be supplied as a proc, lambda or symbol which returns an + # enumerable. If the enumerable is a range the test is performed with + # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> + # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. + # * <tt>:message</tt> - Specifies a custom error message (default is: "is + # reserved"). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the + # attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the + # attribute is blank(default is +false+). + # + # There is also a list of default options supported by every validator: + # +:if+, +:unless+, +:on+ and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index d3faa8c6a6..80150229a0 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -2,7 +2,7 @@ module ActiveModel # == Active Model Format Validator module Validations - class FormatValidator < EachValidator + class FormatValidator < EachValidator #:nodoc: def validate_each(record, attribute, value) if options[:with] regexp = option_call(record, :with) @@ -33,59 +33,81 @@ module ActiveModel record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value)) end + def regexp_using_multiline_anchors?(regexp) + regexp.source.start_with?("^") || + (regexp.source.end_with?("$") && !regexp.source.end_with?("\\$")) + end + def check_options_validity(options, name) option = options[name] if option && !option.is_a?(Regexp) && !option.respond_to?(:call) raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" + elsif option && option.is_a?(Regexp) && + regexp_using_multiline_anchors?(option) && options[:multiline] != true + raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \ + "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \ + ":multiline => true option?" end end end module HelperMethods - # Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided. - # You can require that the attribute matches the regular expression: + # Validates whether the value of the specified attribute is of the correct + # form, going by the regular expression provided.You can require that the + # attribute matches the regular expression: # # class Person < ActiveRecord::Base - # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create + # validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create # end # - # Alternatively, you can require that the specified attribute does _not_ match the regular expression: + # Alternatively, you can require that the specified attribute does _not_ + # match the regular expression: # # class Person < ActiveRecord::Base - # validates_format_of :email, :without => /NOSPAM/ + # validates_format_of :email, without: /NOSPAM/ # end # - # You can also provide a proc or lambda which will determine the regular expression that will be used to validate the attribute + # You can also provide a proc or lambda which will determine the regular + # expression that will be used to validate the attribute. # # class Person < ActiveRecord::Base # # Admin can have number as a first letter in their screen name - # validates_format_of :screen_name, :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i } + # validates_format_of :screen_name, + # with: ->(person) { person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\z/i : /\A[a-z][a-z0-9_\-]*\z/i } # end # - # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. + # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the + # string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. # - # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression - # or a proc or lambda, or else an exception will be raised. + # Due to frequent misuse of <tt>^</tt> and <tt>$</tt>, you need to pass + # the <tt>multiline: true</tt> option in case you use any of these two + # anchors in the provided regular expression. In most cases, you should be + # using <tt>\A</tt> and <tt>\z</tt>. + # + # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. + # In addition, both must be a regular expression or a proc or lambda, or + # else an exception will be raised. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation. - # This can be provided as a proc or lambda returning regular expression which will be called at runtime. - # * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation. - # This can be provided as a proc or lambda returning regular expression which will be called at runtime. - # * <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. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the + # attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the + # attribute is blank (default is +false+). + # * <tt>:with</tt> - Regular expression that if the attribute matches will + # result in a successful validation. This can be provided as a proc or + # lambda returning regular expression which will be called at runtime. + # * <tt>:without</tt> - Regular expression that if the attribute does not + # match will result in a successful validation. This can be provided as + # a proc or lambda returning regular expression which will be called at + # runtime. + # * <tt>:multiline</tt> - Set to true if your regular expression contains + # anchors that match the beginning or end of lines as opposed to the + # beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>. + # + # There is also a list of default options supported by every validator: + # +:if+, +:unless+, +:on+ and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 147e2ecb69..babc8982da 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,66 +1,47 @@ -require 'active_support/core_ext/range' +require "active_model/validations/clusivity" module ActiveModel # == Active Model Inclusion Validator module Validations - class InclusionValidator < EachValidator - ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << - "and must be supplied as the :in option of the configuration hash" - - def check_validity! - unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } - raise ArgumentError, ERROR_MESSAGE - end - end + class InclusionValidator < EachValidator #:nodoc: + include Clusivity def validate_each(record, attribute, value) - delimiter = options[:in] - exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter - unless exclusions.send(inclusion_method(exclusions), value) - record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) + unless include?(record, value) + record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(:value => value)) end end - - private - - # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the - # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> - # uses the previous logic of comparing a value with the range endpoints. - def inclusion_method(enumerable) - enumerable.is_a?(Range) ? :cover? : :include? - end end module HelperMethods - # Validates whether the value of the specified attribute is available in a particular enumerable object. + # Validates whether the value of the specified attribute is available in a + # particular enumerable object. # # class Person < ActiveRecord::Base - # validates_inclusion_of :gender, :in => %w( m f ) - # validates_inclusion_of :age, :in => 0..99 - # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %{value} is not included in the list" - # validates_inclusion_of :states, :in => lambda{ |person| STATES[person.country] } + # validates_inclusion_of :gender, in: %w( m f ) + # validates_inclusion_of :age, in: 0..99 + # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list" + # validates_inclusion_of :states, in: ->(person) { STATES[person.country] } + # validates_inclusion_of :karma, in: :available_karmas # end # # Configuration options: # * <tt>:in</tt> - An enumerable object of available items. This can be - # supplied as a proc or lambda which returns an enumerable. 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. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # supplied as a proc, lambda or symbol which returns an enumerable. If the + # enumerable is a range the test is performed with <tt>Range#cover?</tt>, + # otherwise with <tt>include?</tt>. + # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> + # * <tt>:message</tt> - Specifies a custom error message (default is: "is + # not included in the list"). + # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the + # attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the + # attribute is blank (default is +false+). + # + # There is also a list of default options supported by every validator: + # +:if+, +:unless+, +:on+ and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 037f8c2db8..e4a1f9e80a 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -2,7 +2,7 @@ module ActiveModel # == Active Model Length Validator module Validations - class LengthValidator < EachValidator + class LengthValidator < EachValidator #:nodoc: MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze @@ -36,12 +36,12 @@ module ActiveModel def validate_each(record, attribute, value) value = tokenize(value) value_length = value.respond_to?(:length) ? value.length : value.to_s.length - + errors_options = options.except(*RESERVED_OPTIONS) + CHECKS.each do |key, validity_check| next unless check_value = options[key] next if value_length.send(validity_check, check_value) - errors_options = options.except(*RESERVED_OPTIONS) errors_options[:count] = check_value default_message = options[MESSAGES[key]] @@ -62,45 +62,48 @@ module ActiveModel module HelperMethods - # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: + # Validates that the specified attribute matches the length restrictions + # supplied. Only one option can be used at a time: # # class Person < ActiveRecord::Base - # validates_length_of :first_name, :maximum => 30 - # validates_length_of :last_name, :maximum => 30, :message => "less than 30 if you don't mind" - # validates_length_of :fax, :in => 7..32, :allow_nil => true - # validates_length_of :phone, :in => 7..32, :allow_blank => true - # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" - # validates_length_of :zip_code, :minimum => 5, :too_short => "please enter at least 5 characters" - # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words.", :tokenizer => lambda { |str| str.scan(/\w+/) } + # validates_length_of :first_name, maximum: 30 + # validates_length_of :last_name, maximum: 30, message: "less than 30 if you don't mind" + # validates_length_of :fax, in: 7..32, allow_nil: true + # validates_length_of :phone, in: 7..32, allow_blank: true + # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name' + # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters' + # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me." + # validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.', + # tokenizer: ->(str) { str.scan(/\w+/) } # end # # Configuration options: # * <tt>:minimum</tt> - The minimum size of the attribute. # * <tt>:maximum</tt> - The maximum size of the attribute. # * <tt>:is</tt> - The exact size of the attribute. - # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute. - # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>. + # * <tt>:within</tt> - A range specifying the minimum and maximum size of + # the attribute. + # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>. # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. - # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %{count} characters)"). - # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %{count} characters)"). - # * <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. 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. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to - # count words as in above example.) - # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # * <tt>:too_long</tt> - The error message if the attribute goes over the + # maximum (default is: "is too long (maximum is %{count} characters)"). + # * <tt>:too_short</tt> - The error message if the attribute goes under the + # minimum (default is: "is too short (min is %{count} characters)"). + # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> + # method and the attribute is the wrong size (default is: "is the wrong + # length (should be %{count} characters)"). + # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, + # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate + # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. + # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. + # (e.g. <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> to count words + # as in above example). Defaults to <tt>->(value) { value.split(//) }</tt> + # which counts individual characters. + # + # There is also a list of default options supported by every validator: + # +:if+, +:unless+, +:on+ and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index bb9f9679fc..edebca94a8 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -2,7 +2,7 @@ module ActiveModel # == Active Model Numericality Validator module Validations - class NumericalityValidator < EachValidator + class NumericalityValidator < EachValidator #:nodoc: CHECKS = { :greater_than => :>, :greater_than_or_equal_to => :>=, :equal_to => :==, :less_than => :<, :less_than_or_equal_to => :<=, :odd => :odd?, :even => :even?, :other_than => :!= }.freeze @@ -79,50 +79,56 @@ module ActiveModel end module HelperMethods - # Validates whether the value of the specified attribute is numeric by trying to convert it to - # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression - # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true). + # Validates whether the value of the specified attribute is numeric by + # trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt> + # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt> + # (if <tt>only_integer</tt> is set to +true+). # # class Person < ActiveRecord::Base - # validates_numericality_of :value, :on => :create + # validates_numericality_of :value, on: :create # end # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is not a number"). - # * <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. - # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater than or equal the supplied value. - # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value. - # * <tt>:less_than</tt> - Specifies the value must be less than the supplied value. - # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value. - # * <tt>:other_than</tt> - Specifies the value must be other than the supplied value. + # * <tt>: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. + # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be + # greater than or equal the supplied value. + # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied + # value. + # * <tt>:less_than</tt> - Specifies the value must be less than the + # supplied value. + # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less + # than or equal the supplied value. + # * <tt>:other_than</tt> - Specifies the value must be other than the + # supplied value. # * <tt>:odd</tt> - Specifies the value must be an odd number. # * <tt>:even</tt> - Specifies the value must be an even number. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information # - # The following checks can also be supplied with a proc or a symbol which corresponds to a method: + # There is also a list of default options supported by every validator: + # +:if+, +:unless+, +:on+ and +:strict+ . + # See <tt>ActiveModel::Validation#validates</tt> for more information + # + # The following checks can also be supplied with a proc or a symbol which + # corresponds to a method: + # # * <tt>:greater_than</tt> # * <tt>:greater_than_or_equal_to</tt> # * <tt>:equal_to</tt> # * <tt>:less_than</tt> # * <tt>:less_than_or_equal_to</tt> # + # For example: + # # class Person < ActiveRecord::Base - # validates_numericality_of :width, :less_than => Proc.new { |person| person.height } - # validates_numericality_of :width, :greater_than => :minimum_weight + # validates_numericality_of :width, less_than: Proc.new { |person| person.height } + # validates_numericality_of :width, greater_than: :minimum_weight # end - # def validates_numericality_of(*attr_names) validates_with NumericalityValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 9a643a6f5c..f159e40858 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -1,17 +1,17 @@ -require 'active_support/core_ext/object/blank' module ActiveModel # == Active Model Presence Validator module Validations - class PresenceValidator < EachValidator + class PresenceValidator < EachValidator #:nodoc: def validate(record) record.errors.add_on_blank(attributes, options) end end module HelperMethods - # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example: + # Validates that the specified attributes are not blank (as defined by + # Object#blank?). Happens by default on save. # # class Person < ActiveRecord::Base # validates_presence_of :first_name @@ -19,25 +19,19 @@ module ActiveModel # # The first_name attribute must be in the object and it cannot be blank. # - # If you want to validate the presence of a boolean field (where the real values are true and false), - # you will want to use <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. + # If you want to validate the presence of a boolean field (where the real + # values are +true+ and +false+), you will want to use + # <tt>validates_inclusion_of :field_name, in: [true, false]</tt>. # - # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>. + # This is due to the way Object#blank? handles boolean values: + # <tt>false.blank? # => true</tt>. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). - # * <tt>: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. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information # + # There is also a list of default options supported by every validator: + # +:if+, +:unless+, +:on+ and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index d94c4e3f4f..03046a543a 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -11,18 +11,18 @@ module ActiveModel # # Examples of using the default rails validators: # - # validates :terms, :acceptance => true - # validates :password, :confirmation => true - # validates :username, :exclusion => { :in => %w(admin superuser) } - # validates :email, :format => { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create } - # validates :age, :inclusion => { :in => 0..9 } - # validates :first_name, :length => { :maximum => 30 } - # validates :age, :numericality => true - # validates :username, :presence => true - # validates :username, :uniqueness => true + # validates :terms, acceptance: true + # validates :password, confirmation: true + # validates :username, exclusion: { in: %w(admin superuser) } + # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, on: :create } + # validates :age, inclusion: { in: 0..9 } + # validates :first_name, length: { maximum: 30 } + # 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. + # and default validators in one call for a given attribute. # # class EmailValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) @@ -35,12 +35,12 @@ module ActiveModel # include ActiveModel::Validations # attr_accessor :name, :email # - # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } - # validates :email, :presence => true, :email => true + # 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. + # allowing custom modules of validators to be included as needed. # # class Film # include ActiveModel::Validations @@ -51,33 +51,53 @@ module ActiveModel # end # end # - # validates :name, :title => true + # validates :name, title: true # end # - # Additionally validator classes may be in another namespace and still used within any class. + # Additionally validator classes may be in another namespace and still + # used within any class. # # validates :name, :'film/title' => true # - # The validators hash can also handle regular expressions, ranges, - # arrays and strings in shortcut form, e.g. + # The validators hash can also handle regular expressions, ranges, arrays + # and strings in shortcut form. # - # validates :email, :format => /@/ - # validates :gender, :inclusion => %w(male female) - # validates :password, :length => 6..20 + # validates :email, format: /@/ + # validates :gender, inclusion: %w(male female) + # validates :password, length: 6..20 # # 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]+ + # validator's initializer as <tt>options[:in]</tt> while other types + # including regular expressions and strings are passed as <tt>options[:with]</tt>. # - # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ and +:strict+ - # can be given to one specific validator, as a hash: + # There is also a list of options that could be used along with validators: # - # validates :password, :presence => { :if => :password_required? }, :confirmation => true + # * <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. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, + # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a +true+ or + # +false+ value. + # * <tt>:strict</tt> - if the <tt>:strict</tt> option is set to true + # will raise ActiveModel::StrictValidationFailed instead of adding the error. + # <tt>:strict</tt> option can also be set to any other exception. # - # Or to all at the same time: + # Example: # - # validates :password, :presence => true, :confirmation => true, :if => :password_required? + # validates :password, presence: true, confirmation: true, if: :password_required? + # validates :token, uniqueness: true, strict: TokenGenerationException # + # + # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+ + # and +:message+ can be given to one specific validator, as a hash: + # + # validates :password, presence: { if: :password_required?, message: 'is forgotten.' }, confirmation: true def validates(*attributes) defaults = attributes.extract_options!.dup validations = defaults.slice!(*_validates_default_keys) @@ -88,6 +108,7 @@ module ActiveModel defaults.merge!(:attributes => attributes) validations.each do |key, options| + next unless options key = "#{key.to_s.camelize}Validator" begin @@ -104,8 +125,20 @@ module ActiveModel # users and are considered exceptional. So each validator defined with bang # or <tt>:strict</tt> option set to <tt>true</tt> will always raise # <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error - # when validation fails. - # See <tt>validates</tt> for more information about the validation itself. + # when validation fails. See <tt>validates</tt> for more information about + # the validation itself. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates! :name, presence: true + # end + # + # person = Person.new + # person.name = '' + # person.valid? + # # => ActiveModel::StrictValidationFailed: Name can't be blank def validates!(*attributes) options = attributes.extract_options! options[:strict] = true @@ -116,7 +149,7 @@ module ActiveModel # When creating custom validators, it might be useful to be able to specify # additional default keys. This can be done by overwriting this method. - def _validates_default_keys + def _validates_default_keys #:nodoc: [:if, :unless, :on, :allow_blank, :allow_nil , :strict] end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 72b8562b93..869591cd9e 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -3,12 +3,14 @@ module ActiveModel module HelperMethods private def _merge_attributes(attr_names) - options = attr_names.extract_options! - options.merge(:attributes => attr_names.flatten) + options = attr_names.extract_options!.symbolize_keys + attr_names.flatten! + options[:attributes] = attr_names + options end end - class WithValidator < EachValidator + class WithValidator < EachValidator #:nodoc: def validate_each(record, attr, val) method_name = options[:with] @@ -32,7 +34,7 @@ module ActiveModel # class MyValidator < ActiveModel::Validator # def validate(record) # if some_complex_logic - # record.errors.add :base, "This record is invalid" + # record.errors.add :base, 'This record is invalid' # end # end # @@ -46,30 +48,32 @@ module ActiveModel # # class Person # include ActiveModel::Validations - # validates_with MyValidator, MyOtherValidator, :on => :create + # validates_with MyValidator, MyOtherValidator, on: :create # end # # Configuration options: # * <tt>:on</tt> - Specifies when this validation is active - # (<tt>:create</tt> or <tt>:update</tt> + # (<tt>:create</tt> or <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. + # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, + # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). + # The method, proc or string should return or evaluate to a +true+ or + # +false+ value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to # determine if the validation should not occur - # (e.g. <tt>:unless => :skip_validation</tt>, or - # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information - + # (e.g. <tt>unless: :skip_validation</tt>, or + # <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). + # The method, proc or string should return or evaluate to a +true+ or + # +false+ value. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # # If you pass any additional configuration options, they will be passed - # to the class and available as <tt>options</tt>: + # to the class and available as +options+: # # class Person # include ActiveModel::Validations - # validates_with MyValidator, :my_custom_key => "my custom value" + # validates_with MyValidator, my_custom_key: 'my custom value' # end # # class MyValidator < ActiveModel::Validator @@ -77,7 +81,6 @@ module ActiveModel # options[:my_custom_key] # => "my custom value" # end # end - # def validates_with(*args, &block) options = args.extract_options! args.each do |klass| @@ -118,22 +121,21 @@ module ActiveModel # class Person # include ActiveModel::Validations # - # validate :instance_validations, :on => :create + # validate :instance_validations, on: :create # # def instance_validations # validates_with MyValidator, MyOtherValidator # end # end # - # Standard configuration options (:on, :if and :unless), which are - # available on the class version of validates_with, should instead be - # placed on the <tt>validates</tt> method as these are applied and tested - # in the callback + # Standard configuration options (<tt>:on</tt>, <tt>:if</tt> and + # <tt>:unless</tt>), which are available on the class version of + # +validates_with+, should instead be placed on the +validates+ method + # as these are applied and tested in the callback. # # If you pass any additional configuration options, they will be passed - # to the class and available as <tt>options</tt>, please refer to the - # class version of this method for more information - # + # to the class and available as +options+, please refer to the + # class version of this method for more information. def validates_with(*args, &block) options = args.extract_options! args.each do |klass| diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 2953126c3c..85aec00f25 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,8 +1,6 @@ require "active_support/core_ext/module/anonymous" -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/object/inclusion' -module ActiveModel #:nodoc: +module ActiveModel # == Active Model Validator # @@ -28,7 +26,7 @@ module ActiveModel #:nodoc: # end # # Any class that inherits from ActiveModel::Validator must implement a method - # called <tt>validate</tt> which accepts a <tt>record</tt>. + # called +validate+ which accepts a +record+. # # class Person # include ActiveModel::Validations @@ -42,8 +40,8 @@ module ActiveModel #:nodoc: # end # end # - # To cause a validation error, you must add to the <tt>record</tt>'s errors directly - # from within the validators message + # To cause a validation error, you must add to the +record+'s errors directly + # from within the validators message. # # class MyValidator < ActiveModel::Validator # def validate(record) @@ -63,16 +61,16 @@ module ActiveModel #:nodoc: # end # # The easiest way to add custom validators for validating individual attributes - # is with the convenient <tt>ActiveModel::EachValidator</tt>. For example: + # is with the convenient <tt>ActiveModel::EachValidator</tt>. # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) - # record.errors.add attribute, 'must be Mr. Mrs. or Dr.' unless value.in?(['Mr.', 'Mrs.', 'Dr.']) + # record.errors.add attribute, 'must be Mr., Mrs., or Dr.' unless %w(Mr. Mrs. Dr.).include?(value) # end # end # # This can now be used in combination with the +validates+ method - # (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this) + # (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this). # # class Person # include ActiveModel::Validations @@ -83,8 +81,7 @@ module ActiveModel #:nodoc: # # Validator may also define a +setup+ instance method which will get called # with the class that using that validator as its argument. This can be - # useful when there are prerequisites such as an +attr_accessor+ being present - # for example: + # useful when there are prerequisites such as an +attr_accessor+ being present. # # class MyValidator < ActiveModel::Validator # def setup(klass) @@ -94,15 +91,13 @@ 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. # # PresenceValidator.kind # => :presence # UniquenessValidator.kind # => :uniqueness - # def self.kind @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous? end @@ -113,6 +108,9 @@ module ActiveModel #:nodoc: end # Return the kind for this validator. + # + # PresenceValidator.new.kind # => :presence + # UniquenessValidator.new.kind # => :uniqueness def kind self.class.kind end @@ -129,7 +127,7 @@ module ActiveModel #:nodoc: # record, attribute and value. # # All Active Model validations are built on top of this validator. - class EachValidator < Validator + class EachValidator < Validator #:nodoc: attr_reader :attributes # Returns a new validator instance. All options will be available via the @@ -168,7 +166,7 @@ module ActiveModel #:nodoc: # +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 + class BlockValidator < EachValidator #:nodoc: def initialize(options, &block) @block = block super diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index 9406328d3e..baaf842222 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -10,7 +10,7 @@ class ModelWithAttributes end def attributes - { :foo => 'value of foo' } + { :foo => 'value of foo', :baz => 'value of baz' } end private @@ -76,6 +76,19 @@ private end end +class ModelWithRubyKeywordNamedAttributes + include ActiveModel::AttributeMethods + + def attributes + { :begin => 'value of begin', :end => 'value of end' } + end + +private + def attribute(name) + attributes[name.to_sym] + end +end + class ModelWithoutAttributesMethod include ActiveModel::AttributeMethods end @@ -127,29 +140,54 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b') end + test '#define_attribute_methods works passing multiple arguments' do + ModelWithAttributes.define_attribute_methods(:foo, :baz) + + assert_equal "value of foo", ModelWithAttributes.new.foo + assert_equal "value of baz", ModelWithAttributes.new.baz + end + test '#define_attribute_methods generates attribute methods' do - ModelWithAttributes.define_attribute_methods([:foo]) + ModelWithAttributes.define_attribute_methods(:foo) assert_respond_to ModelWithAttributes.new, :foo assert_equal "value of foo", ModelWithAttributes.new.foo end + test '#alias_attribute generates attribute_aliases lookup hash' do + klass = Class.new(ModelWithAttributes) do + define_attribute_methods :foo + alias_attribute :bar, :foo + end + + assert_equal({ "bar" => "foo" }, klass.attribute_aliases) + end + test '#define_attribute_methods generates attribute methods with spaces in their names' do - ModelWithAttributesWithSpaces.define_attribute_methods([:'foo bar']) + ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar' assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar') end test '#alias_attribute works with attributes with spaces in their names' do - ModelWithAttributesWithSpaces.define_attribute_methods([:'foo bar']) + ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar') assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar end + test '#alias_attribute works with attributes named as a ruby keyword' do + ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end]) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end) + + assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from + assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to + end + test '#undefine_attribute_methods removes attribute methods' do - ModelWithAttributes.define_attribute_methods([:foo]) + ModelWithAttributes.define_attribute_methods(:foo) ModelWithAttributes.undefine_attribute_methods assert !ModelWithAttributes.new.respond_to?(:foo) @@ -170,7 +208,7 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_deprecated { klass.attribute_method_suffix '' } assert_deprecated { klass.attribute_method_prefix '' } - klass.define_attribute_methods([:foo]) + klass.define_attribute_methods(:foo) assert_equal 'value of foo', klass.new.foo end @@ -188,6 +226,12 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_raises(NoMethodError) { m.protected_method } end + class ClassWithProtected + protected + def protected_method + end + end + test 'should not interfere with respond_to? if the attribute has a private/protected method' do m = ModelWithAttributes2.new m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' } @@ -195,9 +239,11 @@ class AttributeMethodsTest < ActiveModel::TestCase assert !m.respond_to?(:private_method) assert m.respond_to?(:private_method, true) + c = ClassWithProtected.new + # This is messed up, but it's how Ruby works at the moment. Apparently it will be changed # in the future. - assert m.respond_to?(:protected_method) + assert_equal c.respond_to?(:protected_method), m.respond_to?(:protected_method) assert m.respond_to?(:protected_method, true) end diff --git a/activemodel/test/cases/configuration_test.rb b/activemodel/test/cases/configuration_test.rb deleted file mode 100644 index a172fa26a3..0000000000 --- a/activemodel/test/cases/configuration_test.rb +++ /dev/null @@ -1,154 +0,0 @@ -require 'cases/helper' - -class ConfigurationOnModuleTest < ActiveModel::TestCase - def setup - @mod = mod = Module.new do - extend ActiveSupport::Concern - extend ActiveModel::Configuration - - config_attribute :omg - self.omg = "default" - - config_attribute :wtf, global: true - self.wtf = "default" - - config_attribute :boolean - - config_attribute :lol, instance_writer: true - end - - @klass = Class.new do - include mod - end - - @subklass = Class.new(@klass) - end - - test "default" do - assert_equal "default", @mod.omg - assert_equal "default", @klass.omg - assert_equal "default", @klass.new.omg - end - - test "setting" do - @mod.omg = "lol" - assert_equal "lol", @mod.omg - end - - test "setting on class including the module" do - @klass.omg = "lol" - assert_equal "lol", @klass.omg - assert_equal "lol", @klass.new.omg - assert_equal "default", @mod.omg - end - - test "setting on subclass of class including the module" do - @subklass.omg = "lol" - assert_equal "lol", @subklass.omg - assert_equal "default", @klass.omg - assert_equal "default", @mod.omg - end - - test "setting on instance" do - assert !@klass.new.respond_to?(:omg=) - - @klass.lol = "lol" - obj = @klass.new - assert_equal "lol", obj.lol - obj.lol = "omg" - assert_equal "omg", obj.lol - assert_equal "lol", @klass.lol - assert_equal "lol", @klass.new.lol - obj.lol = false - assert !obj.lol? - end - - test "global attribute" do - assert_equal "default", @mod.wtf - assert_equal "default", @klass.wtf - - @mod.wtf = "wtf" - - assert_equal "wtf", @mod.wtf - assert_equal "wtf", @klass.wtf - - @klass.wtf = "lol" - - assert_equal "lol", @mod.wtf - assert_equal "lol", @klass.wtf - end - - test "boolean" do - assert_equal false, @mod.boolean? - assert_equal false, @klass.new.boolean? - @mod.boolean = true - assert_equal true, @mod.boolean? - assert_equal true, @klass.new.boolean? - end -end - -class ConfigurationOnClassTest < ActiveModel::TestCase - def setup - @klass = Class.new do - extend ActiveModel::Configuration - - config_attribute :omg - self.omg = "default" - - config_attribute :wtf, global: true - self.wtf = "default" - - config_attribute :omg2, instance_writer: true - config_attribute :wtf2, instance_writer: true, global: true - end - - @subklass = Class.new(@klass) - end - - test "defaults" do - assert_equal "default", @klass.omg - assert_equal "default", @klass.wtf - assert_equal "default", @subklass.omg - assert_equal "default", @subklass.wtf - end - - test "changing" do - @klass.omg = "lol" - assert_equal "lol", @klass.omg - assert_equal "lol", @subklass.omg - end - - test "changing in subclass" do - @subklass.omg = "lol" - assert_equal "lol", @subklass.omg - assert_equal "default", @klass.omg - end - - test "changing global" do - @klass.wtf = "wtf" - assert_equal "wtf", @klass.wtf - assert_equal "wtf", @subklass.wtf - - @subklass.wtf = "lol" - assert_equal "lol", @klass.wtf - assert_equal "lol", @subklass.wtf - end - - test "instance_writer" do - obj = @klass.new - - @klass.omg2 = "omg" - @klass.wtf2 = "wtf" - - assert_equal "omg", obj.omg2 - assert_equal "wtf", obj.wtf2 - - obj.omg2 = "lol" - obj.wtf2 = "lol" - - assert_equal "lol", obj.omg2 - assert_equal "lol", obj.wtf2 - assert_equal "omg", @klass.omg2 - assert_equal "lol", @klass.wtf2 - end -end diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index 24552bcaf2..d679ad41aa 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -24,7 +24,7 @@ class ConversionTest < ActiveModel::TestCase assert_equal "1", Contact.new(:id => 1).to_param end - test "to_path default implementation returns a string giving a relative path" do + test "to_partial_path default implementation returns a string giving a relative path" do assert_equal "contacts/contact", Contact.new.to_partial_path assert_equal "helicopters/helicopter", Helicopter.new.to_partial_path, "ActiveModel::Conversion#to_partial_path caching should be class-specific" diff --git a/activemodel/test/cases/deprecated_mass_assignment_security_test.rb b/activemodel/test/cases/deprecated_mass_assignment_security_test.rb new file mode 100644 index 0000000000..c1fe8822cd --- /dev/null +++ b/activemodel/test/cases/deprecated_mass_assignment_security_test.rb @@ -0,0 +1,16 @@ +require 'cases/helper' +require 'models/project' + +class DeprecatedMassAssignmentSecurityTest < ActiveModel::TestCase + def test_attr_accessible_raise_error + assert_raise RuntimeError, /protected_attributes/ do + Project.attr_accessible :username + end + end + + def test_attr_protected_raise_error + assert_raise RuntimeError, /protected_attributes/ do + Project.attr_protected :username + end + end +end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 98244a6290..eaaf910bac 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -3,7 +3,7 @@ require "cases/helper" class DirtyTest < ActiveModel::TestCase class DirtyModel include ActiveModel::Dirty - define_attribute_methods [:name, :color] + define_attribute_methods :name, :color def initialize @name = nil diff --git a/activemodel/test/cases/forbidden_attributes_protection_test.rb b/activemodel/test/cases/forbidden_attributes_protection_test.rb new file mode 100644 index 0000000000..3cb204a2c5 --- /dev/null +++ b/activemodel/test/cases/forbidden_attributes_protection_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' +require 'active_support/core_ext/hash/indifferent_access' +require 'models/account' + +class ProtectedParams < ActiveSupport::HashWithIndifferentAccess + attr_accessor :permitted + alias :permitted? :permitted + + def initialize(attributes) + super(attributes) + @permitted = false + end + + def permit! + @permitted = true + self + end +end + +class ActiveModelMassUpdateProtectionTest < ActiveSupport::TestCase + test "forbidden attributes cannot be used for mass updating" do + params = ProtectedParams.new({ "a" => "b" }) + assert_raises(ActiveModel::ForbiddenAttributesError) do + Account.new.sanitize_for_mass_assignment(params) + end + end + + test "permitted attributes can be used for mass updating" do + params = ProtectedParams.new({ "a" => "b" }).permit! + assert_equal({ "a" => "b" }, Account.new.sanitize_for_mass_assignment(params)) + end + + test "regular attributes should still be allowed" do + assert_equal({ a: "b" }, Account.new.sanitize_for_mass_assignment(a: "b")) + end +end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 4347b17cbc..7d6f11b5a5 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -1,8 +1,5 @@ require File.expand_path('../../../../load_paths', __FILE__) -lib = File.expand_path("#{File.dirname(__FILE__)}/../../lib") -$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) - require 'config' require 'active_model' require 'active_support/core_ext/string/access' diff --git a/activemodel/test/cases/mass_assignment_security/black_list_test.rb b/activemodel/test/cases/mass_assignment_security/black_list_test.rb deleted file mode 100644 index 0ec7f8719c..0000000000 --- a/activemodel/test/cases/mass_assignment_security/black_list_test.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "cases/helper" - -class BlackListTest < ActiveModel::TestCase - - def setup - @black_list = ActiveModel::MassAssignmentSecurity::BlackList.new - @included_key = 'admin' - @black_list += [ @included_key ] - end - - test "deny? is true for included items" do - assert_equal true, @black_list.deny?(@included_key) - end - - test "deny? is false for non-included items" do - assert_equal false, @black_list.deny?('first_name') - end - - -end diff --git a/activemodel/test/cases/mass_assignment_security/permission_set_test.rb b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb deleted file mode 100644 index d005b638e4..0000000000 --- a/activemodel/test/cases/mass_assignment_security/permission_set_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "cases/helper" - -class PermissionSetTest < ActiveModel::TestCase - - def setup - @permission_list = ActiveModel::MassAssignmentSecurity::PermissionSet.new - end - - test "+ stringifies added collection values" do - symbol_collection = [ :admin ] - new_list = @permission_list += symbol_collection - - assert new_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}" - end - - test "include? normalizes multi-parameter keys" do - multi_param_key = 'admin(1)' - new_list = @permission_list += [ 'admin' ] - - assert new_list.include?(multi_param_key), "#{multi_param_key} not found in #{@permission_list.inspect}" - end - - test "include? normal keys" do - normal_key = 'admin' - new_list = @permission_list += [ normal_key ] - - assert new_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}" - end - -end diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb deleted file mode 100644 index 3660b9b1e5..0000000000 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "cases/helper" -require 'active_support/logger' -require 'active_support/core_ext/object/inclusion' - -class SanitizerTest < ActiveModel::TestCase - attr_accessor :logger - - class Authorizer < ActiveModel::MassAssignmentSecurity::PermissionSet - def deny?(key) - ['admin', 'id'].include?(key) - end - end - - def setup - @logger_sanitizer = ActiveModel::MassAssignmentSecurity::LoggerSanitizer.new(self) - @strict_sanitizer = ActiveModel::MassAssignmentSecurity::StrictSanitizer.new(self) - @authorizer = Authorizer.new - end - - test "sanitize attributes" do - original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } - attributes = @logger_sanitizer.sanitize(original_attributes, @authorizer) - - assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" - assert !attributes.key?('admin'), "Denied key should be rejected" - end - - test "debug mass assignment removal with LoggerSanitizer" do - original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } - log = StringIO.new - self.logger = ActiveSupport::Logger.new(log) - @logger_sanitizer.sanitize(original_attributes, @authorizer) - assert_match(/admin/, log.string, "Should log removed attributes: #{log.string}") - end - - test "debug mass assignment removal with StrictSanitizer" do - original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } - assert_raise ActiveModel::MassAssignmentSecurity::Error do - @strict_sanitizer.sanitize(original_attributes, @authorizer) - end - end - - test "mass assignment insensitive attributes" do - original_attributes = {'id' => 1, 'first_name' => 'allowed'} - - assert_nothing_raised do - @strict_sanitizer.sanitize(original_attributes, @authorizer) - end - end - -end diff --git a/activemodel/test/cases/mass_assignment_security/white_list_test.rb b/activemodel/test/cases/mass_assignment_security/white_list_test.rb deleted file mode 100644 index 737b55492a..0000000000 --- a/activemodel/test/cases/mass_assignment_security/white_list_test.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "cases/helper" - -class WhiteListTest < ActiveModel::TestCase - - def setup - @white_list = ActiveModel::MassAssignmentSecurity::WhiteList.new - @included_key = 'first_name' - @white_list += [ @included_key ] - end - - test "deny? is false for included items" do - assert_equal false, @white_list.deny?(@included_key) - end - - test "deny? is true for non-included items" do - assert_equal true, @white_list.deny?('admin') - end - -end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb deleted file mode 100644 index a197dbe748..0000000000 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ /dev/null @@ -1,120 +0,0 @@ -require "cases/helper" -require 'models/mass_assignment_specific' - - -class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer - - def process_removed_attributes(attrs) - raise StandardError - end - -end - -class MassAssignmentSecurityTest < ActiveModel::TestCase - - def test_attribute_protection - user = User.new - expected = { "name" => "John Smith", "email" => "john@smith.com" } - sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) - assert_equal expected, sanitized - end - - def test_attribute_protection_when_role_is_nil - user = User.new - expected = { "name" => "John Smith", "email" => "john@smith.com" } - sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), nil) - assert_equal expected, sanitized - end - - def test_only_moderator_role_attribute_accessible - user = SpecialUser.new - expected = { "name" => "John Smith", "email" => "john@smith.com" } - sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), :moderator) - assert_equal expected, sanitized - - sanitized = user.sanitize_for_mass_assignment({ "name" => "John Smith", "email" => "john@smith.com", "admin" => true }) - assert_equal({}, sanitized) - end - - def test_attributes_accessible - user = Person.new - expected = { "name" => "John Smith", "email" => "john@smith.com" } - sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) - assert_equal expected, sanitized - end - - def test_attributes_accessible_with_admin_role - user = Person.new - expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true } - sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin) - assert_equal expected, sanitized - end - - def test_attributes_accessible_with_roles_given_as_array - user = Account.new - expected = { "name" => "John Smith", "email" => "john@smith.com" } - sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) - assert_equal expected, sanitized - end - - def test_attributes_accessible_with_admin_role_when_roles_given_as_array - user = Account.new - expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true } - sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin) - assert_equal expected, sanitized - end - - def test_attributes_protected_by_default - firm = Firm.new - expected = { } - sanitized = firm.sanitize_for_mass_assignment({ "type" => "Client" }) - assert_equal expected, sanitized - end - - def test_mass_assignment_protection_inheritance - assert_blank LoosePerson.accessible_attributes - assert_equal Set.new(['credit_rating', 'administrator']), LoosePerson.protected_attributes - - assert_blank LoosePerson.accessible_attributes - assert_equal Set.new(['credit_rating']), LoosePerson.protected_attributes(:admin) - - assert_blank LooseDescendant.accessible_attributes - assert_equal Set.new(['credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes - - assert_blank LooseDescendantSecond.accessible_attributes - assert_equal Set.new(['credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, - 'Running attr_protected twice in one class should merge the protections' - - assert_blank TightPerson.protected_attributes - TightPerson.attributes_protected_by_default - assert_equal Set.new(['name', 'address']), TightPerson.accessible_attributes - - assert_blank TightPerson.protected_attributes(:admin) - TightPerson.attributes_protected_by_default - assert_equal Set.new(['name', 'address', 'admin']), TightPerson.accessible_attributes(:admin) - - assert_blank TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default - assert_equal Set.new(['name', 'address', 'phone_number']), TightDescendant.accessible_attributes - - assert_blank TightDescendant.protected_attributes(:admin) - TightDescendant.attributes_protected_by_default - assert_equal Set.new(['name', 'address', 'admin', 'super_powers']), TightDescendant.accessible_attributes(:admin) - - end - - def test_mass_assignment_multiparameter_protector - task = Task.new - attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" } - sanitized = task.sanitize_for_mass_assignment(attributes) - assert_equal sanitized, { } - end - - def test_custom_sanitizer - user = User.new - User.mass_assignment_sanitizer = CustomSanitizer.new - assert_raise StandardError do - user.sanitize_for_mass_assignment("admin" => true) - end - ensure - User.mass_assignment_sanitizer = nil - - end - -end diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb new file mode 100644 index 0000000000..d93fd96b88 --- /dev/null +++ b/activemodel/test/cases/model_test.rb @@ -0,0 +1,26 @@ +require 'cases/helper' + +class ModelTest < ActiveModel::TestCase + include ActiveModel::Lint::Tests + + class BasicModel + include ActiveModel::Model + attr_accessor :attr + end + + def setup + @model = BasicModel.new + end + + def test_initialize_with_params + object = BasicModel.new(:attr => "value") + assert_equal object.attr, "value" + end + + def test_initialize_with_nil_or_empty_hash_params_does_not_explode + assert_nothing_raised do + BasicModel.new() + BasicModel.new({}) + end + end +end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index 1e14d83bcb..49d8706ac2 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -25,12 +25,6 @@ class NamingTest < ActiveModel::TestCase assert_equal 'post/track_backs', @model_name.collection end - def test_partial_path - assert_deprecated(/#partial_path.*#to_partial_path/) do - assert_equal 'post/track_backs/track_back', @model_name.partial_path - end - end - def test_human assert_equal 'Track back', @model_name.human end @@ -61,12 +55,6 @@ class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase assert_equal 'blog/posts', @model_name.collection end - def test_partial_path - assert_deprecated(/#partial_path.*#to_partial_path/) do - assert_equal 'blog/posts/post', @model_name.partial_path - end - end - def test_human assert_equal 'Post', @model_name.human end @@ -105,12 +93,6 @@ class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase assert_equal 'blog/posts', @model_name.collection end - def test_partial_path - assert_deprecated(/#partial_path.*#to_partial_path/) do - assert_equal 'blog/posts/post', @model_name.partial_path - end - end - def test_human assert_equal 'Post', @model_name.human end @@ -149,12 +131,6 @@ class NamingWithSuppliedModelNameTest < ActiveModel::TestCase assert_equal 'articles', @model_name.collection end - def test_partial_path - assert_deprecated(/#partial_path.*#to_partial_path/) do - assert_equal 'articles/article', @model_name.partial_path - end - end - def test_human assert_equal 'Article', @model_name.human end diff --git a/activemodel/test/cases/observing_test.rb b/activemodel/test/cases/observing_test.rb index f6ec24ae57..ade6026602 100644 --- a/activemodel/test/cases/observing_test.rb +++ b/activemodel/test/cases/observing_test.rb @@ -14,8 +14,8 @@ class FooObserver < ActiveModel::Observer attr_accessor :stub - def on_spec(record) - stub.event_with(record) if stub + def on_spec(record, *args) + stub.event_with(record, *args) if stub end def around_save(record) @@ -70,23 +70,38 @@ class ObservingTest < ActiveModel::TestCase ObservedModel.instantiate_observers end + test "raises an appropriate error when a developer accidentally adds the wrong class (i.e. Widget instead of WidgetObserver)" do + assert_raise ArgumentError do + ObservedModel.observers = ['string'] + ObservedModel.instantiate_observers + end + assert_raise ArgumentError do + ObservedModel.observers = [:string] + ObservedModel.instantiate_observers + end + assert_raise ArgumentError do + ObservedModel.observers = [String] + ObservedModel.instantiate_observers + end + end + test "passes observers to subclasses" do FooObserver.instance bar = Class.new(Foo) - assert_equal Foo.count_observers, bar.count_observers + assert_equal Foo.observers_count, bar.observers_count end end class ObserverTest < ActiveModel::TestCase def setup ObservedModel.observers = :foo_observer - FooObserver.instance_eval do + FooObserver.singleton_class.instance_eval do alias_method :original_observed_classes, :observed_classes end end def teardown - FooObserver.instance_eval do + FooObserver.singleton_class.instance_eval do undef_method :observed_classes alias_method :observed_classes, :original_observed_classes end @@ -98,44 +113,51 @@ class ObserverTest < ActiveModel::TestCase test "tracks implicit observable models" do instance = FooObserver.new - assert instance.send(:observed_classes).include?(Foo), "Foo not in #{instance.send(:observed_classes).inspect}" - assert !instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{instance.send(:observed_classes).inspect}" + assert_equal [Foo], instance.observed_classes end test "tracks explicit observed model class" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe ObservedModel instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "tracks explicit observed model as string" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe 'observed_model' instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "tracks explicit observed model as symbol" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe :observed_model instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "calls existing observer event" do foo = Foo.new FooObserver.instance.stub = stub FooObserver.instance.stub.expects(:event_with).with(foo) - Foo.send(:notify_observers, :on_spec, foo) + Foo.notify_observers(:on_spec, foo) + end + + test "calls existing observer event from the instance" do + foo = Foo.new + FooObserver.instance.stub = stub + FooObserver.instance.stub.expects(:event_with).with(foo) + foo.notify_observers(:on_spec) + end + + test "passes extra arguments" do + foo = Foo.new + FooObserver.instance.stub = stub + FooObserver.instance.stub.expects(:event_with).with(foo, :bar) + Foo.send(:notify_observers, :on_spec, foo, :bar) end test "skips nonexistent observer event" do foo = Foo.new - Foo.send(:notify_observers, :whatever, foo) + Foo.notify_observers(:whatever, foo) end test "update passes a block on to the observer" do @@ -145,4 +167,15 @@ class ObserverTest < ActiveModel::TestCase end assert_equal :in_around_save, yielded_value end + + test "observe redefines observed_classes class method" do + class BarObserver < ActiveModel::Observer + observe :foo + end + + assert_equal [Foo], BarObserver.observed_classes + + BarObserver.observe(ObservedModel) + assert_equal [ObservedModel], BarObserver.observed_classes + end end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 4338a3fc53..19e74d3cc9 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/user' +require 'models/oauthed_user' require 'models/visitor' require 'models/administrator' @@ -7,28 +8,39 @@ class SecurePasswordTest < ActiveModel::TestCase setup do @user = User.new + @visitor = Visitor.new + @oauthed_user = OauthedUser.new end test "blank password" do - @user.password = '' - assert !@user.valid?, 'user should be invalid' + @user.password = @visitor.password = '' + assert !@user.valid?(:create), 'user should be invalid' + assert @visitor.valid?(:create), 'visitor should be valid' end test "nil password" do - @user.password = nil - assert !@user.valid?, 'user should be invalid' + @user.password = @visitor.password = nil + assert !@user.valid?(:create), 'user should be invalid' + assert @visitor.valid?(:create), 'visitor should be valid' + end + + test "blank password doesn't override previous password" do + @user.password = 'test' + @user.password = '' + assert_equal @user.password, 'test' end test "password must be present" do - assert !@user.valid? + assert !@user.valid?(:create) assert_equal 1, @user.errors.size end - test "password must match confirmation" do - @user.password = "thiswillberight" - @user.password_confirmation = "wrong" + test "match confirmation" do + @user.password = @visitor.password = "thiswillberight" + @user.password_confirmation = @visitor.password_confirmation = "wrong" assert !@user.valid? + assert @visitor.valid? @user.password_confirmation = "thiswillberight" @@ -42,15 +54,19 @@ class SecurePasswordTest < ActiveModel::TestCase assert @user.authenticate("secret") end - test "visitor#password_digest should be protected against mass assignment" do - assert Visitor.active_authorizers[:default].kind_of?(ActiveModel::MassAssignmentSecurity::BlackList) - assert Visitor.active_authorizers[:default].include?(:password_digest) + test "User should not be created with blank digest" do + assert_raise RuntimeError do + @user.run_callbacks :create + end + @user.password = "supersecretpassword" + assert_nothing_raised do + @user.run_callbacks :create + end end - - test "Administrator's mass_assignment_authorizer should be WhiteList" do - active_authorizer = Administrator.active_authorizers[:default] - assert active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList) - assert !active_authorizer.include?(:password_digest) - assert active_authorizer.include?(:name) + + test "Oauthed user can be created with blank digest" do + assert_nothing_raised do + @oauthed_user.run_callbacks :create + end end end diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 3b201a70f5..d2ba9fd95d 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -88,62 +88,80 @@ class SerializationTest < ActiveModel::TestCase def test_include_option_with_singular_association expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com", - :address=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}} + "address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}} assert_equal expected, @user.serializable_hash(:include => :address) end def test_include_option_with_plural_association expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} assert_equal expected, @user.serializable_hash(:include => :friends) end def test_include_option_with_empty_association @user.friends = [] - expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", :friends=>[]} + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", "friends"=>[]} + assert_equal expected, @user.serializable_hash(:include => :friends) + end + + class FriendList + def initialize(friends) + @friends = friends + end + + def to_ary + @friends + end + end + + def test_include_option_with_ary + @user.friends = FriendList.new(@user.friends) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} assert_equal expected, @user.serializable_hash(:include => :friends) end def test_multiple_includes expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :address=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}, - :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + "address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}, + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} assert_equal expected, @user.serializable_hash(:include => [:address, :friends]) end def test_include_with_options expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :address=>{"street"=>"123 Lane"}} + "address"=>{"street"=>"123 Lane"}} assert_equal expected, @user.serializable_hash(:include => {:address => {:only => "street"}}) end def test_nested_include @user.friends.first.friends = [@user] expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male', - :friends => [{"email"=>"david@example.com", "gender"=>"male", "name"=>"David"}]}, - {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', :friends => []}]} + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male', + "friends"=> [{"email"=>"david@example.com", "gender"=>"male", "name"=>"David"}]}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', "friends"=> []}]} assert_equal expected, @user.serializable_hash(:include => {:friends => {:include => :friends}}) end def test_only_include - expected = {"name"=>"David", :friends => [{"name" => "Joe"}, {"name" => "Sue"}]} + expected = {"name"=>"David", "friends" => [{"name" => "Joe"}, {"name" => "Sue"}]} assert_equal expected, @user.serializable_hash(:only => :name, :include => {:friends => {:only => :name}}) end def test_except_include expected = {"name"=>"David", "email"=>"david@example.com", - :friends => [{"name" => 'Joe', "email" => 'joe@example.com'}, + "friends"=> [{"name" => 'Joe', "email" => 'joe@example.com'}, {"name" => "Sue", "email" => 'sue@example.com'}]} assert_equal expected, @user.serializable_hash(:except => :gender, :include => {:friends => {:except => :gender}}) end def test_multiple_includes_with_options expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :address=>{"street"=>"123 Lane"}, - :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + "address"=>{"street"=>"123 Lane"}, + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} assert_equal expected, @user.serializable_hash(:include => [{:address => {:only => "street"}}, :friends]) end diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 7160635eb4..e2690f1827 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -31,10 +31,15 @@ class JsonSerializationTest < ActiveModel::TestCase @contact.preferences = { 'shows' => 'anime' } end - test "should include root in json" do + def teardown + # set to the default value + Contact.include_root_in_json = false + end + + test "should not include root in json (class method)" do json = @contact.to_json - assert_match %r{^\{"contact":\{}, json + assert_no_match %r{^\{"contact":\{}, json assert_match %r{"name":"Konata Izumi"}, json assert_match %r{"age":16}, json assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) @@ -42,41 +47,31 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"preferences":\{"shows":"anime"\}}, json end - test "should not include root in json (class method)" do - begin - Contact.include_root_in_json = false - json = @contact.to_json - - assert_no_match %r{^\{"contact":\{}, json - assert_match %r{"name":"Konata Izumi"}, json - assert_match %r{"age":16}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) - assert_match %r{"awesome":true}, json - assert_match %r{"preferences":\{"shows":"anime"\}}, json - ensure - Contact.include_root_in_json = true - end + test "should include root in json if include_root_in_json is true" do + Contact.include_root_in_json = true + json = @contact.to_json + + assert_match %r{^\{"contact":\{}, json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"age":16}, json + assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_match %r{"awesome":true}, json + assert_match %r{"preferences":\{"shows":"anime"\}}, json end test "should include root in json (option) even if the default is set to false" do - begin - Contact.include_root_in_json = false - json = @contact.to_json(:root => true) - assert_match %r{^\{"contact":\{}, json - ensure - Contact.include_root_in_json = true - end + json = @contact.to_json(root: true) + assert_match %r{^\{"contact":\{}, json end test "should not include root in json (option)" do - - json = @contact.to_json(:root => false) + json = @contact.to_json(root: false) assert_no_match %r{^\{"contact":\{}, json end test "should include custom root in json" do - json = @contact.to_json(:root => 'json_contact') + json = @contact.to_json(root: 'json_contact') assert_match %r{^\{"json_contact":\{}, json assert_match %r{"name":"Konata Izumi"}, json @@ -107,7 +102,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "should allow attribute filtering with except" do - json = @contact.to_json(:except => [:name, :age]) + json = @contact.to_json(except: [:name, :age]) assert_no_match %r{"name":"Konata Izumi"}, json assert_no_match %r{"age":16}, json @@ -122,10 +117,10 @@ class JsonSerializationTest < ActiveModel::TestCase def @contact.favorite_quote; "Constraints are liberating"; end # Single method. - assert_match %r{"label":"Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label) + assert_match %r{"label":"Has cheezburger"}, @contact.to_json(only: :name, methods: :label) # Both methods. - methods_json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote]) + methods_json = @contact.to_json(only: :name, methods: [:label, :favorite_quote]) assert_match %r{"label":"Has cheezburger"}, methods_json assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end @@ -143,14 +138,15 @@ class JsonSerializationTest < ActiveModel::TestCase end test "serializable_hash should not modify options passed in argument" do - options = { :except => :name } + options = { except: :name } @contact.serializable_hash(options) assert_nil options[:only] assert_equal :name, options[:except] end - test "as_json should return a hash" do + test "as_json should return a hash if include_root_in_json is true" do + Contact.include_root_in_json = true json = @contact.as_json assert_kind_of Hash, json @@ -160,7 +156,7 @@ class JsonSerializationTest < ActiveModel::TestCase end end - test "from_json should set the object's attributes" do + test "from_json should work without a root (class attribute)" do json = @contact.to_json result = Contact.new.from_json(json) @@ -172,7 +168,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "from_json should work without a root (method parameter)" do - json = @contact.to_json(:root => false) + json = @contact.to_json result = Contact.new.from_json(json, false) assert_equal result.name, @contact.name @@ -182,24 +178,19 @@ class JsonSerializationTest < ActiveModel::TestCase assert_equal result.preferences, @contact.preferences end - test "from_json should work without a root (class attribute)" do - begin - Contact.include_root_in_json = false - json = @contact.to_json - result = Contact.new.from_json(json) - - assert_equal result.name, @contact.name - assert_equal result.age, @contact.age - assert_equal Time.parse(result.created_at), @contact.created_at - assert_equal result.awesome, @contact.awesome - assert_equal result.preferences, @contact.preferences - ensure - Contact.include_root_in_json = true - end + test "from_json should work with a root (method parameter)" do + json = @contact.to_json(root: :true) + result = Contact.new.from_json(json, true) + + assert_equal result.name, @contact.name + assert_equal result.age, @contact.age + assert_equal Time.parse(result.created_at), @contact.created_at + assert_equal result.awesome, @contact.awesome + assert_equal result.preferences, @contact.preferences end test "custom as_json should be honored when generating json" do - def @contact.as_json(options); { :name => name, :created_at => created_at }; end + def @contact.as_json(options); { name: name, created_at: created_at }; end json = @contact.to_json assert_match %r{"name":"Konata Izumi"}, json @@ -209,7 +200,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "custom as_json options should be extendible" do - def @contact.as_json(options = {}); super(options.merge(:only => [:name])); end + def @contact.as_json(options = {}); super(options.merge(only: [:name])); end json = @contact.to_json assert_match %r{"name":"Konata Izumi"}, json @@ -217,5 +208,4 @@ class JsonSerializationTest < ActiveModel::TestCase assert_no_match %r{"awesome":}, json assert_no_match %r{"preferences":}, json end - end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 38aecf51ff..e2bb0dda0b 100644..100755 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -28,7 +28,7 @@ class Address extend ActiveModel::Naming include ActiveModel::Serializers::Xml - attr_accessor :street, :city, :state, :zip + attr_accessor :street, :city, :state, :zip, :apt_number def attributes instance_values @@ -56,6 +56,7 @@ class XmlSerializationTest < ActiveModel::TestCase @contact.address.city = "Springfield" @contact.address.state = "CA" @contact.address.zip = 11111 + @contact.address.apt_number = 35 @contact.friends = [Contact.new, Contact.new] end @@ -104,7 +105,7 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<createdAt}, @xml end - test "should use serialiable hash" do + test "should use serializable hash" do @contact = SerializableContact.new @contact.name = 'aaron stack' @contact.age = 25 @@ -132,7 +133,7 @@ class XmlSerializationTest < ActiveModel::TestCase end test "should serialize nil" do - assert_match %r{<pseudonyms nil=\"true\"></pseudonyms>}, @contact.to_xml(:methods => :pseudonyms) + assert_match %r{<pseudonyms nil=\"true\"/>}, @contact.to_xml(:methods => :pseudonyms) end test "should serialize integer" do @@ -140,7 +141,7 @@ class XmlSerializationTest < ActiveModel::TestCase end test "should serialize datetime" do - assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml end test "should serialize boolean" do @@ -188,6 +189,23 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<friend type="Contact">}, xml end + class FriendList + def initialize(friends) + @friends = friends + end + + def to_ary + @friends + end + end + + test "include option with ary" do + @contact.friends = FriendList.new(@contact.friends) + xml = @contact.to_xml :include => :friends, :indent => 0 + assert_match %r{<friends type="array">}, xml + assert_match %r{<friend type="Contact">}, xml + end + test "multiple includes" do xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => [ :address, :friends ] assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true)) @@ -205,4 +223,39 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<friends>}, xml assert_match %r{<friend>}, xml end + + test "propagates skip-types option to included associations and attributes" do + xml = @contact.to_xml :skip_types => true, :include => :address, :indent => 0 + assert_match %r{<address>}, xml + assert_match %r{<apt-number>}, xml + end + + test "propagates camelize option to included associations and attributes" do + xml = @contact.to_xml :camelize => true, :include => :address, :indent => 0 + assert_match %r{<Address>}, xml + assert_match %r{<AptNumber type="integer">}, xml + end + + test "propagates dasherize option to included associations and attributes" do + xml = @contact.to_xml :dasherize => false, :include => :address, :indent => 0 + assert_match %r{<apt_number type="integer">}, xml + end + + test "don't propagate skip_types if skip_types is defined at the included association level" do + xml = @contact.to_xml :skip_types => true, :include => { :address => { :skip_types => false } }, :indent => 0 + assert_match %r{<address>}, xml + assert_match %r{<apt-number type="integer">}, xml + end + + test "don't propagate camelize if camelize is defined at the included association level" do + xml = @contact.to_xml :camelize => true, :include => { :address => { :camelize => false } }, :indent => 0 + assert_match %r{<address>}, xml + assert_match %r{<apt-number type="integer">}, xml + end + + test "don't propagate dasherize if dasherize is defined at the included association level" do + xml = @contact.to_xml :dasherize => false, :include => { :address => { :dasherize => true } }, :indent => 0 + assert_match %r{<address>}, xml + assert_match %r{<apt-number type="integer">}, xml + end end diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index 54e86d48db..fd833cdd06 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -56,6 +56,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute') end + def test_translated_deeply_nested_model_attributes + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/contacts/addresses" => {:street => 'Deeply Nested Address Street'}}} + assert_equal 'Deeply Nested Address Street', Person.human_attribute_name('contacts.addresses.street') + end + def test_translated_nested_model_attributes I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/addresses" => {:street => 'Person Address Street'}}} assert_equal 'Person Address Street', Person.human_attribute_name('addresses.street') @@ -82,9 +87,15 @@ class ActiveModelI18nTests < ActiveModel::TestCase end def test_human_does_not_modify_options - options = {:default => 'person model'} + options = { :default => 'person model' } Person.model_name.human(options) - assert_equal({:default => 'person model'}, options) + assert_equal({ :default => 'person model' }, options) + end + + def test_human_attribute_name_does_not_modify_options + options = { :default => 'Cool gender' } + Person.human_attribute_name('gender', options) + assert_equal({ :default => 'Cool gender' }, options) end end diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb index e4f602bd80..0015b3c196 100644 --- a/activemodel/test/cases/validations/callbacks_test.rb +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -20,7 +20,7 @@ class DogWithMethodCallbacks < Dog def set_after_validation_marker; self.history << 'after_validation_marker' ; end end -class DogValidtorsAreProc < Dog +class DogValidatorsAreProc < Dog before_validation { self.history << 'before_validation_marker' } after_validation { self.history << 'after_validation_marker' } end @@ -49,7 +49,7 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase end def test_before_validation_and_after_validation_callbacks_should_be_called_with_proc - d = DogValidtorsAreProc.new + d = DogValidatorsAreProc.new d.valid? assert_equal ['before_validation_marker', 'after_validation_marker'], d.history end diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index d0418170fa..f7556a249f 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -44,7 +44,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase p.karma_confirmation = "None" assert p.invalid? - assert_equal ["doesn't match confirmation"], p.errors[:karma] + assert_equal ["doesn't match Karma"], p.errors[:karma_confirmation] p.karma = "None" assert p.valid? @@ -52,4 +52,23 @@ class ConfirmationValidationTest < ActiveModel::TestCase Person.reset_callbacks(:validate) end + def test_title_confirmation_with_i18n_attribute + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations('en', { + :errors => {:messages => {:confirmation => "doesn't match %{attribute}"}}, + :activemodel => {:attributes => {:topic => {:title => 'Test Title'}}} + }) + + Topic.validates_confirmation_of(:title) + + t = Topic.new("title" => "We should be confirmed","title_confirmation" => "") + assert t.invalid? + assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] + + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + end diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index adab8ccb2b..7d5af27f3d 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -28,6 +28,16 @@ class ExclusionValidationTest < ActiveModel::TestCase assert_equal ["option monkey is restricted"], t.errors[:title] end + def test_validates_exclusion_of_with_within_option + Topic.validates_exclusion_of( :title, :within => %w( abe monkey ) ) + + assert Topic.new("title" => "something", "content" => "abc") + + t = Topic.new("title" => "monkey") + assert t.invalid? + assert t.errors[:title].any? + end + def test_validates_exclusion_of_for_ruby_class Person.validates_exclusion_of :karma, :in => %w( abe monkey ) @@ -54,4 +64,29 @@ class ExclusionValidationTest < ActiveModel::TestCase t.title = "wasabi" assert t.valid? end + + def test_validates_inclusion_of_with_symbol + Person.validates_exclusion_of :karma, :in => :reserved_karmas + + p = Person.new + p.karma = "abe" + + def p.reserved_karmas + %w(abe) + end + + assert p.invalid? + assert_equal ["is reserved"], p.errors[:karma] + + p = Person.new + p.karma = "abe" + + def p.reserved_karmas + %w() + end + + assert p.valid? + ensure + Person.reset_callbacks(:validate) + end end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 41a1131bcb..308a3c6cef 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -11,7 +11,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format - Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data") + Topic.validates_format_of(:title, :content, :with => /\AValidation\smacros \w+!\z/, :message => "is bad data") t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!") assert t.invalid?, "Shouldn't be valid" @@ -27,7 +27,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format_with_allow_blank - Topic.validates_format_of(:title, :with => /^Validation\smacros \w+!$/, :allow_blank => true) + Topic.validates_format_of(:title, :with => /\AValidation\smacros \w+!\z/, :allow_blank => true) assert Topic.new("title" => "Shouldn't be valid").invalid? assert Topic.new("title" => "").valid? assert Topic.new("title" => nil).valid? @@ -36,7 +36,7 @@ class PresenceValidationTest < ActiveModel::TestCase # testing ticket #3142 def test_validate_format_numeric - Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data") + Topic.validates_format_of(:title, :content, :with => /\A[1-9][0-9]*\z/, :message => "is bad data") t = Topic.new("title" => "72x", "content" => "6789") assert t.invalid?, "Shouldn't be valid" @@ -63,11 +63,21 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format_with_formatted_message - Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be %{value}") + Topic.validates_format_of(:title, :with => /\AValid Title\z/, :message => "can't be %{value}") t = Topic.new(:title => 'Invalid title') assert t.invalid? assert_equal ["can't be Invalid title"], t.errors[:title] end + + def test_validate_format_of_with_multiline_regexp_should_raise_error + assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => /^Valid Title$/) } + end + + def test_validate_format_of_with_multiline_regexp_and_option + assert_nothing_raised(ArgumentError) do + Topic.validates_format_of(:title, :with => /^Valid Title$/, :multiline => true) + end + end def test_validate_format_with_not_option Topic.validates_format_of(:title, :without => /foo/, :message => "should not contain foo") diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb index 0679e67f84..df0fcd243a 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -37,7 +37,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase # validates_confirmation_of: generate_message(attr_name, :confirmation, :message => custom_message) def test_generate_message_confirmation_with_default_message - assert_equal "doesn't match confirmation", @person.errors.generate_message(:title, :confirmation) + assert_equal "doesn't match Title", @person.errors.generate_message(:title, :confirmation) end def test_generate_message_confirmation_with_custom_message diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index e9f0e430fe..4f8b7327c0 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -81,7 +81,7 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_confirmation_of on generated message #{name}" do Person.validates_confirmation_of :title, validation_options @person.title_confirmation = 'foo' - @person.errors.expects(:generate_message).with(:title, :confirmation, generate_message_options) + @person.errors.expects(:generate_message).with(:title_confirmation, :confirmation, generate_message_options.merge(:attribute => 'Title')) @person.valid? end end @@ -141,7 +141,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_format_of on generated message #{name}" do - Person.validates_format_of :title, validation_options.merge(:with => /^[1-9][0-9]*$/) + Person.validates_format_of :title, validation_options.merge(:with => /\A[1-9][0-9]*\z/) @person.title = '72x' @person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(:value => '72x')) @person.valid? @@ -159,6 +159,17 @@ class I18nValidationTest < ActiveModel::TestCase end end + # validates_inclusion_of using :within w/ mocha + + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_inclusion_of using :within on generated message #{name}" do + Person.validates_inclusion_of :title, validation_options.merge(:within => %w(a b c)) + @person.title = 'z' + @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(:value => 'z')) + @person.valid? + end + end + # validates_exclusion_of w/ mocha COMMON_CASES.each do |name, validation_options, generate_message_options| @@ -170,6 +181,17 @@ class I18nValidationTest < ActiveModel::TestCase end end + # validates_exclusion_of using :within w/ mocha + + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_exclusion_of using :within generated message #{name}" do + Person.validates_exclusion_of :title, validation_options.merge(:within => %w(a b c)) + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(:value => 'a')) + @person.valid? + end + end + # validates_numericality_of without :only_integer w/ mocha COMMON_CASES.each do |name, validation_options, generate_message_options| @@ -217,24 +239,29 @@ class I18nValidationTest < ActiveModel::TestCase # To make things DRY this macro is defined to define 3 tests for every validation case. def self.set_expectations_for_validation(validation, error_type, &block_that_sets_validation) + if error_type == :confirmation + attribute = :title_confirmation + else + attribute = :title + end # test "validates_confirmation_of finds custom model key translation when blank" test "#{validation} finds custom model key translation when #{error_type}" do - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {error_type => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {attribute => {error_type => 'custom message'}}}}}} I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} yield(@person, {}) @person.valid? - assert_equal ['custom message'], @person.errors[:title] + assert_equal ['custom message'], @person.errors[attribute] end # test "validates_confirmation_of finds custom model key translation with interpolation when blank" test "#{validation} finds custom model key translation with interpolation when #{error_type}" do - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {error_type => 'custom message with %{extra}'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {attribute => {error_type => 'custom message with %{extra}'}}}}}} I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} yield(@person, {:extra => "extra information"}) @person.valid? - assert_equal ['custom message with extra information'], @person.errors[:title] + assert_equal ['custom message with extra information'], @person.errors[attribute] end # test "validates_confirmation_of finds global default key translation when blank" @@ -243,7 +270,7 @@ class I18nValidationTest < ActiveModel::TestCase yield(@person, {}) @person.valid? - assert_equal ['global message'], @person.errors[:title] + assert_equal ['global message'], @person.errors[attribute] end end @@ -286,7 +313,7 @@ class I18nValidationTest < ActiveModel::TestCase # validates_format_of w/o mocha set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge| - Person.validates_format_of :title, options_to_merge.merge(:with => /^[1-9][0-9]*$/) + Person.validates_format_of :title, options_to_merge.merge(:with => /\A[1-9][0-9]*\z/) end # validates_inclusion_of w/o mocha diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 851d345eab..117e9109fc 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -60,6 +60,16 @@ class InclusionValidationTest < ActiveModel::TestCase assert_equal ["option uhoh is not in the list"], t.errors[:title] end + def test_validates_inclusion_of_with_within_option + Topic.validates_inclusion_of( :title, :within => %w( a b c d e f g ) ) + + assert Topic.new("title" => "a", "content" => "abc").valid? + + t = Topic.new("title" => "uhoh", "content" => "abc") + assert t.invalid? + assert t.errors[:title].any? + end + def test_validates_inclusion_of_for_ruby_class Person.validates_inclusion_of :karma, :in => %w( abe monkey ) @@ -86,4 +96,29 @@ class InclusionValidationTest < ActiveModel::TestCase t.title = "elephant" assert t.valid? end + + def test_validates_inclusion_of_with_symbol + Person.validates_inclusion_of :karma, :in => :available_karmas + + p = Person.new + p.karma = "Lifo" + + def p.available_karmas + %w() + end + + assert p.invalid? + assert_equal ["is not included in the list"], p.errors[:karma] + + p = Person.new + p.karma = "Lifo" + + def p.available_karmas + %w(Lifo) + end + + assert p.valid? + ensure + Person.reset_callbacks(:validate) + end end diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 575154ffbd..90bc018ae1 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -154,6 +154,6 @@ class ValidatesTest < ActiveModel::TestCase topic.title = "What's happening" topic.title_confirmation = "Not this" assert !topic.valid? - assert_equal ['Y U NO CONFIRM'], topic.errors[:title] + assert_equal ['Y U NO CONFIRM'], topic.errors[:title_confirmation] end end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index a716d0896e..a9d32808da 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -11,6 +11,8 @@ require 'active_support/xml_mini' class ValidationsTest < ActiveModel::TestCase + class CustomStrictValidationException < StandardError; end + def setup Topic._validators.clear end @@ -323,6 +325,13 @@ class ValidationsTest < ActiveModel::TestCase end end + def test_strict_validation_custom_exception + Topic.validates_presence_of :title, :strict => CustomStrictValidationException + assert_raises CustomStrictValidationException do + Topic.new.valid? + end + end + def test_validates_with_bang Topic.validates! :title, :presence => true assert_raises ActiveModel::StrictValidationFailed do @@ -330,6 +339,11 @@ class ValidationsTest < ActiveModel::TestCase end end + def test_validates_with_false_hash_value + Topic.validates :title, :presence => false + assert Topic.new.valid? + end + def test_strict_validation_error_message Topic.validates :title, :strict => true, :presence => true @@ -344,4 +358,19 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates :title, options assert_equal({ :presence => true }, options) end + + def test_dup_validity_is_independent + Topic.validates_presence_of :title + topic = Topic.new("title" => "Litterature") + topic.valid? + + duped = topic.dup + duped.title = nil + assert duped.invalid? + + topic.title = nil + duped.title = 'Mathematics' + assert topic.invalid? + assert duped.valid? + end end diff --git a/activemodel/test/models/account.rb b/activemodel/test/models/account.rb new file mode 100644 index 0000000000..eed668d38f --- /dev/null +++ b/activemodel/test/models/account.rb @@ -0,0 +1,5 @@ +class Account + include ActiveModel::ForbiddenAttributesProtection + + public :sanitize_for_mass_assignment +end diff --git a/activemodel/test/models/administrator.rb b/activemodel/test/models/administrator.rb index a48f8b064f..2f3aff290c 100644 --- a/activemodel/test/models/administrator.rb +++ b/activemodel/test/models/administrator.rb @@ -1,10 +1,11 @@ class Administrator + extend ActiveModel::Callbacks include ActiveModel::Validations include ActiveModel::SecurePassword - include ActiveModel::MassAssignmentSecurity + + define_model_callbacks :create attr_accessor :name, :password_digest - attr_accessible :name has_secure_password end diff --git a/activemodel/test/models/mass_assignment_specific.rb b/activemodel/test/models/mass_assignment_specific.rb deleted file mode 100644 index 1d123fa58c..0000000000 --- a/activemodel/test/models/mass_assignment_specific.rb +++ /dev/null @@ -1,76 +0,0 @@ -class User - include ActiveModel::MassAssignmentSecurity - attr_protected :admin - - public :sanitize_for_mass_assignment -end - -class SpecialUser - include ActiveModel::MassAssignmentSecurity - attr_accessible :name, :email, :as => :moderator - - public :sanitize_for_mass_assignment -end - -class Person - include ActiveModel::MassAssignmentSecurity - attr_accessible :name, :email - attr_accessible :name, :email, :admin, :as => :admin - - public :sanitize_for_mass_assignment -end - -class Account - include ActiveModel::MassAssignmentSecurity - attr_accessible :name, :email, :as => [:default, :admin] - attr_accessible :admin, :as => :admin - - public :sanitize_for_mass_assignment -end - -class Firm - include ActiveModel::MassAssignmentSecurity - - public :sanitize_for_mass_assignment - - def self.attributes_protected_by_default - ["type"] - end -end - -class Task - include ActiveModel::MassAssignmentSecurity - attr_protected :starting - - public :sanitize_for_mass_assignment -end - -class LoosePerson - include ActiveModel::MassAssignmentSecurity - attr_protected :credit_rating, :administrator - attr_protected :credit_rating, :as => :admin -end - -class LooseDescendant < LoosePerson - attr_protected :phone_number -end - -class LooseDescendantSecond< LoosePerson - attr_protected :phone_number - attr_protected :name -end - -class TightPerson - include ActiveModel::MassAssignmentSecurity - attr_accessible :name, :address - attr_accessible :name, :address, :admin, :as => :admin - - def self.attributes_protected_by_default - ["mobile_number"] - end -end - -class TightDescendant < TightPerson - attr_accessible :phone_number - attr_accessible :super_powers, :as => :admin -end diff --git a/activemodel/test/models/oauthed_user.rb b/activemodel/test/models/oauthed_user.rb new file mode 100644 index 0000000000..9750bc19d4 --- /dev/null +++ b/activemodel/test/models/oauthed_user.rb @@ -0,0 +1,11 @@ +class OauthedUser + extend ActiveModel::Callbacks + include ActiveModel::Validations + include ActiveModel::SecurePassword + + define_model_callbacks :create + + has_secure_password(validations: false) + + attr_accessor :password_digest, :password_salt +end diff --git a/activemodel/test/models/project.rb b/activemodel/test/models/project.rb new file mode 100644 index 0000000000..581b6dc0b3 --- /dev/null +++ b/activemodel/test/models/project.rb @@ -0,0 +1,3 @@ +class Project + include ActiveModel::DeprecatedMassAssignmentSecurity +end diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index e221bb8091..4b11df12bf 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -1,6 +1,9 @@ class User + extend ActiveModel::Callbacks include ActiveModel::Validations include ActiveModel::SecurePassword + + define_model_callbacks :create has_secure_password diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb index 36c0a16688..4d7f4be097 100644 --- a/activemodel/test/models/visitor.rb +++ b/activemodel/test/models/visitor.rb @@ -1,9 +1,11 @@ class Visitor + extend ActiveModel::Callbacks include ActiveModel::Validations include ActiveModel::SecurePassword - include ActiveModel::MassAssignmentSecurity - has_secure_password + define_model_callbacks :create - attr_accessor :password_digest + has_secure_password(validations: false) + + attr_accessor :password_digest, :password_confirmation end |