aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG.md58
-rw-r--r--activemodel/MIT-LICENSE21
-rw-r--r--activemodel/README.rdoc272
-rw-r--r--activemodel/Rakefile35
-rw-r--r--activemodel/activemodel.gemspec24
-rw-r--r--activemodel/examples/validations.rb30
-rw-r--r--activemodel/lib/active_model.rb71
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb480
-rw-r--r--activemodel/lib/active_model/callbacks.rb146
-rw-r--r--activemodel/lib/active_model/conversion.rb100
-rw-r--r--activemodel/lib/active_model/dirty.rb240
-rw-r--r--activemodel/lib/active_model/errors.rb475
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb27
-rw-r--r--activemodel/lib/active_model/gem_version.rb15
-rw-r--r--activemodel/lib/active_model/lint.rb110
-rw-r--r--activemodel/lib/active_model/locale/en.yml35
-rw-r--r--activemodel/lib/active_model/model.rb99
-rw-r--r--activemodel/lib/active_model/naming.rb317
-rw-r--r--activemodel/lib/active_model/railtie.rb12
-rw-r--r--activemodel/lib/active_model/secure_password.rb134
-rw-r--r--activemodel/lib/active_model/serialization.rb163
-rw-r--r--activemodel/lib/active_model/serializers/json.rb145
-rw-r--r--activemodel/lib/active_model/serializers/xml.rb238
-rw-r--r--activemodel/lib/active_model/test_case.rb4
-rw-r--r--activemodel/lib/active_model/translation.rb69
-rw-r--r--activemodel/lib/active_model/validations.rb392
-rw-r--r--activemodel/lib/active_model/validations/absence.rb31
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb55
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb115
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb51
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb67
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb46
-rw-r--r--activemodel/lib/active_model/validations/format.rb113
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb46
-rw-r--r--activemodel/lib/active_model/validations/length.rb126
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb148
-rw-r--r--activemodel/lib/active_model/validations/presence.rb39
-rw-r--r--activemodel/lib/active_model/validations/validates.rb171
-rw-r--r--activemodel/lib/active_model/validations/with.rb150
-rw-r--r--activemodel/lib/active_model/validator.rb182
-rw-r--r--activemodel/lib/active_model/version.rb8
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb281
-rw-r--r--activemodel/test/cases/callbacks_test.rb114
-rw-r--r--activemodel/test/cases/conversion_test.rb50
-rw-r--r--activemodel/test/cases/dirty_test.rb228
-rw-r--r--activemodel/test/cases/errors_test.rb315
-rw-r--r--activemodel/test/cases/forbidden_attributes_protection_test.rb36
-rw-r--r--activemodel/test/cases/helper.rb15
-rw-r--r--activemodel/test/cases/lint_test.rb20
-rw-r--r--activemodel/test/cases/model_test.rb75
-rw-r--r--activemodel/test/cases/naming_test.rb280
-rw-r--r--activemodel/test/cases/railtie_test.rb32
-rw-r--r--activemodel/test/cases/secure_password_test.rb218
-rw-r--r--activemodel/test/cases/serialization_test.rb168
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb215
-rw-r--r--activemodel/test/cases/serializers/xml_serialization_test.rb262
-rw-r--r--activemodel/test/cases/translation_test.rb108
-rw-r--r--activemodel/test/cases/validations/absence_validation_test.rb68
-rw-r--r--activemodel/test/cases/validations/acceptance_validation_test.rb68
-rw-r--r--activemodel/test/cases/validations/callbacks_test.rb98
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb139
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb108
-rw-r--r--activemodel/test/cases/validations/exclusion_validation_test.rb92
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb149
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb150
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb373
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb147
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb424
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb211
-rw-r--r--activemodel/test/cases/validations/presence_validation_test.rb107
-rw-r--r--activemodel/test/cases/validations/validates_test.rb159
-rw-r--r--activemodel/test/cases/validations/validations_context_test.rb50
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb172
-rw-r--r--activemodel/test/cases/validations_test.rb392
-rw-r--r--activemodel/test/config.rb3
-rw-r--r--activemodel/test/models/account.rb5
-rw-r--r--activemodel/test/models/blog_post.rb9
-rw-r--r--activemodel/test/models/contact.rb26
-rw-r--r--activemodel/test/models/custom_reader.rb15
-rw-r--r--activemodel/test/models/helicopter.rb7
-rw-r--r--activemodel/test/models/person.rb17
-rw-r--r--activemodel/test/models/person_with_validator.rb24
-rw-r--r--activemodel/test/models/project.rb3
-rw-r--r--activemodel/test/models/reply.rb32
-rw-r--r--activemodel/test/models/sheep.rb3
-rw-r--r--activemodel/test/models/topic.rb40
-rw-r--r--activemodel/test/models/track_back.rb11
-rw-r--r--activemodel/test/models/user.rb10
-rw-r--r--activemodel/test/models/visitor.rb10
-rw-r--r--activemodel/test/validators/email_validator.rb6
-rw-r--r--activemodel/test/validators/namespace/email_validator.rb6
91 files changed, 10611 insertions, 0 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
new file mode 100644
index 0000000000..c14b0688c7
--- /dev/null
+++ b/activemodel/CHANGELOG.md
@@ -0,0 +1,58 @@
+* Passwords with spaces only allowed in `ActiveModel::SecurePassword`.
+
+ Presence validation can be used to restore old behavior.
+
+ *Yevhene Shemet*
+
+* Validate options passed to `ActiveModel::Validations.validate`.
+
+ Preventing, in many cases, the simple mistake of using `validate` instead of `validates`.
+
+ *Sonny Michaud*
+
+* Deprecate `reset_#{attribute}` in favor of `restore_#{attribute}`.
+
+ These methods may cause confusion with the `reset_changes` that behaves differently
+ of them.
+
+* Deprecate `ActiveModel::Dirty#reset_changes` in favor of `#clear_changes_information`.
+
+ This method name is causing confusion with the `reset_#{attribute}`
+ methods. While `reset_name` set the value of the name attribute for the
+ previous value `reset_changes` only discard the changes and previous
+ changes.
+
+* Added `restore_attributes` method to `ActiveModel::Dirty` API to restore all the
+ changed values to the previous data.
+
+ *Igor G.*
+
+* Allow proc and symbol as values for `only_integer` of `NumericalityValidator`
+
+ *Robin Mehner*
+
+* `has_secure_password` now verifies that the given password is less than 72
+ characters if validations are enabled.
+
+ Fixes #14591.
+
+ *Akshay Vishnoi*
+
+* Remove deprecated `Validator#setup` without replacement.
+
+ See #10716.
+
+ *Kuldeep Aggarwal*
+
+* Add plural and singular form for length validator's default messages.
+
+ *Abd ar-Rahman Hamid*
+
+* Introduce `validate` as an alias for `valid?`.
+
+ This is more intuitive when you want to run validations but don't care about
+ the return value.
+
+ *Henrik Nyh*
+
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
new file mode 100644
index 0000000000..d58dd9ed9b
--- /dev/null
+++ b/activemodel/MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2004-2014 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc
new file mode 100644
index 0000000000..f6beff14e1
--- /dev/null
+++ b/activemodel/README.rdoc
@@ -0,0 +1,272 @@
+= Active Model -- model interfaces for Rails
+
+Active Model provides a known set of interfaces for usage in model classes.
+They allow for Action Pack helpers to interact with non-Active Record models,
+for example. Active Model also helps with building custom ORMs for use outside of
+the Rails framework.
+
+Prior to Rails 3.0, if a plugin or gem developer wanted to have an object
+interact with Action Pack helpers, it was required to either copy chunks of
+code from Rails, or monkey patch entire helpers to make them handle objects
+that did not exactly conform to the Active Record interface. This would result
+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 <tt>ActiveModel::Lint::Tests</tt>.
+
+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:
+
+* Add attribute magic to objects
+
+ class Person
+ include ActiveModel::AttributeMethods
+
+ attribute_method_prefix 'clear_'
+ define_attribute_methods :name, :age
+
+ attr_accessor :name, :age
+
+ def clear_attribute(attr)
+ send("#{attr}=", nil)
+ end
+ end
+
+ person = Person.new
+ person.clear_name
+ person.clear_age
+
+ {Learn more}[link:classes/ActiveModel/AttributeMethods.html]
+
+* Callbacks for certain operations
+
+ class Person
+ extend ActiveModel::Callbacks
+ define_model_callbacks :create
+
+ def create
+ run_callbacks :create do
+ # Your create action methods here
+ end
+ end
+ end
+
+ This generates +before_create+, +around_create+ and +after_create+
+ class methods that wrap your create method.
+
+ {Learn more}[link:classes/ActiveModel/Callbacks.html]
+
+* Tracking value changes
+
+ class Person
+ include ActiveModel::Dirty
+
+ define_attribute_methods :name
+
+ def name
+ @name
+ end
+
+ def name=(val)
+ name_will_change! unless val == @name
+ @name = val
+ end
+
+ def save
+ # do persistence work
+ changes_applied
+ end
+ end
+
+ person = Person.new
+ person.name # => nil
+ person.changed? # => false
+ person.name = 'bob'
+ person.changed? # => true
+ person.changed # => ['name']
+ person.changes # => { 'name' => [nil, 'bob'] }
+ person.save
+ person.name = 'robert'
+ person.save
+ person.previous_changes # => {'name' => ['bob, 'robert']}
+
+ {Learn more}[link:classes/ActiveModel/Dirty.html]
+
+* Adding +errors+ interface to objects
+
+ Exposing error messages allows objects to interact with Action Pack
+ helpers seamlessly.
+
+ class Person
+
+ def initialize
+ @errors = ActiveModel::Errors.new(self)
+ end
+
+ attr_accessor :name
+ attr_reader :errors
+
+ def validate!
+ errors.add(:name, "cannot be nil") if name.nil?
+ end
+
+ def self.human_attribute_name(attr, options = {})
+ "Name"
+ end
+ end
+
+ person = Person.new
+ person.name = nil
+ person.validate!
+ person.errors.full_messages
+ # => ["Name cannot be nil"]
+
+ {Learn more}[link:classes/ActiveModel/Errors.html]
+
+* Model name introspection
+
+ class NamedPerson
+ extend ActiveModel::Naming
+ end
+
+ NamedPerson.model_name.name # => "NamedPerson"
+ NamedPerson.model_name.human # => "Named person"
+
+ {Learn more}[link:classes/ActiveModel/Naming.html]
+
+* Making objects serializable
+
+ ActiveModel::Serialization provides a standard interface for your object
+ to provide +to_json+ or +to_xml+ serialization.
+
+ class SerialPerson
+ include ActiveModel::Serialization
+
+ attr_accessor :name
+
+ def attributes
+ {'name' => name}
+ end
+ end
+
+ s = SerialPerson.new
+ s.serializable_hash # => {"name"=>nil}
+
+ class SerialPerson
+ include ActiveModel::Serializers::JSON
+ end
+
+ s = SerialPerson.new
+ s.to_json # => "{\"name\":null}"
+
+ class SerialPerson
+ include ActiveModel::Serializers::Xml
+ end
+
+ s = SerialPerson.new
+ s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
+
+ {Learn more}[link:classes/ActiveModel/Serialization.html]
+
+* Internationalization (i18n) support
+
+ class Person
+ extend ActiveModel::Translation
+ end
+
+ Person.human_attribute_name('my_attribute')
+ # => "My attribute"
+
+ {Learn more}[link:classes/ActiveModel/Translation.html]
+
+* Validation support
+
+ class Person
+ include ActiveModel::Validations
+
+ attr_accessor :first_name, :last_name
+
+ validates_each :first_name, :last_name do |record, attr, value|
+ record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
+ end
+ end
+
+ person = Person.new
+ person.first_name = 'zoolander'
+ person.valid? # => false
+
+ {Learn more}[link:classes/ActiveModel/Validations.html]
+
+* Custom validators
+
+ class HasNameValidator < ActiveModel::Validator
+ def validate(record)
+ record.errors[:name] = "must exist" if record.name.blank?
+ end
+ end
+
+ class ValidatorPerson
+ include ActiveModel::Validations
+ validates_with HasNameValidator
+ attr_accessor :name
+ end
+
+ p = ValidatorPerson.new
+ p.valid? # => false
+ p.errors.full_messages # => ["Name must exist"]
+ p.name = "Bob"
+ p.valid? # => true
+
+ {Learn more}[link:classes/ActiveModel/Validator.html]
+
+
+== Download and installation
+
+The latest version of Active Model can be installed with RubyGems:
+
+ % [sudo] gem install activemodel
+
+Source code can be downloaded as part of the Rails project on GitHub
+
+* https://github.com/rails/rails/tree/master/activemodel
+
+
+== License
+
+Active Model is released under the MIT license:
+
+* http://www.opensource.org/licenses/MIT
+
+
+== Support
+
+API documentation is at
+
+* http://api.rubyonrails.org
+
+Bug reports can be filed for the Ruby on Rails project here:
+
+* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/activemodel/Rakefile b/activemodel/Rakefile
new file mode 100644
index 0000000000..407dda2ec3
--- /dev/null
+++ b/activemodel/Rakefile
@@ -0,0 +1,35 @@
+dir = File.dirname(__FILE__)
+
+require 'rake/testtask'
+
+task :default => :test
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb").sort
+ t.warning = true
+ t.verbose = true
+end
+
+namespace :test do
+ task :isolated do
+ Dir.glob("#{dir}/test/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
+ end or raise "Failures"
+ end
+end
+
+require 'rubygems/package_task'
+
+spec = eval(File.read("#{dir}/activemodel.gemspec"))
+
+Gem::PackageTask.new(spec) do |p|
+ p.gem_spec = spec
+end
+
+desc "Release to rubygems"
+task :release => :package do
+ require 'rake/gemcutter'
+ Rake::Gemcutter::Tasks.new(spec).define
+ Rake::Task['gem:push'].invoke
+end
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
new file mode 100644
index 0000000000..36e565f692
--- /dev/null
+++ b/activemodel/activemodel.gemspec
@@ -0,0 +1,24 @@
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ 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. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.'
+
+ s.required_ruby_version = '>= 1.9.3'
+
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
+
+ s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']
+ s.require_path = 'lib'
+
+ s.add_dependency 'activesupport', version
+
+ s.add_dependency 'builder', '~> 3.1'
+end
diff --git a/activemodel/examples/validations.rb b/activemodel/examples/validations.rb
new file mode 100644
index 0000000000..b8e74acd5e
--- /dev/null
+++ b/activemodel/examples/validations.rb
@@ -0,0 +1,30 @@
+require File.expand_path('../../../load_paths', __FILE__)
+require 'active_model'
+
+class Person
+ include ActiveModel::Conversion
+ include ActiveModel::Validations
+
+ validates :name, presence: true
+
+ attr_accessor :name
+
+ def initialize(attributes = {})
+ @name = attributes[:name]
+ end
+
+ def persist
+ @persisted = true
+ end
+
+ def persisted?
+ @persisted
+ end
+end
+
+person1 = Person.new
+p person1.valid? # => false
+p person1.errors.messages # => {:name=>["can't be blank"]}
+
+person2 = Person.new(name: 'matz')
+p person2.valid? # => true
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
new file mode 100644
index 0000000000..feb3d9371d
--- /dev/null
+++ b/activemodel/lib/active_model.rb
@@ -0,0 +1,71 @@
+#--
+# Copyright (c) 2004-2014 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+require 'active_support'
+require 'active_support/rails'
+require 'active_model/version'
+
+module ActiveModel
+ extend ActiveSupport::Autoload
+
+ autoload :AttributeMethods
+ autoload :BlockValidator, 'active_model/validator'
+ autoload :Callbacks
+ autoload :Conversion
+ autoload :Dirty
+ autoload :EachValidator, 'active_model/validator'
+ autoload :ForbiddenAttributesProtection
+ autoload :Lint
+ autoload :Model
+ autoload :Name, 'active_model/naming'
+ autoload :Naming
+ autoload :SecurePassword
+ autoload :Serialization
+ autoload :TestCase
+ autoload :Translation
+ autoload :Validations
+ autoload :Validator
+
+ eager_autoload do
+ autoload :Errors
+ autoload :StrictValidationFailed, 'active_model/errors'
+ end
+
+ module Serializers
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :JSON
+ autoload :Xml
+ end
+ end
+
+ def self.eager_load!
+ super
+ ActiveModel::Serializers.eager_load!
+ end
+end
+
+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
new file mode 100644
index 0000000000..ea07c5c039
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -0,0 +1,480 @@
+require 'thread_safe'
+require 'mutex_m'
+
+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
+ #
+ # Provides a way to add prefixes and suffixes to your methods as
+ # well as handling the creation of <tt>ActiveRecord::Base</tt>-like
+ # class methods such as +table_name+.
+ #
+ # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
+ #
+ # * <tt>include ActiveModel::AttributeMethods</tt> in your class.
+ # * Call each of its method you want to add, such as +attribute_method_suffix+
+ # or +attribute_method_prefix+.
+ # * Call +define_attribute_methods+ after the other methods are called.
+ # * Define the various generic +_attribute+ methods that you have declared.
+ # * Define an +attributes+ method 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.
+ #
+ # A minimal implementation could be:
+ #
+ # class Person
+ # include ActiveModel::AttributeMethods
+ #
+ # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
+ # attribute_method_suffix '_contrived?'
+ # attribute_method_prefix 'clear_'
+ # define_attribute_methods :name
+ #
+ # attr_accessor :name
+ #
+ # def attributes
+ # { 'name' => @name }
+ # end
+ #
+ # private
+ #
+ # def attribute_contrived?(attr)
+ # true
+ # end
+ #
+ # def clear_attribute(attr)
+ # send("#{attr}=", nil)
+ # end
+ #
+ # def reset_attribute_to_default!(attr)
+ # send("#{attr}=", 'Default Name')
+ # end
+ # end
+ module AttributeMethods
+ extend ActiveSupport::Concern
+
+ NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
+ CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
+
+ included do
+ class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false
+ self.attribute_aliases = {}
+ self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new]
+ end
+
+ module ClassMethods
+ # Declares a method available for all attributes with the given prefix.
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
+ #
+ # #{prefix}#{attr}(*args, &block)
+ #
+ # to
+ #
+ # #{prefix}attribute(#{attr}, *args, &block)
+ #
+ # An instance method <tt>#{prefix}attribute</tt> must exist and accept
+ # at least the +attr+ argument.
+ #
+ # class Person
+ # include ActiveModel::AttributeMethods
+ #
+ # attr_accessor :name
+ # attribute_method_prefix 'clear_'
+ # define_attribute_methods :name
+ #
+ # private
+ #
+ # def clear_attribute(attr)
+ # send("#{attr}=", nil)
+ # end
+ # end
+ #
+ # person = Person.new
+ # 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 }
+ undefine_attribute_methods
+ end
+
+ # Declares a method available for all attributes with the given suffix.
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
+ #
+ # #{attr}#{suffix}(*args, &block)
+ #
+ # to
+ #
+ # attribute#{suffix}(#{attr}, *args, &block)
+ #
+ # 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
+ #
+ # 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 attribute_method_suffix(*suffixes)
+ self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
+ undefine_attribute_methods
+ end
+
+ # Declares a method available for all attributes with the given prefix
+ # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
+ # the method.
+ #
+ # #{prefix}#{attr}#{suffix}(*args, &block)
+ #
+ # to
+ #
+ # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
+ #
+ # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
+ # accept at least the +attr+ argument.
+ #
+ # class Person
+ # include ActiveModel::AttributeMethods
+ #
+ # attr_accessor :name
+ # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
+ # define_attribute_methods :name
+ #
+ # private
+ #
+ # def reset_attribute_to_default!(attr)
+ # send("#{attr}=", 'Default Name')
+ # end
+ # end
+ #
+ # person = Person.new
+ # person.name # => 'Gem'
+ # person.reset_name_to_default!
+ # person.name # => 'Default Name'
+ def attribute_method_affix(*affixes)
+ 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_proxy_call false, self, matcher_new, matcher_old
+ end
+ end
+
+ # Is +new_name+ an alias?
+ def attribute_alias?(new_name)
+ attribute_aliases.key? new_name.to_s
+ end
+
+ # Returns the original name for the alias +name+
+ def attribute_alias(name)
+ attribute_aliases[name.to_s]
+ end
+
+ # Declares the attributes that should be prefixed and suffixed by
+ # ActiveModel::AttributeMethods.
+ #
+ # 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
+ #
+ # private
+ #
+ # def clear_attribute(attr)
+ # send("#{attr}=", nil)
+ # end
+ # end
+ 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)
+
+ unless instance_method_already_implemented?(method_name)
+ generate_method = "define_method_#{matcher.method_missing_target}"
+
+ if respond_to?(generate_method, true)
+ send(generate_method, attr_name)
+ else
+ 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.
+ #
+ # 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) }
+ end
+ attribute_method_matchers_cache.clear
+ end
+
+ def generated_attribute_methods #:nodoc:
+ @generated_attribute_methods ||= Module.new {
+ extend Mutex_m
+ }.tap { |mod| include mod }
+ end
+
+ protected
+ def instance_method_already_implemented?(method_name) #:nodoc:
+ generated_attribute_methods.method_defined?(method_name)
+ end
+
+ private
+ # The methods +method_missing+ and +respond_to?+ of this module are
+ # invoked often in a typical rails, both of which invoke the method
+ # +match_attribute_method?+. The latter method iterates through an
+ # array doing regular expression matches, which results in a lot of
+ # object creations. Most of the time it returns a +nil+ match. As the
+ # match result is always the same given a +method_name+, this cache is
+ # used to alleviate the GC, which ultimately also speeds up the app
+ # significantly (in our case our test suite finishes 10% faster with
+ # this cache).
+ def attribute_method_matchers_cache #:nodoc:
+ @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4)
+ end
+
+ def attribute_method_matcher(method_name) #:nodoc:
+ attribute_method_matchers_cache.compute_if_absent(method_name) do
+ # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
+ # will match every time.
+ matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
+ match = nil
+ matchers.detect { |method| match = method.match(method_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_proxy_call(include_private, mod, name, send, *extra) #:nodoc:
+ defn = if name =~ NAME_COMPILABLE_REGEXP
+ "def #{name}(*args)"
+ else
+ "define_method(:'#{name}') do |*args|"
+ end
+
+ extra = (extra.map!(&:inspect) << "*args").join(", ")
+
+ target = if send =~ CALL_COMPILABLE_REGEXP
+ "#{"self." unless include_private}#{send}(#{extra})"
+ else
+ "send(:'#{send}', #{extra})"
+ end
+
+ mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ #{defn}
+ #{target}
+ end
+ RUBY
+ end
+
+ class AttributeMethodMatcher #:nodoc:
+ attr_reader :prefix, :suffix, :method_missing_target
+
+ AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
+
+ def initialize(options = {})
+ @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}"
+ end
+
+ def match(method_name)
+ if @regex =~ method_name
+ AttributeMethodMatch.new(method_missing_target, $1, method_name)
+ end
+ end
+
+ def method_name(attr_name)
+ @method_name % attr_name
+ end
+
+ def plain?
+ prefix.empty? && suffix.empty?
+ end
+ end
+ end
+
+ # Allows access to the object attributes, which are held in the hash
+ # returned by <tt>attributes</tt>, as though they were first-class
+ # methods. So a +Person+ class with a +name+ attribute can for example use
+ # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
+ # the attributes hash -- except for multiple assigns with
+ # <tt>ActiveRecord::Base#attributes=</tt>.
+ #
+ # It's also possible to instantiate related objects, so a <tt>Client</tt>
+ # class belonging to the +clients+ table with a +master_id+ foreign key
+ # can instantiate master through <tt>Client#master</tt>.
+ def method_missing(method, *args, &block)
+ if respond_to_without_attributes?(method, true)
+ super
+ else
+ match = match_attribute_method?(method.to_s)
+ match ? attribute_missing(match, *args, &block) : super
+ end
+ end
+
+ # +attribute_missing+ is like +method_missing+, but for attributes. When
+ # +method_missing+ is called we check to see if there is a matching
+ # attribute method. If so, we tell +attribute_missing+ to dispatch the
+ # attribute. This method can be overloaded to customize the behavior.
+ def attribute_missing(match, *args, &block)
+ __send__(match.target, match.attr_name, *args, &block)
+ end
+
+ # A +Person+ instance with a +name+ attribute can ask
+ # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
+ # and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
+ alias :respond_to_without_attributes? :respond_to?
+ def respond_to?(method, include_private_methods = false)
+ if super
+ true
+ elsif !include_private_methods && super(method, true)
+ # If we're here then we haven't found among non-private methods
+ # but found among all methods. Which means that the given method is private.
+ false
+ else
+ !match_attribute_method?(method.to_s).nil?
+ end
+ end
+
+ protected
+ def attribute_method?(attr_name) #:nodoc:
+ respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
+ end
+
+ private
+ # Returns a struct representing the matching attribute method.
+ # 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 if match && attribute_method?(match.attr_name)
+ end
+
+ def missing_attribute(attr_name, stack)
+ raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
new file mode 100644
index 0000000000..b27a39b787
--- /dev/null
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -0,0 +1,146 @@
+require 'active_support/core_ext/array/extract_options'
+
+module ActiveModel
+ # == Active \Model \Callbacks
+ #
+ # Provides an interface for any class to have Active Record like callbacks.
+ #
+ # Like the Active Record methods, the callback chain is aborted as soon as
+ # one of the methods in the chain returns +false+.
+ #
+ # First, extend ActiveModel::Callbacks from the class you are creating:
+ #
+ # class MyModel
+ # extend ActiveModel::Callbacks
+ # end
+ #
+ # Then define a list of methods that you want callbacks attached to:
+ #
+ # define_model_callbacks :create, :update
+ #
+ # This will provide all three standard callbacks (before, around and after)
+ # for both the <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
+ # # Your create action methods here
+ # 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 model.
+ #
+ # before_create :action_before_create
+ #
+ # def action_before_create
+ # # 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 :create, only: [:after, :before]
+ #
+ # Would only create the +after_create+ and +before_create+ callback methods in
+ # your class.
+ module Callbacks
+ 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
+ # <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
+ #
+ # Note, the <tt>only: <type></tt> hash will apply to all callbacks defined
+ # on that method call. To get around this you can call the define_model_callbacks
+ # method as many times as you need.
+ #
+ # define_model_callbacks :create, only: :after
+ # define_model_callbacks :update, only: :before
+ # define_model_callbacks :destroy, only: :around
+ #
+ # Would create +after_create+, +before_update+ and +around_destroy+ methods
+ # only.
+ #
+ # You can pass in a class to before_<type>, after_<type> and around_<type>,
+ # in which case the callback will call that class's <action>_<type> method
+ # passing the object that the callback is being called on.
+ #
+ # class MyModel
+ # extend ActiveModel::Callbacks
+ # define_model_callbacks :create
+ #
+ # before_create AnotherClass
+ # end
+ #
+ # class AnotherClass
+ # def self.before_create( obj )
+ # # obj is the MyModel instance that the callback is being called on
+ # end
+ # end
+ def define_model_callbacks(*callbacks)
+ options = callbacks.extract_options!
+ options = {
+ terminator: ->(_,result) { result == false },
+ skip_after_callbacks_if_terminated: true,
+ scope: [:kind, :name],
+ only: [:before, :around, :after]
+ }.merge!(options)
+
+ types = Array(options.delete(:only))
+
+ callbacks.each do |callback|
+ define_callbacks(callback, options)
+
+ types.each do |type|
+ send("_define_#{type}_model_callback", self, callback)
+ end
+ end
+ end
+
+ private
+
+ def _define_before_model_callback(klass, callback) #:nodoc:
+ klass.define_singleton_method("before_#{callback}") do |*args, &block|
+ set_callback(:"#{callback}", :before, *args, &block)
+ end
+ end
+
+ def _define_around_model_callback(klass, callback) #:nodoc:
+ klass.define_singleton_method("around_#{callback}") do |*args, &block|
+ set_callback(:"#{callback}", :around, *args, &block)
+ end
+ end
+
+ def _define_after_model_callback(klass, callback) #:nodoc:
+ klass.define_singleton_method("after_#{callback}") do |*args, &block|
+ options = args.extract_options!
+ options[:prepend] = true
+ conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
+ v != false
+ }
+ options[:if] = Array(options[:if]) << conditional
+ set_callback(:"#{callback}", :after, *(args << options), &block)
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
new file mode 100644
index 0000000000..9c9b6f4a77
--- /dev/null
+++ b/activemodel/lib/active_model/conversion.rb
@@ -0,0 +1,100 @@
+module ActiveModel
+ # == Active \Model \Conversion
+ #
+ # Handles default conversions: to_model, to_key, to_param, and to_partial_path.
+ #
+ # Let's take for example this non-persisted object.
+ #
+ # class ContactMessage
+ # include ActiveModel::Conversion
+ #
+ # # ContactMessage are never persisted in the DB
+ # def persisted?
+ # false
+ # end
+ # end
+ #
+ # cm = ContactMessage.new
+ # 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+.
+ #
+ # 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
+ # your object with Active Model compliant methods.
+ def to_model
+ self
+ end
+
+ # Returns an Array of all key attributes if any is set, regardless if
+ # the object is persisted or not. Returns +nil+ if there are no key attributes.
+ #
+ # class Person
+ # include ActiveModel::Conversion
+ # attr_accessor :id
+ # end
+ #
+ # person = Person.create(id: 1)
+ # 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+.
+ #
+ # class Person
+ # include ActiveModel::Conversion
+ # attr_accessor :id
+ # def persisted?
+ # true
+ # end
+ # end
+ #
+ # person = Person.create(id: 1)
+ # person.to_param # => "1"
+ def to_param
+ (persisted? && key = to_key) ? key.join('-') : nil
+ end
+
+ # 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 #to_partial_path. This is an
+ # internal method and should not be accessed directly.
+ def _to_partial_path #:nodoc:
+ @_to_partial_path ||= begin
+ element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
+ collection = ActiveSupport::Inflector.tableize(name)
+ "#{collection}/#{element}".freeze
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
new file mode 100644
index 0000000000..d11243c4c0
--- /dev/null
+++ b/activemodel/lib/active_model/dirty.rb
@@ -0,0 +1,240 @@
+require 'active_support/hash_with_indifferent_access'
+require 'active_support/core_ext/object/duplicable'
+
+module ActiveModel
+ # == Active \Model \Dirty
+ #
+ # Provides a way to track changes in your object in the same way as
+ # Active Record does.
+ #
+ # The requirements for implementing ActiveModel::Dirty are:
+ #
+ # * <tt>include ActiveModel::Dirty</tt> in your object.
+ # * Call <tt>define_attribute_methods</tt> passing each method you want to
+ # track.
+ # * Call <tt>attr_name_will_change!</tt> before each change to the tracked
+ # attribute.
+ # * Call <tt>changes_applied</tt> after the changes are persisted.
+ # * Call <tt>clear_changes_information</tt> when you want to reset the changes
+ # information.
+ # * Call <tt>restore_attributes</tt> when you want to restore previous data.
+ #
+ # A minimal implementation could be:
+ #
+ # class Person
+ # include ActiveModel::Dirty
+ #
+ # define_attribute_methods :name
+ #
+ # def name
+ # @name
+ # end
+ #
+ # def name=(val)
+ # name_will_change! unless val == @name
+ # @name = val
+ # end
+ #
+ # def save
+ # # do persistence work
+ #
+ # changes_applied
+ # end
+ #
+ # def reload!
+ # # get the values from the persistence layer
+ #
+ # clear_changes_information
+ # end
+ #
+ # def rollback!
+ # restore_attributes
+ # end
+ # end
+ #
+ # 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_changed?(from: "Uncle Bob", to: "Bob") # => true
+ # person.name_was # => "Uncle Bob"
+ # person.name_change # => ["Uncle Bob", "Bob"]
+ # person.name = 'Bill'
+ # person.name_change # => ["Uncle Bob", "Bill"]
+ #
+ # Save the changes:
+ #
+ # person.save
+ # person.changed? # => false
+ # person.name_changed? # => false
+ #
+ # Reset the changes:
+ #
+ # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
+ # person.reload!
+ # person.previous_changes # => {}
+ #
+ # Rollback the changes:
+ #
+ # person.name = "Uncle Bob"
+ # person.rollback!
+ # person.name # => "Bill"
+ # 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"]}
+ #
+ # If an attribute is modified in-place then make use of
+ # +[attribute_name]_will_change!+ to mark that the attribute is changing.
+ # Otherwise Active Model can't track changes to in-place attributes. Note
+ # that Active Record can detect in-place modifications automatically. You do
+ # not need to call +[attribute_name]_will_change!+ on Active Record models.
+ #
+ # person.name_will_change!
+ # person.name_change # => ["Bill", "Bill"]
+ # person.name << 'y'
+ # person.name_change # => ["Bill", "Billy"]
+ module Dirty
+ extend ActiveSupport::Concern
+ include ActiveModel::AttributeMethods
+
+ included do
+ attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
+ attribute_method_affix prefix: 'reset_', suffix: '!'
+ attribute_method_affix prefix: 'restore_', suffix: '!'
+ end
+
+ # Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
+ #
+ # person.changed? # => false
+ # person.name = 'bob'
+ # person.changed? # => true
+ def changed?
+ changed_attributes.present?
+ end
+
+ # Returns an array with the name of the attributes with unsaved changes.
+ #
+ # person.changed # => []
+ # person.name = 'bob'
+ # person.changed # => ["name"]
+ def changed
+ changed_attributes.keys
+ end
+
+ # 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"] }
+ def changes
+ ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ end
+
+ # 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"]}
+ def previous_changes
+ @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ # 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 ||= ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ # Handle <tt>*_changed?</tt> for +method_missing+.
+ def attribute_changed?(attr, options = {}) #:nodoc:
+ result = changed_attributes.include?(attr)
+ result &&= options[:to] == __send__(attr) if options.key?(:to)
+ result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
+ result
+ end
+
+ # Handle <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr) # :nodoc:
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
+ end
+
+ # Restore all previous data of the provided attributes.
+ def restore_attributes(attributes = changed)
+ attributes.each { |attr| restore_attribute! attr }
+ end
+
+ private
+
+ # Removes current changes and makes them accessible through +previous_changes+.
+ def changes_applied # :doc:
+ @previously_changed = changes
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ # Clear all dirty data: current changes and previous changes.
+ def clear_changes_information # :doc:
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ def reset_changes
+ ActiveSupport::Deprecation.warn "#reset_changes is deprecated and will be removed on Rails 5. Please use #clear_changes_information instead."
+ clear_changes_information
+ end
+
+ # Handle <tt>*_change</tt> for +method_missing+.
+ def attribute_change(attr)
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
+ end
+
+ # 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
+ end
+
+ # Handle <tt>reset_*!</tt> for +method_missing+.
+ def reset_attribute!(attr)
+ ActiveSupport::Deprecation.warn "#reset_#{attr}! is deprecated and will be removed on Rails 5. Please use #restore_#{attr}! instead."
+
+ restore_attribute!(attr)
+ end
+
+ # Handle <tt>restore_*!</tt> for +method_missing+.
+ def restore_attribute!(attr)
+ if attribute_changed?(attr)
+ __send__("#{attr}=", changed_attributes[attr])
+ changed_attributes.delete(attr)
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
new file mode 100644
index 0000000000..1d025beeef
--- /dev/null
+++ b/activemodel/lib/active_model/errors.rb
@@ -0,0 +1,475 @@
+# -*- coding: utf-8 -*-
+
+require 'active_support/core_ext/array/conversions'
+require 'active_support/core_ext/string/inflections'
+
+module ActiveModel
+ # == Active \Model \Errors
+ #
+ # Provides a modified +Hash+ that you can include in your object
+ # for handling error messages and interacting with Action View helpers.
+ #
+ # A minimal implementation could be:
+ #
+ # class Person
+ # # Required dependency for ActiveModel::Errors
+ # extend ActiveModel::Naming
+ #
+ # def initialize
+ # @errors = ActiveModel::Errors.new(self)
+ # end
+ #
+ # attr_accessor :name
+ # attr_reader :errors
+ #
+ # def validate!
+ # errors.add(:name, "cannot be nil") if name.nil?
+ # end
+ #
+ # # The following methods are needed to be minimally implemented
+ #
+ # def read_attribute_for_validation(attr)
+ # send(attr)
+ # end
+ #
+ # def Person.human_attribute_name(attr, options = {})
+ # attr
+ # end
+ #
+ # def Person.lookup_ancestors
+ # [self]
+ # end
+ # end
+ #
+ # The last three methods are required in your object for Errors to be
+ # able to generate error messages correctly and also handle multiple
+ # languages. Of course, if you extend your object with ActiveModel::Translation
+ # you will not need to implement the last two. Likewise, using
+ # ActiveModel::Validations will handle the validation related methods
+ # for you.
+ #
+ # The above allows you to do:
+ #
+ # person = Person.new
+ # person.validate! # => ["cannot be nil"]
+ # person.errors.full_messages # => ["name cannot be nil"]
+ # # etc..
+ class Errors
+ include Enumerable
+
+ CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
+
+ attr_reader :messages
+
+ # Pass in the instance of the object that is using the errors object.
+ #
+ # class Person
+ # def initialize
+ # @errors = ActiveModel::Errors.new(self)
+ # end
+ # end
+ def initialize(base)
+ @base = base
+ @messages = {}
+ end
+
+ def initialize_dup(other) # :nodoc:
+ @messages = other.messages.dup
+ super
+ end
+
+ # Clear the error messages.
+ #
+ # person.errors.full_messages # => ["name cannot be nil"]
+ # person.errors.clear
+ # person.errors.full_messages # => []
+ def clear
+ messages.clear
+ end
+
+ # Returns +true+ if the error messages include an error for the given key
+ # +attribute+, +false+ otherwise.
+ #
+ # person.errors.messages # => {:name=>["cannot be nil"]}
+ # person.errors.include?(:name) # => true
+ # person.errors.include?(:age) # => false
+ def include?(attribute)
+ messages[attribute].present?
+ end
+ # aliases include?
+ alias :has_key? :include?
+
+ # Get messages for +key+.
+ #
+ # person.errors.messages # => {:name=>["cannot be nil"]}
+ # person.errors.get(:name) # => ["cannot be nil"]
+ # person.errors.get(:age) # => nil
+ def get(key)
+ messages[key]
+ end
+
+ # Set messages for +key+ to +value+.
+ #
+ # person.errors.get(:name) # => ["cannot 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+. Returns the deleted messages.
+ #
+ # person.errors.get(:name) # => ["cannot be nil"]
+ # person.errors.delete(:name) # => ["cannot be nil"]
+ # person.errors.get(:name) # => nil
+ def delete(key)
+ messages.delete(key)
+ end
+
+ # When passed a symbol or a name of a method, returns an array of errors
+ # for the method.
+ #
+ # person.errors[:name] # => ["cannot be nil"]
+ # person.errors['name'] # => ["cannot be nil"]
+ def [](attribute)
+ get(attribute.to_sym) || set(attribute.to_sym, [])
+ end
+
+ # Adds to the supplied attribute the supplied error message.
+ #
+ # person.errors[:name] = "must be set"
+ # person.errors[:name] # => ['must be set']
+ def []=(attribute, error)
+ self[attribute] << error
+ end
+
+ # Iterates through each error key, value pair in the error messages hash.
+ # Yields the attribute and the error for that attribute. If the attribute
+ # has more than one error message, yields once for each error message.
+ #
+ # person.errors.add(:name, "can't be blank")
+ # person.errors.each do |attribute, error|
+ # # Will yield :name and "can't be blank"
+ # end
+ #
+ # 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
+ def each
+ messages.each_key do |attribute|
+ self[attribute].each { |error| yield attribute, error }
+ end
+ end
+
+ # Returns the number of error messages.
+ #
+ # 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.
+ #
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
+ # person.errors.values # => [["cannot be nil", "must be specified"]]
+ def values
+ messages.values
+ end
+
+ # Returns all message keys.
+ #
+ # person.errors.messages # => {:name=>["cannot 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.
+ #
+ # 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.
+ #
+ # 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.
+ # If the error message is a string it can be empty.
+ #
+ # person.errors.full_messages # => ["name cannot be nil"]
+ # person.errors.empty? # => false
+ def empty?
+ all? { |k, v| v && v.empty? && !v.is_a?(String) }
+ end
+ # aliases empty?
+ alias_method :blank?, :empty?
+
+ # Returns an xml formatted representation of the Errors hash.
+ #
+ # 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>
+ # # <error>name can't be blank</error>
+ # # <error>name must be specified</error>
+ # # </errors>
+ def to_xml(options={})
+ to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
+ end
+
+ # Returns a Hash that can be used as the JSON representation for this
+ # object. You can pass the <tt>:full_messages</tt> option. This determines
+ # if the json object should contain full messages or not (false by default).
+ #
+ # person.errors.as_json # => {:name=>["cannot be nil"]}
+ # person.errors.as_json(full_messages: true) # => {:name=>["name cannot 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.errors.to_hash # => {:name=>["cannot be nil"]}
+ # person.errors.to_hash(true) # => {:name=>["name cannot 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) }
+ end
+ messages
+ else
+ self.messages.dup
+ end
+ end
+
+ # Adds +message+ to the error messages on +attribute+. More than one error
+ # can be added to the same +attribute+. If no +message+ is supplied,
+ # <tt>:invalid</tt> is assumed.
+ #
+ # 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 the <tt>:strict</tt> option is set to +true+, it 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 # => {}
+ #
+ # +attribute+ should be set to <tt>:base</tt> if the error is not
+ # directly associated with a single attribute.
+ #
+ # person.errors.add(:base, "either name or email must be present")
+ # person.errors.messages
+ # # => {:base=>["either name or email must be present"]}
+ def add(attribute, message = :invalid, options = {})
+ message = normalize_message(attribute, message, options)
+ 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.
+ #
+ # person.errors.add_on_empty(:name)
+ # person.errors.messages
+ # # => {:name=>["can't be empty"]}
+ def add_on_empty(attributes, options = {})
+ Array(attributes).each do |attribute|
+ value = @base.send(:read_attribute_for_validation, attribute)
+ is_empty = value.respond_to?(:empty?) ? value.empty? : false
+ add(attribute, :empty, options) if value.nil? || is_empty
+ end
+ end
+
+ # 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 = {})
+ Array(attributes).each do |attribute|
+ value = @base.send(:read_attribute_for_validation, attribute)
+ add(attribute, :blank, options) if value.blank?
+ 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+.
+ #
+ # person.errors.add :name, :blank
+ # person.errors.added? :name, :blank # => true
+ def added?(attribute, message = :invalid, options = {})
+ message = normalize_message(attribute, message, options)
+ self[attribute].include? message
+ end
+
+ # Returns all the full error messages in an array.
+ #
+ # class Person
+ # validates_presence_of :name, :address, :email
+ # validates_length_of :name, in: 5..30
+ # end
+ #
+ # 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 all the full error messages for a given attribute in an array.
+ #
+ # class Person
+ # validates_presence_of :name, :email
+ # validates_length_of :name, in: 5..30
+ # end
+ #
+ # person = Person.create()
+ # person.errors.full_messages_for(:name)
+ # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
+ def full_messages_for(attribute)
+ (get(attribute) || []).map { |message| full_message(attribute, message) }
+ end
+
+ # Returns a full message for a given attribute.
+ #
+ # person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
+ def full_message(attribute, message)
+ return message if attribute == :base
+ attr_name = attribute.to_s.tr('.', '_').humanize
+ attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
+ I18n.t(:"errors.format", {
+ default: "%{attribute} %{message}",
+ attribute: attr_name,
+ message: message
+ })
+ end
+
+ # Translates an error message in its default scope
+ # (<tt>activemodel.errors.messages</tt>).
+ #
+ # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
+ # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if
+ # that is not there also, it returns the translation of the default message
+ # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model
+ # name, translated attribute name and the value are available for
+ # interpolation.
+ #
+ # When using inheritance in your models, it will check all the inherited
+ # models too, but only if the model itself hasn't been found. Say you have
+ # <tt>class Admin < User; end</tt> and you wanted the translation for
+ # the <tt>:blank</tt> error message for the <tt>title</tt> attribute,
+ # it looks for these translations:
+ #
+ # * <tt>activemodel.errors.models.admin.attributes.title.blank</tt>
+ # * <tt>activemodel.errors.models.admin.blank</tt>
+ # * <tt>activemodel.errors.models.user.attributes.title.blank</tt>
+ # * <tt>activemodel.errors.models.user.blank</tt>
+ # * any default you provided through the +options+ hash (in the <tt>activemodel.errors</tt> scope)
+ # * <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)
+
+ if @base.class.respond_to?(:i18n_scope)
+ defaults = @base.class.lookup_ancestors.map do |klass|
+ [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
+ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
+ end
+ else
+ defaults = []
+ end
+
+ defaults << options.delete(:message)
+ defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
+ defaults << :"errors.attributes.#{attribute}.#{type}"
+ defaults << :"errors.messages.#{type}"
+
+ defaults.compact!
+ defaults.flatten!
+
+ key = defaults.shift
+ value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
+
+ options = {
+ default: defaults,
+ model: @base.class.model_name.human,
+ attribute: @base.class.human_attribute_name(attribute),
+ value: value
+ }.merge!(options)
+
+ I18n.translate(key, options)
+ end
+
+ private
+ def normalize_message(attribute, message, options)
+ case message
+ when Symbol
+ generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
+ when Proc
+ message.call
+ else
+ message
+ end
+ 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..7468f95548
--- /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)
+ if attributes.respond_to?(:permitted?) && !attributes.permitted?
+ raise ActiveModel::ForbiddenAttributesError
+ else
+ attributes
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
new file mode 100644
index 0000000000..964b24398d
--- /dev/null
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveModel
+ # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
new file mode 100644
index 0000000000..c6bc18b008
--- /dev/null
+++ b/activemodel/lib/active_model/lint.rb
@@ -0,0 +1,110 @@
+module ActiveModel
+ module Lint
+ # == Active \Model \Lint \Tests
+ #
+ # You can test whether an object is compliant with the Active \Model API by
+ # including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will
+ # include tests that tell you whether your object is fully compliant,
+ # or if not, which aspects of the API are not implemented.
+ #
+ # Note an object is not required to implement all APIs in order to work
+ # with Action Pack. This module only intends to provide guidance in case
+ # you want all features out of the box.
+ #
+ # These tests do not attempt to determine the semantic correctness of the
+ # returned values. For instance, you could implement <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 <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 <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
+ assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
+ end
+
+ # == Responds to <tt>to_param</tt>
+ #
+ # Returns a string representing the object's key suitable for use in URLs
+ # 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 <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
+ def model.persisted?() false end
+ assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
+ end
+
+ # == Responds to <tt>to_partial_path</tt>
+ #
+ # 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
+ end
+
+ # == Responds to <tt>persisted?</tt>
+ #
+ # Returns a boolean that specifies whether the object has been persisted
+ # yet. This is used when calculating the URL for an object. If the object
+ # is not persisted, a form for that object, for instance, will 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?"
+ end
+
+ # == \Naming
+ #
+ # Model.model_name must return a string with some convenience methods:
+ # <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 model_name.respond_to?(:to_str)
+ assert model_name.human.respond_to?(:to_str)
+ assert model_name.singular.respond_to?(:to_str)
+ assert model_name.plural.respond_to?(:to_str)
+ end
+
+ # == \Errors Testing
+ #
+ # Returns an object that implements [](attribute) defined which returns an
+ # Array of Strings that are the errors for the attribute in question.
+ # If localization is used, the Strings should be localized for the current
+ # locale. If no error is present, this method should return an empty Array.
+ def test_errors_aref
+ assert model.respond_to?(:errors), "The model should respond to errors"
+ assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
+ end
+
+ private
+ def model
+ assert @model.respond_to?(:to_model), "The object should respond to to_model"
+ @model.to_model
+ end
+
+ def assert_boolean(result, name)
+ assert result == true || result == false, "#{name} should be a boolean"
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml
new file mode 100644
index 0000000000..bf07945fe1
--- /dev/null
+++ b/activemodel/lib/active_model/locale/en.yml
@@ -0,0 +1,35 @@
+en:
+ errors:
+ # The default format to use in full error messages.
+ format: "%{attribute} %{message}"
+
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "is not included in the list"
+ exclusion: "is reserved"
+ invalid: "is invalid"
+ confirmation: "doesn't match %{attribute}"
+ accepted: "must be accepted"
+ empty: "can't be empty"
+ blank: "can't be blank"
+ present: "must be blank"
+ too_long:
+ one: "is too long (maximum is 1 character)"
+ other: "is too long (maximum is %{count} characters)"
+ too_short:
+ one: "is too short (minimum is 1 character)"
+ other: "is too short (minimum is %{count} characters)"
+ wrong_length:
+ one: "is the wrong length (should be 1 character)"
+ other: "is the wrong length (should be %{count} characters)"
+ not_a_number: "is not a number"
+ not_an_integer: "must be an integer"
+ greater_than: "must be greater than %{count}"
+ greater_than_or_equal_to: "must be greater than or equal to %{count}"
+ equal_to: "must be equal to %{count}"
+ less_than: "must be less than %{count}"
+ less_than_or_equal_to: "must be less than or equal to %{count}"
+ other_than: "must be other than %{count}"
+ odd: "must be odd"
+ even: "must be even"
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
new file mode 100644
index 0000000000..640024eaa1
--- /dev/null
+++ b/activemodel/lib/active_model/model.rb
@@ -0,0 +1,99 @@
+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
+
+ super()
+ 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
new file mode 100644
index 0000000000..241e88deeb
--- /dev/null
+++ b/activemodel/lib/active_model/naming.rb
@@ -0,0 +1,317 @@
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/module/introspection'
+
+module ActiveModel
+ 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
+
+ ##
+ # :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
+
+ ##
+ # :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
+
+ ##
+ # :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
+
+ ##
+ # :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
+
+ ##
+ # :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(@name)
+ @plural = ActiveSupport::Inflector.pluralize(@singular)
+ @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
+ @human = ActiveSupport::Inflector.humanize(@element)
+ @collection = ActiveSupport::Inflector.tableize(@name)
+ @param_key = (namespace ? _singularize(@unnamespaced) : @singular)
+ @i18n_key = @name.underscore.to_sym
+
+ @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup)
+ @singular_route_key = ActiveSupport::Inflector.singularize(@route_key)
+ @route_key << "_index" if @plural == @singular
+ end
+
+ # Transform the model name into a more humane format, using I18n. By default,
+ # it will underscore then humanize the class name.
+ #
+ # class BlogPost
+ # extend ActiveModel::Naming
+ # end
+ #
+ # BlogPost.model_name.human # => "Blog post"
+ #
+ # Specify +options+ with additional translating options.
+ def human(options={})
+ return @human unless @klass.respond_to?(:lookup_ancestors) &&
+ @klass.respond_to?(:i18n_scope)
+
+ defaults = @klass.lookup_ancestors.map do |klass|
+ klass.model_name.i18n_key
+ end
+
+ defaults << options[:default] if options[:default]
+ defaults << @human
+
+ options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
+ I18n.translate(defaults.shift, options)
+ end
+
+ private
+
+ def _singularize(string, replacement='_')
+ ActiveSupport::Inflector.underscore(string).tr('/', replacement)
+ end
+ end
+
+ # == Active \Model \Naming
+ #
+ # Creates a +model_name+ method on your object.
+ #
+ # To implement, just extend ActiveModel::Naming in your object:
+ #
+ # class BookCover
+ # extend ActiveModel::Naming
+ # end
+ #
+ # BookCover.model_name.name # => "BookCover"
+ # BookCover.model_name.human # => "Book cover"
+ #
+ # BookCover.model_name.i18n_key # => :book_cover
+ # BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
+ #
+ # 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.
+ module Naming
+ def self.extended(base) #:nodoc:
+ base.remove_possible_method :model_name
+ base.delegate :model_name, to: :class
+ end
+
+ # Returns an ActiveModel::Name object for module. It can be
+ # used to retrieve all kinds of naming-related information
+ # (See ActiveModel::Name for more information).
+ #
+ # class Person
+ # include ActiveModel::Model
+ # end
+ #
+ # Person.model_name.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|
+ n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
+ end
+ ActiveModel::Name.new(self, namespace)
+ end
+ end
+
+ # Returns the plural class name of a record or class.
+ #
+ # ActiveModel::Naming.plural(post) # => "posts"
+ # ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people"
+ def self.plural(record_or_class)
+ model_name_from_record_or_class(record_or_class).plural
+ end
+
+ # Returns the singular class name of a record or class.
+ #
+ # ActiveModel::Naming.singular(post) # => "post"
+ # ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person"
+ def self.singular(record_or_class)
+ model_name_from_record_or_class(record_or_class).singular
+ end
+
+ # Identifies whether the class name of a record or class is uncountable.
+ #
+ # ActiveModel::Naming.uncountable?(Sheep) # => true
+ # ActiveModel::Naming.uncountable?(Post) # => false
+ def self.uncountable?(record_or_class)
+ plural(record_or_class) == singular(record_or_class)
+ end
+
+ # Returns string to use while generating route names. It differs for
+ # namespaced models regarding whether it's inside isolated engine.
+ #
+ # # For isolated engine:
+ # ActiveModel::Naming.singular_route_key(Blog::Post) # => "post"
+ #
+ # # For shared engine:
+ # ActiveModel::Naming.singular_route_key(Blog::Post) # => "blog_post"
+ def self.singular_route_key(record_or_class)
+ model_name_from_record_or_class(record_or_class).singular_route_key
+ end
+
+ # Returns string to use while generating route names. It differs for
+ # namespaced models regarding whether it's inside isolated engine.
+ #
+ # # For isolated engine:
+ # ActiveModel::Naming.route_key(Blog::Post) # => "posts"
+ #
+ # # For shared engine:
+ # ActiveModel::Naming.route_key(Blog::Post) # => "blog_posts"
+ #
+ # The route key also considers if the noun is uncountable and, in
+ # such cases, automatically appends _index.
+ def self.route_key(record_or_class)
+ model_name_from_record_or_class(record_or_class).route_key
+ end
+
+ # Returns string to use for params names. It differs for
+ # namespaced models regarding whether it's inside isolated engine.
+ #
+ # # For isolated engine:
+ # ActiveModel::Naming.param_key(Blog::Post) # => "post"
+ #
+ # # For shared engine:
+ # ActiveModel::Naming.param_key(Blog::Post) # => "blog_post"
+ def self.param_key(record_or_class)
+ model_name_from_record_or_class(record_or_class).param_key
+ end
+
+ 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/railtie.rb b/activemodel/lib/active_model/railtie.rb
new file mode 100644
index 0000000000..1671eb7bd4
--- /dev/null
+++ b/activemodel/lib/active_model/railtie.rb
@@ -0,0 +1,12 @@
+require "active_model"
+require "rails"
+
+module ActiveModel
+ class Railtie < Rails::Railtie # :nodoc:
+ config.eager_load_namespaces << ActiveModel
+
+ initializer "active_model.secure_password" do
+ ActiveModel::SecurePassword.min_cost = Rails.env.test?
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
new file mode 100644
index 0000000000..f6ad35769f
--- /dev/null
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -0,0 +1,134 @@
+module ActiveModel
+ module SecurePassword
+ extend ActiveSupport::Concern
+
+ # BCrypt hash function can handle maximum 72 characters, and if we pass
+ # password of length more than 72 characters it ignores extra characters.
+ # Hence need to put a restriction on password length.
+ MAX_PASSWORD_LENGTH_ALLOWED = 72
+
+ class << self
+ attr_accessor :min_cost # :nodoc:
+ end
+ self.min_cost = false
+
+ module ClassMethods
+ # Adds methods to set and authenticate against a BCrypt password.
+ # This mechanism requires you to have a +password_digest+ attribute.
+ #
+ # The following validations are added automatically:
+ # * Password must be present on creation
+ # * Password length should be less than or equal to 72 characters
+ # * Confirmation of password (using a +password_confirmation+ attribute)
+ #
+ # If password confirmation validation is not needed, simply leave out the
+ # value for +password_confirmation+ (i.e. don't provide a form field for
+ # it). When this attribute has a +nil+ value, the validation will not be
+ # triggered.
+ #
+ # For further customizability, it is possible to supress the default
+ # validations by passing <tt>validations: false</tt> as an argument.
+ #
+ # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
+ #
+ # gem 'bcrypt', '~> 3.1.7'
+ #
+ # Example using Active Record (which automatically includes ActiveModel::SecurePassword):
+ #
+ # # Schema: User(name:string, password_digest:string)
+ # class User < ActiveRecord::Base
+ # has_secure_password
+ # end
+ #
+ # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
+ # user.save # => false, password required
+ # user.password = 'mUc3m00RsqyRe'
+ # user.save # => false, confirmation doesn't match
+ # user.password_confirmation = 'mUc3m00RsqyRe'
+ # user.save # => true
+ # user.authenticate('notright') # => false
+ # user.authenticate('mUc3m00RsqyRe') # => user
+ # User.find_by(name: 'david').try(:authenticate, 'notright') # => false
+ # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
+ def has_secure_password(options = {})
+ # Load bcrypt gem only when has_secure_password is used.
+ # This is to avoid ActiveModel (and by extension the entire framework)
+ # being dependent on a binary library.
+ begin
+ require 'bcrypt'
+ rescue LoadError
+ $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
+ raise
+ end
+
+ include InstanceMethodsOnActivation
+
+ if options.fetch(:validations, true)
+ include ActiveModel::Validations
+
+ # This ensures the model has a password by checking whether the password_digest
+ # is present, so that this works with both new and existing records. However,
+ # when there is an error, the message is added to the password attribute instead
+ # so that the error message will make sense to the end-user.
+ validate do |record|
+ record.errors.add(:password, :blank) unless record.password_digest.present?
+ end
+
+ validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
+ validates_confirmation_of :password, if: ->{ password.present? }
+ end
+
+ # This code is necessary as long as the protected_attributes gem is supported.
+ if respond_to?(:attributes_protected_by_default)
+ def self.attributes_protected_by_default #:nodoc:
+ super + ['password_digest']
+ end
+ end
+ end
+ end
+
+ module InstanceMethodsOnActivation
+ # 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)
+ BCrypt::Password.new(password_digest) == unencrypted_password && self
+ end
+
+ attr_reader :password
+
+ # Encrypts the password into the +password_digest+ attribute, only if the
+ # new password is not empty.
+ #
+ # 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)
+ if unencrypted_password.nil?
+ self.password_digest = nil
+ elsif !unencrypted_password.empty?
+ @password = unencrypted_password
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+ self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
+ end
+ end
+
+ def password_confirmation=(unencrypted_password)
+ @password_confirmation = unencrypted_password
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
new file mode 100644
index 0000000000..976f50b13e
--- /dev/null
+++ b/activemodel/lib/active_model/serialization.rb
@@ -0,0 +1,163 @@
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/slice'
+
+module ActiveModel
+ # == Active \Model \Serialization
+ #
+ # Provides a basic serialization to a serializable_hash for your objects.
+ #
+ # A minimal implementation could be:
+ #
+ # class Person
+ # include ActiveModel::Serialization
+ #
+ # attr_accessor :name
+ #
+ # def attributes
+ # {'name' => nil}
+ # end
+ # end
+ #
+ # Which would provide you with:
+ #
+ # person = Person.new
+ # person.serializable_hash # => {"name"=>nil}
+ # person.name = "Bob"
+ # person.serializable_hash # => {"name"=>"Bob"}
+ #
+ # An +attributes+ hash must be defined and should contain any attributes you
+ # need to be serialized. 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, either the JSON or XML serializations are needed.
+ # Both of these modules automatically include the
+ # <tt>ActiveModel::Serialization</tt> module, so there is no need to
+ # explicitly include it.
+ #
+ # A minimal implementation including XML and JSON would be:
+ #
+ # class Person
+ # include ActiveModel::Serializers::JSON
+ # include ActiveModel::Serializers::Xml
+ #
+ # attr_accessor :name
+ #
+ # def attributes
+ # {'name' => nil}
+ # end
+ # end
+ #
+ # Which would provide you with:
+ #
+ # person = Person.new
+ # person.serializable_hash # => {"name"=>nil}
+ # person.as_json # => {"name"=>nil}
+ # person.to_json # => "{\"name\":null}"
+ # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
+ #
+ # person.name = "Bob"
+ # person.serializable_hash # => {"name"=>"Bob"}
+ # person.as_json # => {"name"=>"Bob"}
+ # person.to_json # => "{\"name\":\"Bob\"}"
+ # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
+ #
+ # Valid options are <tt>:only</tt>, <tt>:except</tt>, <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
+ if only = options[:only]
+ attribute_names &= Array(only).map(&:to_s)
+ elsif except = options[:except]
+ attribute_names -= Array(except).map(&:to_s)
+ end
+
+ hash = {}
+ attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
+
+ Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) }
+
+ serializable_add_includes(options) do |association, records, 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
+ end
+
+ hash
+ end
+
+ private
+
+ # Hook method defining how an attribute value should be retrieved for
+ # serialization. By default this is assumed to be an instance named after
+ # the attribute. Override this method in subclasses should you need to
+ # retrieve the value for a given attribute differently:
+ #
+ # class MyClass
+ # include ActiveModel::Serialization
+ #
+ # def initialize(data = {})
+ # @data = data
+ # end
+ #
+ # def read_attribute_for_serialization(key)
+ # @data[key]
+ # end
+ # end
+ alias :read_attribute_for_serialization :send
+
+ # Add associations specified via the <tt>:include</tt> option.
+ #
+ # Expects a block that takes as arguments:
+ # +association+ - name of the association
+ # +records+ - the association record(s) to be serialized
+ # +opts+ - options for the association records
+ def serializable_add_includes(options = {}) #:nodoc:
+ return unless includes = options[:include]
+
+ unless includes.is_a?(Hash)
+ includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
+ end
+
+ includes.each do |association, opts|
+ if records = send(association)
+ yield association, records, opts
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
new file mode 100644
index 0000000000..c58e73f6a7
--- /dev/null
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -0,0 +1,145 @@
+require 'active_support/json'
+
+module ActiveModel
+ module Serializers
+ # == Active \Model \JSON \Serializer
+ module JSON
+ extend ActiveSupport::Concern
+ include ActiveModel::Serialization
+
+ included do
+ extend ActiveModel::Naming
+
+ 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+, +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
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
+ # # "created_at" => "2006/08/01", "awesome" => true}
+ #
+ # ActiveRecord::Base.include_root_in_json = true
+ #
+ # user.as_json
+ # # => { "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 +true+ as in:
+ #
+ # user = User.find(1)
+ # 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.
+ #
+ # user = User.find(1)
+ # user.as_json
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
+ # # "created_at" => "2006/08/01", "awesome" => true}
+ #
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
+ # the attributes included, and work similar to the +attributes+ method.
+ #
+ # user.as_json(only: [:id, :name])
+ # # => { "id" => 1, "name" => "Konata Izumi" }
+ #
+ # 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" }
+ #
+ # 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" } ] }
+ #
+ # 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" } ] }
+ def as_json(options = nil)
+ 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) }
+ else
+ serializable_hash(options)
+ 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|
+ # send("#{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
+ self.attributes = hash
+ self
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb
new file mode 100644
index 0000000000..7f99536dbb
--- /dev/null
+++ b/activemodel/lib/active_model/serializers/xml.rb
@@ -0,0 +1,238 @@
+require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/core_ext/array/conversions'
+require 'active_support/core_ext/hash/conversions'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/time/acts_like'
+
+module ActiveModel
+ module Serializers
+ # == Active Model XML Serializer
+ module Xml
+ extend ActiveSupport::Concern
+ include ActiveModel::Serialization
+
+ included do
+ extend ActiveModel::Naming
+ end
+
+ class Serializer #:nodoc:
+ class Attribute #:nodoc:
+ attr_reader :name, :value, :type
+
+ def initialize(name, serializable, value)
+ @name, @serializable = name, serializable
+
+ if value.acts_like?(:time) && value.respond_to?(:in_time_zone)
+ value = value.in_time_zone
+ end
+
+ @value = value
+ @type = compute_type
+ end
+
+ def decorations
+ decorations = {}
+ decorations[:encoding] = 'base64' if type == :binary
+ decorations[:type] = (type == :string) ? nil : type
+ decorations[:nil] = true if value.nil?
+ decorations
+ end
+
+ protected
+
+ def compute_type
+ return if value.nil?
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
+ type ||= :string if value.respond_to?(:to_str)
+ type ||= :yaml
+ type
+ end
+ end
+
+ class MethodAttribute < Attribute #:nodoc:
+ end
+
+ attr_reader :options
+
+ def initialize(serializable, options = nil)
+ @serializable = serializable
+ @options = options ? options.dup : {}
+ end
+
+ def serializable_hash
+ @serializable.serializable_hash(@options.except(:include))
+ end
+
+ def serializable_collection
+ methods = Array(options[:methods]).map(&:to_s)
+ serializable_hash.map do |name, value|
+ name = name.to_s
+ if methods.include?(name)
+ self.class::MethodAttribute.new(name, @serializable, value)
+ else
+ self.class::Attribute.new(name, @serializable, value)
+ end
+ end
+ end
+
+ def serialize
+ require 'builder' unless defined? ::Builder
+
+ options[:indent] ||= 2
+ options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
+
+ @builder = options[:builder]
+ @builder.instruct! unless options[:skip_instruct]
+
+ root = (options[:root] || @serializable.class.model_name.element).to_s
+ root = ActiveSupport::XmlMini.rename_key(root, options)
+
+ args = [root]
+ args << { xmlns: options[:namespace] } if options[:namespace]
+ args << { type: options[:type] } if options[:type] && !options[:skip_types]
+
+ @builder.tag!(*args) do
+ add_attributes_and_methods
+ add_includes
+ add_extra_behavior
+ add_procs
+ yield @builder if block_given?
+ end
+ end
+
+ private
+
+ def add_extra_behavior
+ end
+
+ def add_attributes_and_methods
+ serializable_collection.each do |attribute|
+ key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
+ ActiveSupport::XmlMini.to_tag(key, attribute.value,
+ options.merge(attribute.decorations))
+ end
+ end
+
+ def add_includes
+ @serializable.send(:serializable_add_includes, options) do |association, records, opts|
+ add_associations(association, records, opts)
+ end
+ end
+
+ # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
+ def add_associations(association, records, opts)
+ merged_options = opts.merge(options.slice(:builder, :indent))
+ merged_options[:skip_instruct] = true
+
+ [:skip_types, :dasherize, :camelize].each do |key|
+ merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil?
+ end
+
+ if records.respond_to?(:to_ary)
+ records = records.to_ary
+
+ tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
+ type = options[:skip_types] ? { } : { type: "array" }
+ association_name = association.to_s.singularize
+ merged_options[:root] = association_name
+
+ if records.empty?
+ @builder.tag!(tag, type)
+ else
+ @builder.tag!(tag, type) do
+ records.each do |record|
+ if options[:skip_types]
+ record_type = {}
+ else
+ record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
+ record_type = { type: record_class }
+ end
+
+ record.to_xml merged_options.merge(record_type)
+ end
+ end
+ end
+ else
+ merged_options[:root] = association.to_s
+
+ unless records.class.to_s.underscore == association.to_s
+ merged_options[:type] = records.class.name
+ end
+
+ records.to_xml merged_options
+ end
+ end
+
+ def add_procs
+ if procs = options.delete(:procs)
+ Array(procs).each do |proc|
+ if proc.arity == 1
+ proc.call(options)
+ else
+ proc.call(options, @serializable)
+ end
+ end
+ end
+ end
+ end
+
+ # Returns XML representing the model. Configuration can be
+ # passed through +options+.
+ #
+ # Without any +options+, the returned XML string will include all the
+ # model's attributes.
+ #
+ # user = User.find(1)
+ # user.to_xml
+ #
+ # <?xml version="1.0" encoding="UTF-8"?>
+ # <user>
+ # <id type="integer">1</id>
+ # <name>David</name>
+ # <age type="integer">16</age>
+ # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at>
+ # </user>
+ #
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the
+ # attributes included, and work similar to the +attributes+ method.
+ #
+ # To include the result of some method calls on the model use <tt>:methods</tt>.
+ #
+ # To include associations use <tt>:include</tt>.
+ #
+ # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt>
+ def to_xml(options = {}, &block)
+ Serializer.new(self, options).serialize(&block)
+ end
+
+ # Sets the model +attributes+ from an XML string. Returns +self+.
+ #
+ # class Person
+ # include ActiveModel::Serializers::Xml
+ #
+ # attr_accessor :name, :age, :awesome
+ #
+ # def attributes=(hash)
+ # hash.each do |key, value|
+ # instance_variable_set("@#{key}", value)
+ # end
+ # end
+ #
+ # def attributes
+ # instance_values
+ # end
+ # end
+ #
+ # xml = { name: 'bob', age: 22, awesome:true }.to_xml
+ # person = Person.new
+ # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob">
+ # person.name # => "bob"
+ # person.age # => 22
+ # person.awesome # => true
+ def from_xml(xml)
+ self.attributes = Hash.from_xml(xml).values.first
+ self
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb
new file mode 100644
index 0000000000..5004855d56
--- /dev/null
+++ b/activemodel/lib/active_model/test_case.rb
@@ -0,0 +1,4 @@
+module ActiveModel #:nodoc:
+ class TestCase < ActiveSupport::TestCase #:nodoc:
+ end
+end
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
new file mode 100644
index 0000000000..8470915abb
--- /dev/null
+++ b/activemodel/lib/active_model/translation.rb
@@ -0,0 +1,69 @@
+module ActiveModel
+
+ # == Active \Model \Translation
+ #
+ # Provides integration between your object and the Rails internationalization
+ # (i18n) framework.
+ #
+ # A minimal implementation could be:
+ #
+ # class TranslatedPerson
+ # extend ActiveModel::Translation
+ # end
+ #
+ # TranslatedPerson.human_attribute_name('my_attribute')
+ # # => "My attribute"
+ #
+ # This also provides the required class methods for hooking into the
+ # Rails internationalization API, including being able to define a
+ # class based +i18n_scope+ and +lookup_ancestors+ to find translations in
+ # parent classes.
+ module Translation
+ include ActiveModel::Naming
+
+ # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
+ def i18n_scope
+ :activemodel
+ end
+
+ # When localizing a string, it goes through the lookup returned by this
+ # method, which is used in ActiveModel::Name#human,
+ # ActiveModel::Errors#full_messages and
+ # ActiveModel::Translation#human_attribute_name.
+ def lookup_ancestors
+ self.ancestors.select { |x| x.respond_to?(:model_name) }
+ end
+
+ # Transforms attribute names into a more human format, such as "First name"
+ # instead of "first_name".
+ #
+ # Person.human_attribute_name("first_name") # => "First name"
+ #
+ # Specify +options+ with additional translating options.
+ def human_attribute_name(attribute, options = {})
+ options = { count: 1 }.merge!(options)
+ parts = attribute.to_s.split(".")
+ attribute = parts.pop
+ namespace = parts.join("/") unless parts.empty?
+ attributes_scope = "#{self.i18n_scope}.attributes"
+
+ if namespace
+ defaults = lookup_ancestors.map do |klass|
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
+ end
+ defaults << :"#{attributes_scope}.#{namespace}.#{attribute}"
+ else
+ defaults = lookup_ancestors.map do |klass|
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}"
+ end
+ end
+
+ defaults << :"attributes.#{attribute}"
+ defaults << options.delete(:default) if options[:default]
+ defaults << attribute.humanize
+
+ options[:default] = defaults
+ I18n.translate(defaults.shift, options)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
new file mode 100644
index 0000000000..f67a3be5c1
--- /dev/null
+++ b/activemodel/lib/active_model/validations.rb
@@ -0,0 +1,392 @@
+require 'active_support/core_ext/array/extract_options'
+require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/hash/except'
+
+module ActiveModel
+
+ # == Active \Model \Validations
+ #
+ # Provides a full validation framework to your objects.
+ #
+ # A minimal implementation could be:
+ #
+ # class Person
+ # include ActiveModel::Validations
+ #
+ # attr_accessor :first_name, :last_name
+ #
+ # validates_each :first_name, :last_name do |record, attr, value|
+ # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
+ # end
+ # end
+ #
+ # Which provides you with the full standard validation stack that you
+ # know from Active Record:
+ #
+ # person = Person.new
+ # person.valid? # => true
+ # person.invalid? # => false
+ #
+ # person.first_name = 'zoolander'
+ # person.valid? # => false
+ # person.invalid? # => true
+ # 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
+
+ included do
+ extend ActiveModel::Callbacks
+ extend ActiveModel::Translation
+
+ extend HelperMethods
+ include HelperMethods
+
+ attr_accessor :validation_context
+ define_callbacks :validate, scope: :name
+
+ class_attribute :_validators
+ self._validators = Hash.new { |h,k| h[k] = [] }
+ end
+
+ module ClassMethods
+ # Validates each attribute against a block.
+ #
+ # class Person
+ # include ActiveModel::Validations
+ #
+ # attr_accessor :first_name, :last_name
+ #
+ # 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 contexts where this validation is active.
+ # You can pass a symbol or an array of symbols.
+ # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :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 validates_each(*attr_names, &block)
+ validates_with BlockValidator, _merge_attributes(attr_names), &block
+ end
+
+ # Adds a validation method or block to the class. This is useful when
+ # overriding the +validate+ instance method becomes too unwieldy and
+ # you're looking for more descriptive declaration of your validations.
+ #
+ # This can be done with a symbol pointing to a method:
+ #
+ # class Comment
+ # include ActiveModel::Validations
+ #
+ # validate :must_be_friends
+ #
+ # def must_be_friends
+ # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
+ # end
+ # end
+ #
+ # With a block which is passed with the current record to be validated:
+ #
+ # class Comment
+ # include ActiveModel::Validations
+ #
+ # validate do |comment|
+ # comment.must_be_friends
+ # end
+ #
+ # def must_be_friends
+ # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
+ # end
+ # end
+ #
+ # Or with a block where self points to the current record to be validated:
+ #
+ # class Comment
+ # include ActiveModel::Validations
+ #
+ # validate do
+ # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
+ # end
+ # end
+ #
+ # Options:
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # You can pass a symbol or an array of symbols.
+ # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</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.
+ def validate(*args, &block)
+ options = args.extract_options!
+
+ if args.all? { |arg| arg.is_a?(Symbol) }
+ options.assert_valid_keys([:on, :if, :unless])
+ end
+
+ if options.key?(:on)
+ options = options.dup
+ options[:if] = Array(options[:if])
+ options[:if].unshift lambda { |o|
+ Array(options[:on]).include?(o.validation_context)
+ }
+ end
+
+ args << options
+ set_callback(:validate, *args, &block)
+ end
+
+ # 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
+
+ # Clears all of the validators and validations.
+ #
+ # Note that this will clear anything that is being used to validate
+ # the model for both the +validates_with+ and +validate+ methods.
+ # It clears the validators that are created with an invocation of
+ # +validates_with+ and the callbacks that are set by an invocation
+ # of +validate+.
+ #
+ # class Person
+ # include ActiveModel::Validations
+ #
+ # validates_with MyValidator
+ # validates_with OtherValidator, on: :create
+ # validates_with StrictValidator, strict: true
+ # validate :cannot_be_robot
+ #
+ # def cannot_be_robot
+ # errors.add(:base, 'A person cannot be a robot') if person_is_robot
+ # end
+ # end
+ #
+ # Person.validators
+ # # => [
+ # # #<MyValidator:0x007fbff403e808 @options={}>,
+ # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
+ # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
+ # # ]
+ #
+ # If one runs <tt>Person.clear_validators!</tt> and then checks to see what
+ # validators this class has, you would obtain:
+ #
+ # Person.validators # => []
+ #
+ # Also, the callback set by <tt>validate :cannot_be_robot</tt> will be erased
+ # so that:
+ #
+ # Person._validate_callbacks.empty? # => true
+ #
+ def clear_validators!
+ reset_callbacks(:validate)
+ _validators.clear
+ end
+
+ # List all validators that are being used to validate a specific attribute.
+ #
+ # class Person
+ # 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={}>,
+ # # ]
+ def validators_on(*attributes)
+ attributes.flat_map do |attribute|
+ _validators[attribute.to_sym]
+ end
+ end
+
+ # 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) #:nodoc:
+ dup = _validators.dup
+ base._validators = dup.each { |k, v| dup[k] = v.dup }
+ super
+ end
+ end
+
+ # 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+.
+ #
+ # Aliased as validate.
+ #
+ # 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
+ run_validations!
+ ensure
+ self.validation_context = current_context
+ end
+
+ alias_method :validate, :valid?
+
+ # 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
+
+ # Hook method defining how an attribute value should be retrieved. By default
+ # this is assumed to be an instance named after the attribute. Override this
+ # method in subclasses should you need to retrieve the value for a given
+ # attribute differently:
+ #
+ # class MyClass
+ # include ActiveModel::Validations
+ #
+ # def initialize(data = {})
+ # @data = data
+ # end
+ #
+ # def read_attribute_for_validation(key)
+ # @data[key]
+ # end
+ # end
+ alias :read_attribute_for_validation :send
+
+ protected
+
+ def run_validations! #:nodoc:
+ run_callbacks :validate
+ errors.empty?
+ end
+ end
+end
+
+Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file }
diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb
new file mode 100644
index 0000000000..9b5416fb1d
--- /dev/null
+++ b/activemodel/lib/active_model/validations/absence.rb
@@ -0,0 +1,31 @@
+module ActiveModel
+ module Validations
+ # == Active Model Absence Validator
+ class AbsenceValidator < EachValidator #:nodoc:
+ def validate_each(record, attr_name, value)
+ record.errors.add(attr_name, :present, options) if value.present?
+ end
+ end
+
+ module HelperMethods
+ # Validates that the specified attributes are blank (as defined by
+ # Object#blank?). Happens by default on save.
+ #
+ # class Person < ActiveRecord::Base
+ # validates_absence_of :first_name
+ # end
+ #
+ # The first_name attribute must be in the object and it must be blank.
+ #
+ # Configuration options:
+ # * <tt>:message</tt> - A custom error message (default is: "must be blank").
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
+ # See <tt>ActiveModel::Validation#validates</tt> for more information
+ def validates_absence_of(*attr_names)
+ validates_with AbsenceValidator, _merge_attributes(attr_names)
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
new file mode 100644
index 0000000000..ac5e79859b
--- /dev/null
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -0,0 +1,55 @@
+module ActiveModel
+
+ module Validations
+ class AcceptanceValidator < EachValidator # :nodoc:
+ def initialize(options)
+ super({ allow_nil: true, accept: "1" }.merge!(options))
+ setup!(options[:class])
+ end
+
+ def validate_each(record, attribute, value)
+ unless value == options[:accept]
+ record.errors.add(attribute, :accepted, options.except(:accept, :allow_nil))
+ end
+ end
+
+ private
+ def setup!(klass)
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
+ klass.send(:attr_reader, *attr_readers)
+ klass.send(:attr_writer, *attr_writers)
+ end
+ end
+
+ module HelperMethods
+ # Encapsulates the pattern of wanting to validate the acceptance of a
+ # 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'
+ # end
+ #
+ # If the database column does not exist, the +terms_of_service+ attribute
+ # is entirely virtual. This check is performed only if +terms_of_service+
+ # is not +nil+ and by default on save.
+ #
+ # Configuration options:
+ # * <tt>:message</tt> - A custom error message (default is: "must be
+ # accepted").
+ # * <tt>:accept</tt> - Specifies value that is considered accepted.
+ # The default value is a string "1", which makes it easy to relate to
+ # an HTML checkbox. This should be set to +true+ if you are validating
+ # a database column, since the attribute is typecast from "1" to +true+
+ # before validation.
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, 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
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
new file mode 100644
index 0000000000..edfffdd3ce
--- /dev/null
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -0,0 +1,115 @@
+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
+ extend ActiveSupport::Concern
+
+ included do
+ include ActiveSupport::Callbacks
+ define_callbacks :validation,
+ terminator: ->(_,result) { result == false },
+ skip_after_callbacks_if_terminated: true,
+ scope: [:kind, :name]
+ 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]
+ options[:if] = Array(options[:if])
+ options[:on] = Array(options[:on])
+ options[:if].unshift lambda { |o|
+ options[:on].include? o.validation_context
+ }
+ end
+ 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
+ options[:if] = Array(options[:if])
+ if options[:on]
+ options[:on] = Array(options[:on])
+ options[:if].unshift("#{options[:on]}.include? self.validation_context")
+ end
+ set_callback(:validation, :after, *(args << options), &block)
+ end
+ end
+
+ protected
+
+ # Overwrite run validations to include callbacks.
+ def run_validations! #:nodoc:
+ run_callbacks(:validation) { super }
+ end
+ end
+ 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..bad9e4f9a9
--- /dev/null
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -0,0 +1,51 @@
+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)
+ members = if delimiter.respond_to?(:call)
+ delimiter.call(record)
+ elsif delimiter.respond_to?(:to_sym)
+ record.send(delimiter)
+ else
+ delimiter
+ end
+
+ members.send(inclusion_method(members), value)
+ end
+
+ def delimiter
+ @delimiter ||= options[:in] || options[:within]
+ end
+
+ # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # possible values in the range for equality, which is slower but more accurate.
+ # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
+ # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges.
+ def inclusion_method(enumerable)
+ if enumerable.is_a? Range
+ case enumerable.first
+ when Numeric, Time, DateTime
+ :cover?
+ else
+ :include?
+ end
+ else
+ :include?
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
new file mode 100644
index 0000000000..a51523912f
--- /dev/null
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -0,0 +1,67 @@
+module ActiveModel
+
+ module Validations
+ class ConfirmationValidator < EachValidator # :nodoc:
+ def initialize(options)
+ super
+ setup!(options[:class])
+ end
+
+ def validate_each(record, attribute, value)
+ if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed)
+ human_attribute_name = record.class.human_attribute_name(attribute)
+ record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name))
+ end
+ end
+
+ private
+ def setup!(klass)
+ klass.send(:attr_reader, *attributes.map do |attribute|
+ :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
+ end.compact)
+
+ klass.send(:attr_writer, *attributes.map do |attribute|
+ :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
+ end.compact)
+ end
+ end
+
+ module HelperMethods
+ # Encapsulates the pattern of wanting to validate a password or email
+ # 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'
+ # end
+ #
+ # View:
+ # <%= password_field "person", "password" %>
+ # <%= password_field "person", "password_confirmation" %>
+ #
+ # The added +password_confirmation+ attribute is virtual; it exists only
+ # as an in-memory attribute for validating the password. To achieve this,
+ # the validation adds accessors to the model for the confirmation
+ # 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:
+ #
+ # validates_presence_of :password_confirmation, if: :password_changed?
+ #
+ # Configuration options:
+ # * <tt>:message</tt> - A custom error message (default is: "doesn't match
+ # confirmation").
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, 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
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
new file mode 100644
index 0000000000..f342d27275
--- /dev/null
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -0,0 +1,46 @@
+require "active_model/validations/clusivity"
+
+module ActiveModel
+
+ module Validations
+ class ExclusionValidator < EachValidator # :nodoc:
+ include Clusivity
+
+ def validate_each(record, attribute, value)
+ if include?(record, value)
+ record.errors.add(attribute, :exclusion, options.except(:in, :within).merge!(value: value))
+ end
+ end
+ end
+
+ module HelperMethods
+ # 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: ->(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, 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").
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, 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
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
new file mode 100644
index 0000000000..ff3e95da34
--- /dev/null
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -0,0 +1,113 @@
+module ActiveModel
+
+ module Validations
+ class FormatValidator < EachValidator # :nodoc:
+ def validate_each(record, attribute, value)
+ if options[:with]
+ regexp = option_call(record, :with)
+ record_error(record, attribute, :with, value) if value.to_s !~ regexp
+ elsif options[:without]
+ regexp = option_call(record, :without)
+ record_error(record, attribute, :without, value) if value.to_s =~ regexp
+ end
+ end
+
+ def check_validity!
+ unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or"
+ raise ArgumentError, "Either :with or :without must be supplied (but not both)"
+ end
+
+ check_options_validity :with
+ check_options_validity :without
+ end
+
+ private
+
+ def option_call(record, name)
+ option = options[name]
+ option.respond_to?(:call) ? option.call(record) : option
+ end
+
+ def record_error(record, attribute, name, value)
+ record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
+ end
+
+ def check_options_validity(name)
+ if option = options[name]
+ if option.is_a?(Regexp)
+ if options[:multiline] != true && regexp_using_multiline_anchors?(option)
+ 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
+ elsif !option.respond_to?(:call)
+ raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
+ end
+ end
+ end
+
+ def regexp_using_multiline_anchors?(regexp)
+ source = regexp.source
+ source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
+ 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:
+ #
+ # class Person < ActiveRecord::Base
+ # 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:
+ #
+ # class Person < ActiveRecord::Base
+ # 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.
+ #
+ # class Person < ActiveRecord::Base
+ # # Admin can have number as a first letter in their screen name
+ # 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.
+ #
+ # 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>: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+, +:allow_nil+, +:allow_blank+, 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
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
new file mode 100644
index 0000000000..c84025f083
--- /dev/null
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -0,0 +1,46 @@
+require "active_model/validations/clusivity"
+
+module ActiveModel
+
+ module Validations
+ class InclusionValidator < EachValidator # :nodoc:
+ include Clusivity
+
+ def validate_each(record, attribute, value)
+ unless include?(record, value)
+ record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(value: value))
+ end
+ end
+ end
+
+ module HelperMethods
+ # 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: ->(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, lambda or symbol which returns an enumerable. If the
+ # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>,
+ # otherwise with <tt>include?</tt>. When using a proc or lambda the instance
+ # under validation is passed as an argument.
+ # * <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").
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, 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
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
new file mode 100644
index 0000000000..a96b30cadd
--- /dev/null
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -0,0 +1,126 @@
+module ActiveModel
+
+ # == Active \Model Length Validator
+ module Validations
+ class LengthValidator < EachValidator # :nodoc:
+ MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
+ CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
+
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
+
+ def initialize(options)
+ if range = (options.delete(:in) || options.delete(:within))
+ raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
+ options[:minimum], options[:maximum] = range.min, range.max
+ end
+
+ if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil?
+ options[:minimum] = 1
+ end
+
+ super
+ end
+
+ def check_validity!
+ keys = CHECKS.keys & options.keys
+
+ if keys.empty?
+ raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.'
+ end
+
+ keys.each do |key|
+ value = options[key]
+
+ unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY
+ raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity"
+ end
+ end
+ end
+
+ 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]
+
+ if !value.nil? || skip_nil_check?(key)
+ next if value_length.send(validity_check, check_value)
+ end
+
+ errors_options[:count] = check_value
+
+ default_message = options[MESSAGES[key]]
+ errors_options[:message] ||= default_message if default_message
+
+ record.errors.add(attribute, MESSAGES[key], errors_options)
+ end
+ end
+
+ private
+
+ def tokenize(value)
+ if options[:tokenizer] && value.kind_of?(String)
+ options[:tokenizer].call(value)
+ end || value
+ end
+
+ def skip_nil_check?(key)
+ key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
+ end
+ end
+
+ module HelperMethods
+
+ # 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: ->(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. Allows +nil+ by
+ # default if not used with :minimum.
+ # * <tt>:is</tt> - The exact size of the attribute.
+ # * <tt>:within</tt> - A range specifying the minimum and maximum size of
+ # the attribute.
+ # * <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>: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
+
+ alias_method :validates_size_of, :validates_length_of
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
new file mode 100644
index 0000000000..5bd162433d
--- /dev/null
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -0,0 +1,148 @@
+module ActiveModel
+
+ module Validations
+ class NumericalityValidator < EachValidator # :nodoc:
+ CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
+ equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
+ odd: :odd?, even: :even?, other_than: :!= }.freeze
+
+ RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
+
+ def check_validity!
+ keys = CHECKS.keys - [:odd, :even]
+ options.slice(*keys).each do |option, value|
+ unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
+ raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
+ end
+ end
+ end
+
+ def validate_each(record, attr_name, value)
+ before_type_cast = :"#{attr_name}_before_type_cast"
+
+ raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast)
+ raw_value ||= value
+
+ return if options[:allow_nil] && raw_value.nil?
+
+ unless value = parse_raw_value_as_a_number(raw_value)
+ record.errors.add(attr_name, :not_a_number, filtered_options(raw_value))
+ return
+ end
+
+ if allow_only_integer?(record)
+ unless value = parse_raw_value_as_an_integer(raw_value)
+ record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
+ return
+ end
+ end
+
+ options.slice(*CHECKS.keys).each do |option, option_value|
+ case option
+ when :odd, :even
+ unless value.to_i.send(CHECKS[option])
+ record.errors.add(attr_name, option, filtered_options(value))
+ end
+ else
+ case option_value
+ when Proc
+ option_value = option_value.call(record)
+ when Symbol
+ option_value = record.send(option_value)
+ end
+
+ unless value.send(CHECKS[option], option_value)
+ record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
+ end
+ end
+ end
+ end
+
+ protected
+
+ def parse_raw_value_as_a_number(raw_value)
+ Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
+ rescue ArgumentError, TypeError
+ nil
+ end
+
+ def parse_raw_value_as_an_integer(raw_value)
+ raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/
+ end
+
+ def filtered_options(value)
+ filtered = options.except(*RESERVED_OPTIONS)
+ filtered[:value] = value
+ filtered
+ end
+
+ def allow_only_integer?(record)
+ case options[:only_integer]
+ when Symbol
+ record.send(options[:only_integer])
+ when Proc
+ options[:only_integer].call(record)
+ else
+ options[:only_integer]
+ end
+ end
+ 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+).
+ #
+ # class Person < ActiveRecord::Base
+ # validates_numericality_of :value, on: :create
+ # end
+ #
+ # Configuration options:
+ # * <tt>:message</tt> - A custom error message (default is: "is not a number").
+ # * <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.
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, 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>
+ # * <tt>:only_integer</tt>
+ #
+ # For example:
+ #
+ # class Person < ActiveRecord::Base
+ # validates_numericality_of :width, less_than: ->(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
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb
new file mode 100644
index 0000000000..5d593274eb
--- /dev/null
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -0,0 +1,39 @@
+
+module ActiveModel
+
+ module Validations
+ class PresenceValidator < EachValidator # :nodoc:
+ def validate_each(record, attr_name, value)
+ record.errors.add(attr_name, :blank, options) if value.blank?
+ end
+ end
+
+ module HelperMethods
+ # 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
+ # end
+ #
+ # 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>.
+ #
+ # 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").
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, 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
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
new file mode 100644
index 0000000000..ae8d377fdf
--- /dev/null
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -0,0 +1,171 @@
+require 'active_support/core_ext/hash/slice'
+
+module ActiveModel
+ module Validations
+ module ClassMethods
+ # This method is a shortcut to all default validators and any custom
+ # validator classes ending in 'Validator'. Note that Rails default
+ # validators can be overridden inside specific classes by creating
+ # custom validator classes in their place such as PresenceValidator.
+ #
+ # Examples of using the default rails validators:
+ #
+ # validates :terms, acceptance: true
+ # 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.
+ #
+ # class EmailValidator < ActiveModel::EachValidator
+ # def validate_each(record, attribute, value)
+ # record.errors.add attribute, (options[:message] || "is not an email") unless
+ # value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
+ # end
+ # end
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # attr_accessor :name, :email
+ #
+ # validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
+ # validates :email, presence: true, email: true
+ # end
+ #
+ # Validator classes may also exist within the class being validated
+ # allowing custom modules of validators to be included as needed.
+ #
+ # class Film
+ # include ActiveModel::Validations
+ #
+ # class TitleValidator < ActiveModel::EachValidator
+ # def validate_each(record, attribute, value)
+ # record.errors.add attribute, "must start with 'the'" unless value =~ /\Athe/i
+ # end
+ # end
+ #
+ # validates :name, title: true
+ # end
+ #
+ # 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.
+ #
+ # 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 <tt>options[:in]</tt> while other types
+ # including regular expressions and strings are passed as <tt>options[:with]</tt>.
+ #
+ # There is also a list of options that could be used along with validators:
+ #
+ # * <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>:allow_nil</tt> - Skip validation if the attribute is +nil+.
+ # * <tt>:allow_blank</tt> - Skip validation if the attribute is blank.
+ # * <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.
+ #
+ # Example:
+ #
+ # 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)
+
+ raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
+ raise ArgumentError, "You need to supply at least one validation" if validations.empty?
+
+ defaults[:attributes] = attributes
+
+ validations.each do |key, options|
+ next unless options
+ key = "#{key.to_s.camelize}Validator"
+
+ begin
+ validator = key.include?('::') ? key.constantize : const_get(key)
+ rescue NameError
+ raise ArgumentError, "Unknown validator: '#{key}'"
+ end
+
+ validates_with(validator, defaults.merge(_parse_validates_options(options)))
+ end
+ end
+
+ # This method is used to define validations that cannot be corrected by end
+ # users and are considered exceptional. So each validator defined with bang
+ # or <tt>:strict</tt> option set to <tt>true</tt> will always raise
+ # <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error
+ # when validation fails. See <tt>validates</tt> for more information about
+ # the validation itself.
+ #
+ # 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
+ validates(*(attributes << options))
+ end
+
+ protected
+
+ # When creating custom validators, it might be useful to be able to specify
+ # additional default keys. This can be done by overwriting this method.
+ def _validates_default_keys # :nodoc:
+ [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
+ end
+
+ def _parse_validates_options(options) # :nodoc:
+ case options
+ when TrueClass
+ {}
+ when Hash
+ options
+ when Range, Array
+ { in: options }
+ else
+ { with: options }
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb
new file mode 100644
index 0000000000..ff41572105
--- /dev/null
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -0,0 +1,150 @@
+module ActiveModel
+ module Validations
+ module HelperMethods
+ private
+ def _merge_attributes(attr_names)
+ options = attr_names.extract_options!.symbolize_keys
+ attr_names.flatten!
+ options[:attributes] = attr_names
+ options
+ end
+ end
+
+ class WithValidator < EachValidator # :nodoc:
+ def validate_each(record, attr, val)
+ method_name = options[:with]
+
+ if record.method(method_name).arity == 0
+ record.send method_name
+ else
+ record.send method_name, attr
+ end
+ end
+ end
+
+ module ClassMethods
+ # Passes the record off to the class or classes specified and allows them
+ # to add errors based on more complex conditions.
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # validates_with MyValidator
+ # end
+ #
+ # class MyValidator < ActiveModel::Validator
+ # def validate(record)
+ # if some_complex_logic
+ # record.errors.add :base, 'This record is invalid'
+ # end
+ # end
+ #
+ # private
+ # def some_complex_logic
+ # # ...
+ # end
+ # end
+ #
+ # You may also pass it multiple classes, like so:
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # 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>: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.
+ #
+ # If you pass any additional configuration options, they will be passed
+ # to the class and available as +options+:
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # validates_with MyValidator, my_custom_key: 'my custom value'
+ # end
+ #
+ # class MyValidator < ActiveModel::Validator
+ # def validate(record)
+ # options[:my_custom_key] # => "my custom value"
+ # end
+ # end
+ def validates_with(*args, &block)
+ options = args.extract_options!
+ options[:class] = self
+
+ args.each do |klass|
+ validator = klass.new(options, &block)
+
+ if validator.respond_to?(:attributes) && !validator.attributes.empty?
+ validator.attributes.each do |attribute|
+ _validators[attribute.to_sym] << validator
+ end
+ else
+ _validators[nil] << validator
+ end
+
+ validate(validator, options)
+ end
+ end
+ end
+
+ # Passes the record off to the class or classes specified and allows them
+ # to add errors based on more complex conditions.
+ #
+ # class Person
+ # include ActiveModel::Validations
+ #
+ # validate :instance_validations
+ #
+ # def instance_validations
+ # validates_with MyValidator
+ # end
+ # end
+ #
+ # Please consult the class method documentation for more information on
+ # creating your own validator.
+ #
+ # You may also pass it multiple classes, like so:
+ #
+ # class Person
+ # include ActiveModel::Validations
+ #
+ # validate :instance_validations, on: :create
+ #
+ # def instance_validations
+ # validates_with MyValidator, MyOtherValidator
+ # end
+ # end
+ #
+ # 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 +options+, please refer to the
+ # class version of this method for more information.
+ def validates_with(*args, &block)
+ options = args.extract_options!
+ options[:class] = self.class
+
+ args.each do |klass|
+ validator = klass.new(options, &block)
+ validator.validate(self)
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
new file mode 100644
index 0000000000..0116de68ab
--- /dev/null
+++ b/activemodel/lib/active_model/validator.rb
@@ -0,0 +1,182 @@
+require "active_support/core_ext/module/anonymous"
+
+module ActiveModel
+
+ # == Active \Model \Validator
+ #
+ # A simple base class that can be used along with
+ # ActiveModel::Validations::ClassMethods.validates_with
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # validates_with MyValidator
+ # end
+ #
+ # class MyValidator < ActiveModel::Validator
+ # def validate(record)
+ # if some_complex_logic
+ # record.errors[:base] = "This record is invalid"
+ # end
+ # end
+ #
+ # private
+ # def some_complex_logic
+ # # ...
+ # end
+ # end
+ #
+ # Any class that inherits from ActiveModel::Validator must implement a method
+ # called +validate+ which accepts a +record+.
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # validates_with MyValidator
+ # end
+ #
+ # class MyValidator < ActiveModel::Validator
+ # def validate(record)
+ # record # => The person instance being validated
+ # options # => Any non-standard options passed to validates_with
+ # end
+ # end
+ #
+ # 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)
+ # record.errors.add :base, "This is some custom error message"
+ # record.errors.add :first_name, "This is some complex validation"
+ # # etc...
+ # end
+ # end
+ #
+ # To add behavior to the initialize method, use the following signature:
+ #
+ # class MyValidator < ActiveModel::Validator
+ # def initialize(options)
+ # super
+ # @my_custom_field = options[:field_name] || :first_name
+ # end
+ # end
+ #
+ # Note that the validator is initialized only once for the whole application
+ # life cycle, and not on each validation run.
+ #
+ # The easiest way to add custom validators for validating individual attributes
+ # is with the convenient <tt>ActiveModel::EachValidator</tt>.
+ #
+ # class TitleValidator < ActiveModel::EachValidator
+ # def validate_each(record, attribute, value)
+ # 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).
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # attr_accessor :title
+ #
+ # validates :title, presence: true, title: true
+ # end
+ #
+ # It can be useful to access the class that is using that validator when there are prerequisites such
+ # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor.
+ # To setup your validator override the constructor.
+ #
+ # class MyValidator < ActiveModel::Validator
+ # def initialize(options={})
+ # super
+ # options[:class].send :attr_accessor, :custom_attribute
+ # end
+ # end
+ class Validator
+ attr_reader :options
+
+ # 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
+
+ # Accepts options that will be made available through the +options+ reader.
+ def initialize(options = {})
+ @options = options.except(:class).freeze
+ end
+
+ # Returns the kind for this validator.
+ #
+ # PresenceValidator.new.kind # => :presence
+ # UniquenessValidator.new.kind # => :uniqueness
+ def kind
+ self.class.kind
+ end
+
+ # Override this method in subclasses with validation logic, adding errors
+ # to the records +errors+ array where necessary.
+ def validate(record)
+ raise NotImplementedError, "Subclasses must implement a validate(record) method."
+ end
+ end
+
+ # +EachValidator+ is a validator which iterates through the attributes given
+ # in the options hash invoking the <tt>validate_each</tt> method passing in the
+ # record, attribute and value.
+ #
+ # All Active Model validations are built on top of this validator.
+ class EachValidator < Validator #:nodoc:
+ attr_reader :attributes
+
+ # Returns a new validator instance. All options will be available via the
+ # +options+ reader, however the <tt>:attributes</tt> option will be removed
+ # and instead be made available through the +attributes+ reader.
+ def initialize(options)
+ @attributes = Array(options.delete(:attributes))
+ raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
+ super
+ check_validity!
+ end
+
+ # Performs validation on the supplied record. By default this will call
+ # +validates_each+ to determine validity therefore subclasses should
+ # override +validates_each+ with validation logic.
+ def validate(record)
+ attributes.each do |attribute|
+ value = record.read_attribute_for_validation(attribute)
+ next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
+ validate_each(record, attribute, value)
+ end
+ end
+
+ # Override this method in subclasses with the validation logic, adding
+ # errors to the records +errors+ array where necessary.
+ def validate_each(record, attribute, value)
+ raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method"
+ end
+
+ # Hook method that gets called by the initializer allowing verification
+ # that the arguments supplied are valid. You could for example raise an
+ # +ArgumentError+ when invalid options are supplied.
+ def check_validity!
+ end
+ end
+
+ # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
+ # and call this block for each attribute being validated. +validates_each+ uses this validator.
+ class BlockValidator < EachValidator #:nodoc:
+ def initialize(options, &block)
+ @block = block
+ super
+ end
+
+ private
+
+ def validate_each(record, attribute, value)
+ @block.call(record, attribute, value)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
new file mode 100644
index 0000000000..b1f9082ea7
--- /dev/null
+++ b/activemodel/lib/active_model/version.rb
@@ -0,0 +1,8 @@
+require_relative 'gem_version'
+
+module ActiveModel
+ # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
+ end
+end
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
new file mode 100644
index 0000000000..e81b7ac424
--- /dev/null
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -0,0 +1,281 @@
+require 'cases/helper'
+
+class ModelWithAttributes
+ include ActiveModel::AttributeMethods
+
+ class << self
+ define_method(:bar) do
+ 'original bar'
+ end
+ end
+
+ def attributes
+ { foo: 'value of foo', baz: 'value of baz' }
+ end
+
+private
+ def attribute(name)
+ attributes[name.to_sym]
+ end
+end
+
+class ModelWithAttributes2
+ include ActiveModel::AttributeMethods
+
+ attr_accessor :attributes
+
+ attribute_method_suffix '_test'
+
+private
+ def attribute(name)
+ attributes[name.to_s]
+ end
+
+ alias attribute_test attribute
+
+ def private_method
+ "<3 <3"
+ end
+
+protected
+
+ def protected_method
+ "O_o O_o"
+ end
+end
+
+class ModelWithAttributesWithSpaces
+ include ActiveModel::AttributeMethods
+
+ def attributes
+ { :'foo bar' => 'value of foo bar'}
+ end
+
+private
+ def attribute(name)
+ attributes[name.to_sym]
+ end
+end
+
+class ModelWithWeirdNamesAttributes
+ include ActiveModel::AttributeMethods
+
+ class << self
+ define_method(:'c?d') do
+ 'original c?d'
+ end
+ end
+
+ def attributes
+ { :'a?b' => 'value of a?b' }
+ end
+
+private
+ def attribute(name)
+ attributes[name.to_sym]
+ 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
+
+class AttributeMethodsTest < ActiveModel::TestCase
+ test 'method missing works correctly even if attributes method is not defined' do
+ assert_raises(NoMethodError) { ModelWithoutAttributesMethod.new.foo }
+ end
+
+ test 'unrelated classes should not share attribute method matchers' do
+ assert_not_equal ModelWithAttributes.send(:attribute_method_matchers),
+ ModelWithAttributes2.send(:attribute_method_matchers)
+ end
+
+ test '#define_attribute_method generates attribute method' do
+ begin
+ ModelWithAttributes.define_attribute_method(:foo)
+
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
+ end
+
+ test '#define_attribute_method does not generate attribute method if already defined in attribute module' do
+ klass = Class.new(ModelWithAttributes)
+ klass.generated_attribute_methods.module_eval do
+ def foo
+ '<3'
+ end
+ end
+ klass.define_attribute_method(:foo)
+
+ assert_equal '<3', klass.new.foo
+ end
+
+ test '#define_attribute_method generates a method that is already defined on the host' do
+ klass = Class.new(ModelWithAttributes) do
+ def foo
+ super
+ end
+ end
+ klass.define_attribute_method(:foo)
+
+ assert_equal 'value of foo', klass.new.foo
+ end
+
+ test '#define_attribute_method generates attribute method with invalid identifier characters' do
+ begin
+ ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
+
+ assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
+ assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b')
+ ensure
+ ModelWithWeirdNamesAttributes.undefine_attribute_methods
+ end
+ end
+
+ test '#define_attribute_methods works passing multiple arguments' do
+ begin
+ ModelWithAttributes.define_attribute_methods(:foo, :baz)
+
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_equal "value of baz", ModelWithAttributes.new.baz
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
+ end
+
+ test '#define_attribute_methods generates attribute methods' do
+ begin
+ ModelWithAttributes.define_attribute_methods(:foo)
+
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
+ 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
+ begin
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+
+ assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
+ end
+ end
+
+ test '#alias_attribute works with attributes with spaces in their names' do
+ begin
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
+
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
+ end
+ end
+
+ test '#alias_attribute works with attributes named as a ruby keyword' do
+ begin
+ 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
+ ensure
+ ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods
+ end
+ end
+
+ test '#undefine_attribute_methods removes attribute methods' do
+ ModelWithAttributes.define_attribute_methods(:foo)
+ ModelWithAttributes.undefine_attribute_methods
+
+ assert !ModelWithAttributes.new.respond_to?(:foo)
+ assert_raises(NoMethodError) { ModelWithAttributes.new.foo }
+ end
+
+ test 'accessing a suffixed attribute' do
+ m = ModelWithAttributes2.new
+ m.attributes = { 'foo' => 'bar' }
+
+ assert_equal 'bar', m.foo
+ assert_equal 'bar', m.foo_test
+ end
+
+ test 'should not interfere with method_missing if the attr has a private/protected method' do
+ m = ModelWithAttributes2.new
+ m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' }
+
+ # dispatches to the *method*, not the attribute
+ assert_equal '<3 <3', m.send(:private_method)
+ assert_equal 'O_o O_o', m.send(:protected_method)
+
+ # sees that a method is already defined, so doesn't intervene
+ assert_raises(NoMethodError) { m.private_method }
+ 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' }
+
+ 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_equal c.respond_to?(:protected_method), m.respond_to?(:protected_method)
+ assert m.respond_to?(:protected_method, true)
+ end
+
+ test 'should use attribute_missing to dispatch a missing attribute' do
+ m = ModelWithAttributes2.new
+ m.attributes = { 'foo' => 'bar' }
+
+ def m.attribute_missing(match, *args, &block)
+ match
+ end
+
+ match = m.foo_test
+
+ assert_equal 'foo', match.attr_name
+ assert_equal 'attribute_test', match.target
+ assert_equal 'foo_test', match.method_name
+ end
+end
diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb
new file mode 100644
index 0000000000..5fede098d1
--- /dev/null
+++ b/activemodel/test/cases/callbacks_test.rb
@@ -0,0 +1,114 @@
+require "cases/helper"
+
+class CallbacksTest < ActiveModel::TestCase
+
+ class CallbackValidator
+ def around_create(model)
+ model.callbacks << :before_around_create
+ yield
+ model.callbacks << :after_around_create
+ end
+ end
+
+ class ModelCallbacks
+ attr_reader :callbacks
+ extend ActiveModel::Callbacks
+
+ define_model_callbacks :create
+ define_model_callbacks :initialize, only: :after
+ define_model_callbacks :multiple, only: [:before, :around]
+ define_model_callbacks :empty, only: []
+
+ before_create :before_create
+ around_create CallbackValidator.new
+
+ after_create do |model|
+ model.callbacks << :after_create
+ end
+
+ after_create "@callbacks << :final_callback"
+
+ def initialize(valid=true)
+ @callbacks, @valid = [], valid
+ end
+
+ def before_create
+ @callbacks << :before_create
+ end
+
+ def create
+ run_callbacks :create do
+ @callbacks << :create
+ @valid
+ end
+ end
+ end
+
+ test "complete callback chain" do
+ model = ModelCallbacks.new
+ model.create
+ assert_equal model.callbacks, [ :before_create, :before_around_create, :create,
+ :after_around_create, :after_create, :final_callback]
+ end
+
+ test "after callbacks are always appended" do
+ model = ModelCallbacks.new
+ model.create
+ assert_equal model.callbacks.last, :final_callback
+ end
+
+ test "after callbacks are not executed if the block returns false" do
+ model = ModelCallbacks.new(false)
+ model.create
+ assert_equal model.callbacks, [ :before_create, :before_around_create,
+ :create, :after_around_create]
+ end
+
+ test "only selects which types of callbacks should be created" do
+ assert !ModelCallbacks.respond_to?(:before_initialize)
+ assert !ModelCallbacks.respond_to?(:around_initialize)
+ assert_respond_to ModelCallbacks, :after_initialize
+ end
+
+ test "only selects which types of callbacks should be created from an array list" do
+ assert_respond_to ModelCallbacks, :before_multiple
+ assert_respond_to ModelCallbacks, :around_multiple
+ assert !ModelCallbacks.respond_to?(:after_multiple)
+ end
+
+ test "no callbacks should be created" do
+ assert !ModelCallbacks.respond_to?(:before_empty)
+ assert !ModelCallbacks.respond_to?(:around_empty)
+ assert !ModelCallbacks.respond_to?(:after_empty)
+ end
+
+ class Violin
+ attr_reader :history
+ def initialize
+ @history = []
+ end
+ extend ActiveModel::Callbacks
+ define_model_callbacks :create
+ def callback1; self.history << 'callback1'; end
+ def callback2; self.history << 'callback2'; end
+ def create
+ run_callbacks(:create) {}
+ self
+ end
+ end
+ class Violin1 < Violin
+ after_create :callback1, :callback2
+ end
+ class Violin2 < Violin
+ after_create :callback1
+ after_create :callback2
+ end
+
+ test "after_create callbacks with both callbacks declared in one line" do
+ assert_equal ["callback1", "callback2"], Violin1.new.create.history
+ end
+ test "after_create callbacks with both callbacks declared in different lines" do
+ assert_equal ["callback1", "callback2"], Violin2.new.create.history
+ end
+
+end
diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb
new file mode 100644
index 0000000000..800cad6d9a
--- /dev/null
+++ b/activemodel/test/cases/conversion_test.rb
@@ -0,0 +1,50 @@
+require 'cases/helper'
+require 'models/contact'
+require 'models/helicopter'
+
+class ConversionTest < ActiveModel::TestCase
+ test "to_model default implementation returns self" do
+ contact = Contact.new
+ assert_equal contact, contact.to_model
+ end
+
+ test "to_key default implementation returns nil for new records" do
+ assert_nil Contact.new.to_key
+ end
+
+ test "to_key default implementation returns the id in an array for persisted records" do
+ assert_equal [1], Contact.new(id: 1).to_key
+ end
+
+ test "to_param default implementation returns nil for new records" do
+ assert_nil Contact.new.to_param
+ end
+
+ test "to_param default implementation returns a string of ids for persisted records" do
+ assert_equal "1", Contact.new(id: 1).to_param
+ end
+
+ test "to_param returns the string joined by '-'" do
+ assert_equal "abc-xyz", Contact.new(id: ["abc", "xyz"]).to_param
+ end
+
+ test "to_param returns nil if to_key is nil" do
+ klass = Class.new(Contact) do
+ def persisted?
+ true
+ end
+ end
+
+ assert_nil klass.new.to_param
+ end
+
+ 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"
+ end
+
+ test "to_partial_path handles namespaced models" do
+ assert_equal "helicopter/comanches/comanche", Helicopter::Comanche.new.to_partial_path
+ end
+end
diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb
new file mode 100644
index 0000000000..db2cd885e2
--- /dev/null
+++ b/activemodel/test/cases/dirty_test.rb
@@ -0,0 +1,228 @@
+require "cases/helper"
+
+class DirtyTest < ActiveModel::TestCase
+ class DirtyModel
+ include ActiveModel::Dirty
+ define_attribute_methods :name, :color, :size
+
+ def initialize
+ @name = nil
+ @color = nil
+ @size = nil
+ end
+
+ def name
+ @name
+ end
+
+ def name=(val)
+ name_will_change!
+ @name = val
+ end
+
+ def color
+ @color
+ end
+
+ def color=(val)
+ color_will_change! unless val == @color
+ @color = val
+ end
+
+ def size
+ @size
+ end
+
+ def size=(val)
+ attribute_will_change!(:size) unless val == @size
+ @size = val
+ end
+
+ def save
+ changes_applied
+ end
+
+ def reload
+ clear_changes_information
+ end
+
+ def deprecated_reload
+ reset_changes
+ end
+ end
+
+ setup do
+ @model = DirtyModel.new
+ end
+
+ test "setting attribute will result in change" do
+ assert !@model.changed?
+ assert !@model.name_changed?
+ @model.name = "Ringo"
+ assert @model.changed?
+ assert @model.name_changed?
+ end
+
+ test "list of changed attribute keys" do
+ assert_equal [], @model.changed
+ @model.name = "Paul"
+ assert_equal ['name'], @model.changed
+ end
+
+ test "changes to attribute values" do
+ assert !@model.changes['name']
+ @model.name = "John"
+ assert_equal [nil, "John"], @model.changes['name']
+ end
+
+ test "checking if an attribute has changed to a particular value" do
+ @model.name = "Ringo"
+ assert @model.name_changed?(from: nil, to: "Ringo")
+ assert_not @model.name_changed?(from: "Pete", to: "Ringo")
+ assert @model.name_changed?(to: "Ringo")
+ assert_not @model.name_changed?(to: "Pete")
+ assert @model.name_changed?(from: nil)
+ assert_not @model.name_changed?(from: "Pete")
+ end
+
+ test "changes accessible through both strings and symbols" do
+ @model.name = "David"
+ assert_not_nil @model.changes[:name]
+ assert_not_nil @model.changes['name']
+ end
+
+ test "be consistent with symbols arguments after the changes are applied" do
+ @model.name = "David"
+ assert @model.attribute_changed?(:name)
+ @model.save
+ @model.name = 'Rafael'
+ assert @model.attribute_changed?(:name)
+ end
+
+ test "attribute mutation" do
+ @model.instance_variable_set("@name", "Yam")
+ assert !@model.name_changed?
+ @model.name.replace("Hadad")
+ assert !@model.name_changed?
+ @model.name_will_change!
+ @model.name.replace("Baal")
+ assert @model.name_changed?
+ end
+
+ test "resetting attribute" do
+ @model.name = "Bob"
+ @model.restore_name!
+ assert_nil @model.name
+ assert !@model.name_changed?
+ end
+
+ test "setting color to same value should not result in change being recorded" do
+ @model.color = "red"
+ assert @model.color_changed?
+ @model.save
+ assert !@model.color_changed?
+ assert !@model.changed?
+ @model.color = "red"
+ assert !@model.color_changed?
+ assert !@model.changed?
+ end
+
+ test "saving should reset model's changed status" do
+ @model.name = "Alf"
+ assert @model.changed?
+ @model.save
+ assert !@model.changed?
+ assert !@model.name_changed?
+ end
+
+ test "saving should preserve previous changes" do
+ @model.name = "Jericho Cane"
+ @model.save
+ assert_equal [nil, "Jericho Cane"], @model.previous_changes['name']
+ end
+
+ test "previous value is preserved when changed after save" do
+ assert_equal({}, @model.changed_attributes)
+ @model.name = "Paul"
+ assert_equal({ "name" => nil }, @model.changed_attributes)
+
+ @model.save
+
+ @model.name = "John"
+ assert_equal({ "name" => "Paul" }, @model.changed_attributes)
+ end
+
+ test "changing the same attribute multiple times retains the correct original value" do
+ @model.name = "Otto"
+ @model.save
+ @model.name = "DudeFella ManGuy"
+ @model.name = "Mr. Manfredgensonton"
+ assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
+ assert_equal @model.name_was, "Otto"
+ end
+
+ test "using attribute_will_change! with a symbol" do
+ @model.size = 1
+ assert @model.size_changed?
+ end
+
+ test "reload should reset all changes" do
+ @model.name = 'Dmitry'
+ @model.name_changed?
+ @model.save
+ @model.name = 'Bob'
+
+ assert_equal [nil, 'Dmitry'], @model.previous_changes['name']
+ assert_equal 'Dmitry', @model.changed_attributes['name']
+
+ @model.reload
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
+
+ test "reset_changes is deprecated" do
+ @model.name = 'Dmitry'
+ @model.name_changed?
+ @model.save
+ @model.name = 'Bob'
+
+ assert_equal [nil, 'Dmitry'], @model.previous_changes['name']
+ assert_equal 'Dmitry', @model.changed_attributes['name']
+
+ assert_deprecated do
+ @model.deprecated_reload
+ end
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
+
+ test "restore_attributes should restore all previous data" do
+ @model.name = 'Dmitry'
+ @model.color = 'Red'
+ @model.save
+ @model.name = 'Bob'
+ @model.color = 'White'
+
+ @model.restore_attributes
+
+ assert_not @model.changed?
+ assert_equal 'Dmitry', @model.name
+ assert_equal 'Red', @model.color
+ end
+
+ test "restore_attributes can restore only some attributes" do
+ @model.name = 'Dmitry'
+ @model.color = 'Red'
+ @model.save
+ @model.name = 'Bob'
+ @model.color = 'White'
+
+ @model.restore_attributes(['name'])
+
+ assert @model.changed?
+ assert_equal 'Dmitry', @model.name
+ assert_equal 'White', @model.color
+ end
+end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
new file mode 100644
index 0000000000..42d0365521
--- /dev/null
+++ b/activemodel/test/cases/errors_test.rb
@@ -0,0 +1,315 @@
+require "cases/helper"
+
+class ErrorsTest < ActiveModel::TestCase
+ class Person
+ extend ActiveModel::Naming
+ def initialize
+ @errors = ActiveModel::Errors.new(self)
+ end
+
+ attr_accessor :name, :age
+ attr_reader :errors
+
+ def validate!
+ errors.add(:name, "cannot be nil") if name == nil
+ end
+
+ def read_attribute_for_validation(attr)
+ send(attr)
+ end
+
+ def self.human_attribute_name(attr, options = {})
+ attr
+ end
+
+ def self.lookup_ancestors
+ [self]
+ end
+ end
+
+ def test_delete
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = 'omg'
+ errors.delete(:foo)
+ assert_empty errors[:foo]
+ end
+
+ def test_include?
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = 'omg'
+ assert errors.include?(:foo), 'errors should include :foo'
+ end
+
+ def test_dup
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = 'bar'
+ errors_dup = errors.dup
+ errors_dup[:bar] = 'omg'
+ assert_not_same errors_dup.messages, errors.messages
+ end
+
+ def test_has_key?
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = 'omg'
+ assert_equal true, errors.has_key?(:foo), 'errors should have key :foo'
+ end
+
+ def test_has_no_key
+ errors = ActiveModel::Errors.new(self)
+ assert_equal false, errors.has_key?(:name), 'errors should not have key :name'
+ end
+
+ test "clear errors" do
+ person = Person.new
+ person.validate!
+
+ assert_equal 1, person.errors.count
+ person.errors.clear
+ assert person.errors.empty?
+ end
+
+ test "get returns the errors for the provided key" do
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = "omg"
+
+ assert_equal ["omg"], errors.get(:foo)
+ end
+
+ test "sets the error with the provided key" do
+ errors = ActiveModel::Errors.new(self)
+ errors.set(:foo, "omg")
+
+ assert_equal({ foo: "omg" }, errors.messages)
+ end
+
+ test "error access is indifferent" do
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = "omg"
+
+ assert_equal ["omg"], errors["foo"]
+ end
+
+ test "values returns an array of messages" do
+ errors = ActiveModel::Errors.new(self)
+ errors.set(:foo, "omg")
+ errors.set(:baz, "zomg")
+
+ assert_equal ["omg", "zomg"], errors.values
+ end
+
+ test "keys returns the error keys" do
+ errors = ActiveModel::Errors.new(self)
+ errors.set(:foo, "omg")
+ errors.set(:baz, "zomg")
+
+ assert_equal [:foo, :baz], errors.keys
+ end
+
+ test "detecting whether there are errors with empty?, blank?, include?" do
+ person = Person.new
+ person.errors[:foo]
+ assert person.errors.empty?
+ assert person.errors.blank?
+ assert !person.errors.include?(:foo)
+ end
+
+ test "adding errors using conditionals with Person#validate!" do
+ person = Person.new
+ person.validate!
+ assert_equal ["name cannot be nil"], person.errors.full_messages
+ assert_equal ["cannot be nil"], person.errors[:name]
+ end
+
+ test "assign error" do
+ person = Person.new
+ person.errors[:name] = 'should not be nil'
+ assert_equal ["should not be nil"], person.errors[:name]
+ end
+
+ test "add an error message on a specific attribute" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert_equal ["cannot be blank"], person.errors[:name]
+ end
+
+ test "add an error with a symbol" do
+ person = Person.new
+ person.errors.add(:name, :blank)
+ message = person.errors.generate_message(:name, :blank)
+ assert_equal [message], person.errors[:name]
+ end
+
+ test "add an error with a proc" do
+ person = Person.new
+ message = Proc.new { "cannot be blank" }
+ person.errors.add(:name, message)
+ assert_equal ["cannot be blank"], person.errors[:name]
+ end
+
+ test "added? detects if a specific error was added to the object" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert person.errors.added?(:name, "cannot be blank")
+ end
+
+ test "added? handles symbol message" do
+ person = Person.new
+ person.errors.add(:name, :blank)
+ assert person.errors.added?(:name, :blank)
+ end
+
+ test "added? handles proc messages" do
+ person = Person.new
+ message = Proc.new { "cannot be blank" }
+ person.errors.add(:name, message)
+ assert person.errors.added?(:name, message)
+ end
+
+ test "added? defaults message to :invalid" do
+ person = Person.new
+ person.errors.add(:name)
+ assert person.errors.added?(:name)
+ end
+
+ test "added? matches the given message when several errors are present for the same attribute" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "is invalid")
+ assert person.errors.added?(:name, "cannot be blank")
+ end
+
+ test "added? returns false when no errors are present" do
+ person = Person.new
+ assert !person.errors.added?(:name)
+ end
+
+ test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do
+ person = Person.new
+ person.errors.add(:name, "is invalid")
+ assert !person.errors.added?(:name, "cannot be blank")
+ end
+
+ test "size calculates the number of error messages" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert_equal 1, person.errors.size
+ end
+
+ test "to_a returns the list of errors with complete messages containing the attribute names" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.to_a
+ end
+
+ test "to_hash returns the error messages hash" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert_equal({ name: ["cannot be blank"] }, person.errors.to_hash)
+ end
+
+ test "full_messages creates a list of error messages with the attribute name included" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages
+ end
+
+ test "full_messages_for contains all the error messages for the given attribute" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages_for(:name)
+ end
+
+ test "full_messages_for does not contain error messages from other attributes" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:email, "cannot be blank")
+ assert_equal ["name cannot be blank"], person.errors.full_messages_for(:name)
+ end
+
+ test "full_messages_for returns an empty list in case there are no errors for the given attribute" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert_equal [], person.errors.full_messages_for(:email)
+ end
+
+ test "full_message returns the given message when attribute is :base" do
+ person = Person.new
+ assert_equal "press the button", person.errors.full_message(:base, "press the button")
+ end
+
+ test "full_message returns the given message with the attribute name included" do
+ person = Person.new
+ assert_equal "name cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "name_test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
+ end
+
+ test "as_json creates a json formatted representation of the errors hash" do
+ person = Person.new
+ person.validate!
+
+ assert_equal({ name: ["cannot be nil"] }, person.errors.as_json)
+ end
+
+ test "as_json with :full_messages option creates a json formatted representation of the errors containing complete messages" do
+ person = Person.new
+ person.validate!
+
+ assert_equal({ name: ["name cannot be nil"] }, person.errors.as_json(full_messages: true))
+ end
+
+ test "generate_message works without i18n_scope" do
+ person = Person.new
+ assert !Person.respond_to?(:i18n_scope)
+ assert_nothing_raised {
+ person.errors.generate_message(:name, :blank)
+ }
+ end
+
+ test "add_on_empty generates message" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :empty, {})
+ person.errors.add_on_empty :name
+ end
+
+ test "add_on_empty generates message for multiple attributes" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :empty, {})
+ person.errors.expects(:generate_message).with(:age, :empty, {})
+ person.errors.add_on_empty [:name, :age]
+ end
+
+ test "add_on_empty generates message with custom default message" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :empty, { message: 'custom' })
+ person.errors.add_on_empty :name, message: 'custom'
+ end
+
+ test "add_on_empty generates message with empty string value" do
+ person = Person.new
+ person.name = ''
+ person.errors.expects(:generate_message).with(:name, :empty, {})
+ person.errors.add_on_empty :name
+ end
+
+ test "add_on_blank generates message" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :blank, {})
+ person.errors.add_on_blank :name
+ end
+
+ test "add_on_blank generates message for multiple attributes" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :blank, {})
+ person.errors.expects(:generate_message).with(:age, :blank, {})
+ person.errors.add_on_blank [:name, :age]
+ end
+
+ test "add_on_blank generates message with custom default message" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :blank, { message: 'custom' })
+ person.errors.add_on_blank :name, message: 'custom'
+ end
+end
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
new file mode 100644
index 0000000000..804e0c24f6
--- /dev/null
+++ b/activemodel/test/cases/helper.rb
@@ -0,0 +1,15 @@
+require File.expand_path('../../../../load_paths', __FILE__)
+
+require 'config'
+require 'active_model'
+require 'active_support/core_ext/string/access'
+
+# Show backtraces for deprecated behavior for quicker cleanup.
+ActiveSupport::Deprecation.debug = true
+
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
+require 'active_support/testing/autorun'
+
+require 'mocha/setup' # FIXME: stop using mocha
diff --git a/activemodel/test/cases/lint_test.rb b/activemodel/test/cases/lint_test.rb
new file mode 100644
index 0000000000..8faf93c056
--- /dev/null
+++ b/activemodel/test/cases/lint_test.rb
@@ -0,0 +1,20 @@
+require 'cases/helper'
+
+class LintTest < ActiveModel::TestCase
+ include ActiveModel::Lint::Tests
+
+ class CompliantModel
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def persisted?() false end
+
+ def errors
+ Hash.new([])
+ end
+ end
+
+ def setup
+ @model = CompliantModel.new
+ end
+end
diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb
new file mode 100644
index 0000000000..ee0fa26546
--- /dev/null
+++ b/activemodel/test/cases/model_test.rb
@@ -0,0 +1,75 @@
+require 'cases/helper'
+
+class ModelTest < ActiveModel::TestCase
+ include ActiveModel::Lint::Tests
+
+ module DefaultValue
+ def self.included(klass)
+ klass.class_eval { attr_accessor :hello }
+ end
+
+ def initialize(*args)
+ @attr ||= 'default value'
+ super
+ end
+ end
+
+ class BasicModel
+ include DefaultValue
+ include ActiveModel::Model
+ attr_accessor :attr
+ end
+
+ class BasicModelWithReversedMixins
+ include ActiveModel::Model
+ include DefaultValue
+ attr_accessor :attr
+ end
+
+ class SimpleModel
+ 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 "value", object.attr
+ end
+
+ def test_initialize_with_params_and_mixins_reversed
+ object = BasicModelWithReversedMixins.new(attr: "value")
+ assert_equal "value", object.attr
+ end
+
+ def test_initialize_with_nil_or_empty_hash_params_does_not_explode
+ assert_nothing_raised do
+ BasicModel.new()
+ BasicModel.new(nil)
+ BasicModel.new({})
+ SimpleModel.new(attr: 'value')
+ end
+ end
+
+ def test_persisted_is_always_false
+ object = BasicModel.new(attr: "value")
+ assert object.persisted? == false
+ end
+
+ def test_mixin_inclusion_chain
+ object = BasicModel.new
+ assert_equal 'default value', object.attr
+ end
+
+ def test_mixin_initializer_when_args_exist
+ object = BasicModel.new(hello: 'world')
+ assert_equal 'world', object.hello
+ end
+
+ def test_mixin_initializer_when_args_dont_exist
+ assert_raises(NoMethodError) { SimpleModel.new(hello: 'world') }
+ end
+end
diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb
new file mode 100644
index 0000000000..7b8287edbf
--- /dev/null
+++ b/activemodel/test/cases/naming_test.rb
@@ -0,0 +1,280 @@
+require 'cases/helper'
+require 'models/contact'
+require 'models/sheep'
+require 'models/track_back'
+require 'models/blog_post'
+
+class NamingTest < ActiveModel::TestCase
+ def setup
+ @model_name = ActiveModel::Name.new(Post::TrackBack)
+ end
+
+ def test_singular
+ assert_equal 'post_track_back', @model_name.singular
+ end
+
+ def test_plural
+ assert_equal 'post_track_backs', @model_name.plural
+ end
+
+ def test_element
+ assert_equal 'track_back', @model_name.element
+ end
+
+ def test_collection
+ assert_equal 'post/track_backs', @model_name.collection
+ end
+
+ def test_human
+ assert_equal 'Track back', @model_name.human
+ end
+
+ def test_route_key
+ assert_equal 'post_track_backs', @model_name.route_key
+ end
+
+ def test_param_key
+ assert_equal 'post_track_back', @model_name.param_key
+ end
+
+ def test_i18n_key
+ assert_equal :"post/track_back", @model_name.i18n_key
+ end
+end
+
+class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase
+ def setup
+ @model_name = ActiveModel::Name.new(Blog::Post, Blog)
+ end
+
+ def test_singular
+ assert_equal 'blog_post', @model_name.singular
+ end
+
+ def test_plural
+ assert_equal 'blog_posts', @model_name.plural
+ end
+
+ def test_element
+ assert_equal 'post', @model_name.element
+ end
+
+ def test_collection
+ assert_equal 'blog/posts', @model_name.collection
+ end
+
+ def test_human
+ assert_equal 'Post', @model_name.human
+ end
+
+ def test_route_key
+ assert_equal 'posts', @model_name.route_key
+ end
+
+ def test_param_key
+ assert_equal 'post', @model_name.param_key
+ end
+
+ def test_i18n_key
+ assert_equal :"blog/post", @model_name.i18n_key
+ end
+end
+
+class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase
+ def setup
+ @model_name = ActiveModel::Name.new(Blog::Post)
+ end
+
+ def test_singular
+ assert_equal 'blog_post', @model_name.singular
+ end
+
+ def test_plural
+ assert_equal 'blog_posts', @model_name.plural
+ end
+
+ def test_element
+ assert_equal 'post', @model_name.element
+ end
+
+ def test_collection
+ assert_equal 'blog/posts', @model_name.collection
+ end
+
+ def test_human
+ assert_equal 'Post', @model_name.human
+ end
+
+ def test_route_key
+ assert_equal 'blog_posts', @model_name.route_key
+ end
+
+ def test_param_key
+ assert_equal 'blog_post', @model_name.param_key
+ end
+
+ def test_i18n_key
+ assert_equal :"blog/post", @model_name.i18n_key
+ end
+end
+
+class NamingWithSuppliedModelNameTest < ActiveModel::TestCase
+ def setup
+ @model_name = ActiveModel::Name.new(Blog::Post, nil, 'Article')
+ end
+
+ def test_singular
+ assert_equal 'article', @model_name.singular
+ end
+
+ def test_plural
+ assert_equal 'articles', @model_name.plural
+ end
+
+ def test_element
+ assert_equal 'article', @model_name.element
+ end
+
+ def test_collection
+ assert_equal 'articles', @model_name.collection
+ end
+
+ def test_human
+ assert_equal 'Article', @model_name.human
+ end
+
+ def test_route_key
+ assert_equal 'articles', @model_name.route_key
+ end
+
+ def test_param_key
+ assert_equal 'article', @model_name.param_key
+ end
+
+ def test_i18n_key
+ assert_equal :"article", @model_name.i18n_key
+ end
+end
+
+class NamingUsingRelativeModelNameTest < ActiveModel::TestCase
+ def setup
+ @model_name = Blog::Post.model_name
+ end
+
+ def test_singular
+ assert_equal 'blog_post', @model_name.singular
+ end
+
+ def test_plural
+ assert_equal 'blog_posts', @model_name.plural
+ end
+
+ def test_element
+ assert_equal 'post', @model_name.element
+ end
+
+ def test_collection
+ assert_equal 'blog/posts', @model_name.collection
+ end
+
+ def test_human
+ assert_equal 'Post', @model_name.human
+ end
+
+ def test_route_key
+ assert_equal 'posts', @model_name.route_key
+ end
+
+ def test_param_key
+ assert_equal 'post', @model_name.param_key
+ end
+
+ def test_i18n_key
+ assert_equal :"blog/post", @model_name.i18n_key
+ end
+end
+
+class NamingHelpersTest < ActiveModel::TestCase
+ def setup
+ @klass = Contact
+ @record = @klass.new
+ @singular = 'contact'
+ @plural = 'contacts'
+ @uncountable = Sheep
+ @singular_route_key = 'contact'
+ @route_key = 'contacts'
+ @param_key = 'contact'
+ end
+
+ def test_to_model_called_on_record
+ assert_equal 'post_named_track_backs', plural(Post::TrackBack.new)
+ end
+
+ def test_singular
+ assert_equal @singular, singular(@record)
+ end
+
+ def test_singular_for_class
+ assert_equal @singular, singular(@klass)
+ end
+
+ def test_plural
+ assert_equal @plural, plural(@record)
+ end
+
+ def test_plural_for_class
+ assert_equal @plural, plural(@klass)
+ end
+
+ def test_route_key
+ assert_equal @route_key, route_key(@record)
+ assert_equal @singular_route_key, singular_route_key(@record)
+ end
+
+ def test_route_key_for_class
+ assert_equal @route_key, route_key(@klass)
+ assert_equal @singular_route_key, singular_route_key(@klass)
+ end
+
+ def test_param_key
+ assert_equal @param_key, param_key(@record)
+ end
+
+ def test_param_key_for_class
+ assert_equal @param_key, param_key(@klass)
+ end
+
+ def test_uncountable
+ assert uncountable?(@uncountable), "Expected 'sheep' to be uncountable"
+ assert !uncountable?(@klass), "Expected 'contact' to be countable"
+ end
+
+ def test_uncountable_route_key
+ assert_equal "sheep", singular_route_key(@uncountable)
+ assert_equal "sheep_index", route_key(@uncountable)
+ end
+
+ private
+ def method_missing(method, *args)
+ ActiveModel::Naming.send(method, *args)
+ end
+end
+
+class NameWithAnonymousClassTest < ActiveModel::TestCase
+ def test_anonymous_class_without_name_argument
+ assert_raises(ArgumentError) do
+ ActiveModel::Name.new(Class.new)
+ end
+ end
+
+ def test_anonymous_class_with_name_argument
+ model_name = ActiveModel::Name.new(Class.new, nil, "Anonymous")
+ assert_equal "Anonymous", model_name
+ end
+end
+
+class NamingMethodDelegationTest < ActiveModel::TestCase
+ def test_model_name
+ assert_equal Blog::Post.model_name, Blog::Post.new.model_name
+ end
+end
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
new file mode 100644
index 0000000000..96b3b07e50
--- /dev/null
+++ b/activemodel/test/cases/railtie_test.rb
@@ -0,0 +1,32 @@
+require 'cases/helper'
+require 'active_support/testing/isolation'
+
+class RailtieTest < ActiveModel::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ require 'active_model/railtie'
+
+ # Set a fake logger to avoid creating the log directory automatically
+ fake_logger = Logger.new(nil)
+
+ @app ||= Class.new(::Rails::Application) do
+ config.eager_load = false
+ config.logger = fake_logger
+ end
+ end
+
+ test 'secure password min_cost is false in the development environment' do
+ Rails.env = 'development'
+ @app.initialize!
+
+ assert_equal false, ActiveModel::SecurePassword.min_cost
+ end
+
+ test 'secure password min_cost is true in the test environment' do
+ Rails.env = 'test'
+ @app.initialize!
+
+ assert_equal true, ActiveModel::SecurePassword.min_cost
+ end
+end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
new file mode 100644
index 0000000000..6d56c8344a
--- /dev/null
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -0,0 +1,218 @@
+require 'cases/helper'
+require 'models/user'
+require 'models/visitor'
+
+class SecurePasswordTest < ActiveModel::TestCase
+ setup do
+ # Used only to speed up tests
+ @original_min_cost = ActiveModel::SecurePassword.min_cost
+ ActiveModel::SecurePassword.min_cost = true
+
+ @user = User.new
+ @visitor = Visitor.new
+
+ # Simulate loading an existing user from the DB
+ @existing_user = User.new
+ @existing_user.password_digest = BCrypt::Password.create('password', cost: BCrypt::Engine::MIN_COST)
+ end
+
+ teardown do
+ ActiveModel::SecurePassword.min_cost = @original_min_cost
+ end
+
+ test "automatically include ActiveModel::Validations when validations are enabled" do
+ assert_respond_to @user, :valid?
+ end
+
+ test "don't include ActiveModel::Validations when validations are disabled" do
+ assert_not_respond_to @visitor, :valid?
+ end
+
+ test "create a new user with validations and valid password/confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = 'password'
+
+ assert @user.valid?(:create), 'user should be valid'
+
+ @user.password = 'a' * 72
+ @user.password_confirmation = 'a' * 72
+
+ assert @user.valid?(:create), 'user should be valid'
+ end
+
+ test "create a new user with validation and a spaces only password" do
+ @user.password = ' ' * 72
+ assert @user.valid?(:create), 'user should be valid'
+ end
+
+ test "create a new user with validation and a blank password" do
+ @user.password = ''
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["can't be blank"], @user.errors[:password]
+ end
+
+ test "create a new user with validation and a nil password" do
+ @user.password = nil
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["can't be blank"], @user.errors[:password]
+ end
+
+ test 'create a new user with validation and password length greater than 72' do
+ @user.password = 'a' * 73
+ @user.password_confirmation = 'a' * 73
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password]
+ end
+
+ test "create a new user with validation and a blank password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = ''
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
+ end
+
+ test "create a new user with validation and a nil password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = nil
+ assert @user.valid?(:create), 'user should be valid'
+ end
+
+ test "create a new user with validation and an incorrect password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = 'something else'
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
+ end
+
+ test "update an existing user with validation and no change in password" do
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
+
+ test "update an existing user with validations and valid password/confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = 'password'
+
+ assert @existing_user.valid?(:update), 'user should be valid'
+
+ @existing_user.password = 'a' * 72
+ @existing_user.password_confirmation = 'a' * 72
+
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
+
+ test "updating an existing user with validation and a blank password" do
+ @existing_user.password = ''
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
+
+ test "updating an existing user with validation and a spaces only password" do
+ @user.password = ' ' * 72
+ assert @user.valid?(:update), 'user should be valid'
+ end
+
+ test "updating an existing user with validation and a blank password and password_confirmation" do
+ @existing_user.password = ''
+ @existing_user.password_confirmation = ''
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
+
+ test "updating an existing user with validation and a nil password" do
+ @existing_user.password = nil
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test 'updating an existing user with validation and password length greater than 72' do
+ @existing_user.password = 'a' * 73
+ @existing_user.password_confirmation = 'a' * 73
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["is too long (maximum is 72 characters)"], @existing_user.errors[:password]
+ end
+
+ test "updating an existing user with validation and a blank password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = ''
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
+ end
+
+ test "updating an existing user with validation and a nil password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = nil
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
+
+ test "updating an existing user with validation and an incorrect password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = 'something else'
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
+ end
+
+ test "updating an existing user with validation and a blank password digest" do
+ @existing_user.password_digest = ''
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test "updating an existing user with validation and a nil password digest" do
+ @existing_user.password_digest = nil
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test "setting a blank password should not change an existing password" do
+ @existing_user.password = ''
+ assert @existing_user.password_digest == 'password'
+ end
+
+ test "setting a nil password should clear an existing password" do
+ @existing_user.password = nil
+ assert_equal nil, @existing_user.password_digest
+ end
+
+ test "authenticate" do
+ @user.password = "secret"
+
+ assert !@user.authenticate("wrong")
+ assert @user.authenticate("secret")
+ end
+
+ test "Password digest cost defaults to bcrypt default cost when min_cost is false" do
+ ActiveModel::SecurePassword.min_cost = false
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine::DEFAULT_COST, @user.password_digest.cost
+ end
+
+ test "Password digest cost honors bcrypt cost attribute when min_cost is false" do
+ begin
+ original_bcrypt_cost = BCrypt::Engine.cost
+ ActiveModel::SecurePassword.min_cost = false
+ BCrypt::Engine.cost = 5
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine.cost, @user.password_digest.cost
+ ensure
+ BCrypt::Engine.cost = original_bcrypt_cost
+ end
+ end
+
+ test "Password digest cost can be set to bcrypt min cost to speed up tests" do
+ ActiveModel::SecurePassword.min_cost = true
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine::MIN_COST, @user.password_digest.cost
+ end
+end
diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb
new file mode 100644
index 0000000000..4ae41aa19c
--- /dev/null
+++ b/activemodel/test/cases/serialization_test.rb
@@ -0,0 +1,168 @@
+require "cases/helper"
+require 'active_support/core_ext/object/instance_variables'
+
+class SerializationTest < ActiveModel::TestCase
+ class User
+ include ActiveModel::Serialization
+
+ attr_accessor :name, :email, :gender, :address, :friends
+
+ def initialize(name, email, gender)
+ @name, @email, @gender = name, email, gender
+ @friends = []
+ end
+
+ def attributes
+ instance_values.except("address", "friends")
+ end
+
+ def foo
+ 'i_am_foo'
+ end
+ end
+
+ class Address
+ include ActiveModel::Serialization
+
+ attr_accessor :street, :city, :state, :zip
+
+ def attributes
+ instance_values
+ end
+ end
+
+ setup do
+ @user = User.new('David', 'david@example.com', 'male')
+ @user.address = Address.new
+ @user.address.street = "123 Lane"
+ @user.address.city = "Springfield"
+ @user.address.state = "CA"
+ @user.address.zip = 11111
+ @user.friends = [User.new('Joe', 'joe@example.com', 'male'),
+ User.new('Sue', 'sue@example.com', 'female')]
+ end
+
+ def test_method_serializable_hash_should_work
+ expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"}
+ assert_equal expected, @user.serializable_hash
+ end
+
+ def test_method_serializable_hash_should_work_with_only_option
+ expected = {"name"=>"David"}
+ assert_equal expected, @user.serializable_hash(only: [:name])
+ end
+
+ def test_method_serializable_hash_should_work_with_except_option
+ expected = {"gender"=>"male", "email"=>"david@example.com"}
+ assert_equal expected, @user.serializable_hash(except: [:name])
+ end
+
+ def test_method_serializable_hash_should_work_with_methods_option
+ expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "email"=>"david@example.com"}
+ assert_equal expected, @user.serializable_hash(methods: [:foo])
+ end
+
+ def test_method_serializable_hash_should_work_with_only_and_methods
+ expected = {"foo"=>"i_am_foo"}
+ assert_equal expected, @user.serializable_hash(only: [], methods: [:foo])
+ end
+
+ def test_method_serializable_hash_should_work_with_except_and_methods
+ expected = {"gender"=>"male", "foo"=>"i_am_foo"}
+ assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo])
+ end
+
+ def test_should_not_call_methods_that_dont_respond
+ expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"}
+ assert_equal expected, @user.serializable_hash(methods: [:bar])
+ end
+
+ def test_should_use_read_attribute_for_serialization
+ def @user.read_attribute_for_serialization(n)
+ "Jon"
+ end
+
+ expected = { "name" => "Jon" }
+ assert_equal expected, @user.serializable_hash(only: :name)
+ end
+
+ 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}}
+ 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'},
+ {"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"=>[]}
+ 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'},
+ {"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"}}
+ 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"=> []}]}
+ assert_equal expected, @user.serializable_hash(include: { friends: { include: :friends } })
+ end
+
+ def test_only_include
+ 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'},
+ {"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'},
+ {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
+ assert_equal expected, @user.serializable_hash(include: [{ address: {only: "street" } }, :friends])
+ end
+end
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
new file mode 100644
index 0000000000..734656b749
--- /dev/null
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -0,0 +1,215 @@
+require 'cases/helper'
+require 'models/contact'
+require 'active_support/core_ext/object/instance_variables'
+
+class Contact
+ include ActiveModel::Serializers::JSON
+ include ActiveModel::Validations
+
+ def attributes=(hash)
+ hash.each do |k, v|
+ instance_variable_set("@#{k}", v)
+ end
+ end
+
+ remove_method :attributes if method_defined?(:attributes)
+
+ def attributes
+ instance_values
+ end
+end
+
+class JsonSerializationTest < ActiveModel::TestCase
+ def setup
+ @contact = Contact.new
+ @contact.name = 'Konata Izumi'
+ @contact.age = 16
+ @contact.created_at = Time.utc(2006, 8, 1)
+ @contact.awesome = true
+ @contact.preferences = { 'shows' => 'anime' }
+ end
+
+ test "should not include root in json (class method)" do
+ 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
+ end
+
+ test "should include root in json if include_root_in_json is true" do
+ begin
+ original_include_root_in_json = Contact.include_root_in_json
+ 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
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
+ end
+ end
+
+ test "should include root in json (option) even if the default is set to false" do
+ 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)
+
+ assert_no_match %r{^\{"contact":\{}, json
+ end
+
+ test "should include custom root in json" do
+ json = @contact.to_json(root: 'json_contact')
+
+ assert_match %r{^\{"json_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 encode all encodable attributes" do
+ json = @contact.to_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 allow attribute filtering with only" do
+ json = @contact.to_json(only: [:name, :age])
+
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_match %r{"age":16}, json
+ assert_no_match %r{"awesome":true}, json
+ assert !json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_no_match %r{"preferences":\{"shows":"anime"\}}, json
+ end
+
+ test "should allow attribute filtering with except" do
+ json = @contact.to_json(except: [:name, :age])
+
+ assert_no_match %r{"name":"Konata Izumi"}, json
+ assert_no_match %r{"age":16}, json
+ assert_match %r{"awesome":true}, json
+ assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_match %r{"preferences":\{"shows":"anime"\}}, json
+ end
+
+ test "methods are called on object" do
+ # Define methods on fixture.
+ def @contact.label; "Has cheezburger"; end
+ def @contact.favorite_quote; "Constraints are liberating"; end
+
+ # Single method.
+ 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])
+ assert_match %r{"label":"Has cheezburger"}, methods_json
+ assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json
+ end
+
+ test "should return Hash for errors" do
+ contact = Contact.new
+ contact.errors.add :name, "can't be blank"
+ contact.errors.add :name, "is too short (minimum is 2 characters)"
+ contact.errors.add :age, "must be 16 or over"
+
+ hash = {}
+ hash[:name] = ["can't be blank", "is too short (minimum is 2 characters)"]
+ hash[:age] = ["must be 16 or over"]
+ assert_equal hash.to_json, contact.errors.to_json
+ end
+
+ test "serializable_hash should not modify options passed in argument" do
+ options = { except: :name }
+ @contact.serializable_hash(options)
+
+ assert_nil options[:only]
+ assert_equal :name, options[:except]
+ end
+
+ test "as_json should return a hash if include_root_in_json is true" do
+ begin
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.as_json
+
+ assert_kind_of Hash, json
+ assert_kind_of Hash, json['contact']
+ %w(name age created_at awesome preferences).each do |field|
+ assert_equal @contact.send(field), json['contact'][field]
+ end
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
+ end
+ end
+
+ test "from_json should work without a root (class attribute)" do
+ 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
+ end
+
+ test "from_json should work without a root (method parameter)" do
+ json = @contact.to_json
+ result = Contact.new.from_json(json, false)
+
+ 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 "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
+ json = @contact.to_json
+
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_match %r{"created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}}, json
+ assert_no_match %r{"awesome":}, json
+ assert_no_match %r{"preferences":}, json
+ end
+
+ test "custom as_json options should be extensible" do
+ def @contact.as_json(options = {}); super(options.merge(only: [:name])); end
+ json = @contact.to_json
+
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_no_match %r{"created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}}, json
+ 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
new file mode 100644
index 0000000000..5db14c8157
--- /dev/null
+++ b/activemodel/test/cases/serializers/xml_serialization_test.rb
@@ -0,0 +1,262 @@
+require 'cases/helper'
+require 'models/contact'
+require 'active_support/core_ext/object/instance_variables'
+require 'ostruct'
+
+class Contact
+ include ActiveModel::Serializers::Xml
+
+ attr_accessor :address, :friends, :contact
+
+ remove_method :attributes if method_defined?(:attributes)
+
+ def attributes
+ instance_values.except("address", "friends", "contact")
+ end
+end
+
+module Admin
+ class Contact < ::Contact
+ end
+end
+
+class Customer < Struct.new(:name)
+end
+
+class Address
+ include ActiveModel::Serializers::Xml
+
+ attr_accessor :street, :city, :state, :zip, :apt_number
+
+ def attributes
+ instance_values
+ end
+end
+
+class SerializableContact < Contact
+ def serializable_hash(options={})
+ super(options.merge(only: [:name, :age]))
+ end
+end
+
+class XmlSerializationTest < ActiveModel::TestCase
+ def setup
+ @contact = Contact.new
+ @contact.name = 'aaron stack'
+ @contact.age = 25
+ @contact.created_at = Time.utc(2006, 8, 1)
+ @contact.awesome = false
+ customer = Customer.new
+ customer.name = "John"
+ @contact.preferences = customer
+ @contact.address = Address.new
+ @contact.address.city = "Springfield"
+ @contact.address.apt_number = 35
+ @contact.friends = [Contact.new, Contact.new]
+ @contact.contact = SerializableContact.new
+ end
+
+ test "should serialize default root" do
+ xml = @contact.to_xml
+ assert_match %r{^<contact>}, xml
+ assert_match %r{</contact>$}, xml
+ end
+
+ test "should serialize namespaced root" do
+ xml = Admin::Contact.new(@contact.attributes).to_xml
+ assert_match %r{^<contact>}, xml
+ assert_match %r{</contact>$}, xml
+ end
+
+ test "should serialize default root with namespace" do
+ xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact"
+ assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, xml
+ assert_match %r{</contact>$}, xml
+ end
+
+ test "should serialize custom root" do
+ xml = @contact.to_xml root: 'xml_contact'
+ assert_match %r{^<xml-contact>}, xml
+ assert_match %r{</xml-contact>$}, xml
+ end
+
+ test "should allow undasherized tags" do
+ xml = @contact.to_xml root: 'xml_contact', dasherize: false
+ assert_match %r{^<xml_contact>}, xml
+ assert_match %r{</xml_contact>$}, xml
+ assert_match %r{<created_at}, xml
+ end
+
+ test "should allow camelized tags" do
+ xml = @contact.to_xml root: 'xml_contact', camelize: true
+ assert_match %r{^<XmlContact>}, xml
+ assert_match %r{</XmlContact>$}, xml
+ assert_match %r{<CreatedAt}, xml
+ end
+
+ test "should allow lower-camelized tags" do
+ xml = @contact.to_xml root: 'xml_contact', camelize: :lower
+ assert_match %r{^<xmlContact>}, xml
+ assert_match %r{</xmlContact>$}, xml
+ assert_match %r{<createdAt}, xml
+ end
+
+ test "should use serializable hash" do
+ @contact = SerializableContact.new
+ @contact.name = 'aaron stack'
+ @contact.age = 25
+
+ xml = @contact.to_xml
+ assert_match %r{<name>aaron stack</name>}, xml
+ assert_match %r{<age type="integer">25</age>}, xml
+ assert_no_match %r{<awesome>}, xml
+ end
+
+ test "should allow skipped types" do
+ xml = @contact.to_xml skip_types: true
+ assert_match %r{<age>25</age>}, xml
+ end
+
+ test "should include yielded additions" do
+ xml_output = @contact.to_xml do |xml|
+ xml.creator "David"
+ end
+ assert_match %r{<creator>David</creator>}, xml_output
+ end
+
+ test "should serialize string" do
+ assert_match %r{<name>aaron stack</name>}, @contact.to_xml
+ end
+
+ test "should serialize nil" do
+ assert_match %r{<pseudonyms nil="true"/>}, @contact.to_xml(methods: :pseudonyms)
+ end
+
+ test "should serialize integer" do
+ assert_match %r{<age type="integer">25</age>}, @contact.to_xml
+ end
+
+ test "should serialize datetime" do
+ assert_match %r{<created-at type="dateTime">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
+ end
+
+ test "should serialize boolean" do
+ assert_match %r{<awesome type="boolean">false</awesome>}, @contact.to_xml
+ end
+
+ test "should serialize array" do
+ assert_match %r{<social type="array">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(methods: :social)
+ end
+
+ test "should serialize hash" do
+ assert_match %r{<network>\s*<git type="symbol">github</git>\s*</network>}, @contact.to_xml(methods: :network)
+ end
+
+ test "should serialize yaml" do
+ assert_match %r{<preferences type="yaml">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml
+ end
+
+ test "should call proc on object" do
+ proc = Proc.new { |options| options[:builder].tag!('nationality', 'unknown') }
+ xml = @contact.to_xml(procs: [ proc ])
+ assert_match %r{<nationality>unknown</nationality>}, xml
+ end
+
+ test "should supply serializable to second proc argument" do
+ proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
+ xml = @contact.to_xml(procs: [ proc ])
+ assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml
+ end
+
+ test "should serialize string correctly when type passed" do
+ xml = @contact.to_xml type: 'Contact'
+ assert_match %r{<contact type="Contact">}, xml
+ assert_match %r{<name>aaron stack</name>}, xml
+ end
+
+ test "include option with singular association" do
+ xml = @contact.to_xml include: :address, indent: 0
+ assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true))
+ end
+
+ test "include option with plural association" do
+ xml = @contact.to_xml include: :friends, indent: 0
+ assert_match %r{<friends type="array">}, xml
+ 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))
+ assert_match %r{<friends type="array">}, xml
+ assert_match %r{<friend type="Contact">}, xml
+ end
+
+ test "include with options" do
+ xml = @contact.to_xml indent: 0, skip_instruct: true, include: { address: { only: :city } }
+ assert xml.include?(%(><address><city>Springfield</city></address>))
+ end
+
+ test "propagates skip_types option to included associations" do
+ xml = @contact.to_xml include: :friends, indent: 0, skip_types: true
+ 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
+
+ test "association with sti" do
+ xml = @contact.to_xml(include: :contact)
+ assert xml.include?(%(<contact type="SerializableContact">))
+ end
+end
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
new file mode 100644
index 0000000000..cedc812ec7
--- /dev/null
+++ b/activemodel/test/cases/translation_test.rb
@@ -0,0 +1,108 @@
+require 'cases/helper'
+require 'models/person'
+
+class ActiveModelI18nTests < ActiveModel::TestCase
+
+ def setup
+ I18n.backend = I18n::Backend::Simple.new
+ end
+
+ def teardown
+ I18n.backend.reload!
+ end
+
+ def test_translated_model_attributes
+ I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute' } } }
+ assert_equal 'person name attribute', Person.human_attribute_name('name')
+ end
+
+ def test_translated_model_attributes_with_default
+ I18n.backend.store_translations 'en', attributes: { name: 'name default attribute' }
+ assert_equal 'name default attribute', Person.human_attribute_name('name')
+ end
+
+ def test_translated_model_attributes_using_default_option
+ assert_equal 'name default attribute', Person.human_attribute_name('name', default: "name default attribute")
+ end
+
+ def test_translated_model_attributes_using_default_option_as_symbol
+ I18n.backend.store_translations 'en', default_name: 'name default attribute'
+ assert_equal 'name default attribute', Person.human_attribute_name('name', default: :default_name)
+ end
+
+ def test_translated_model_attributes_falling_back_to_default
+ assert_equal 'Name', Person.human_attribute_name('name')
+ end
+
+ def test_translated_model_attributes_using_default_option_as_symbol_and_falling_back_to_default
+ assert_equal 'Name', Person.human_attribute_name('name', default: :default_name)
+ end
+
+ def test_translated_model_attributes_with_symbols
+ I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute'} } }
+ assert_equal 'person name attribute', Person.human_attribute_name(:name)
+ end
+
+ def test_translated_model_attributes_with_ancestor
+ I18n.backend.store_translations 'en', activemodel: { attributes: { child: { name: 'child name attribute'} } }
+ assert_equal 'child name attribute', Child.human_attribute_name('name')
+ end
+
+ def test_translated_model_attributes_with_ancestors_fallback
+ I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute'} } }
+ assert_equal 'person name attribute', Child.human_attribute_name('name')
+ end
+
+ def test_translated_model_attributes_with_attribute_matching_namespaced_model_name
+ I18n.backend.store_translations 'en', activemodel: { attributes: {
+ person: { gender: 'person gender'},
+ :"person/gender" => { attribute: 'person gender attribute' }
+ } }
+
+ assert_equal 'person gender', Person.human_attribute_name('gender')
+ 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')
+ end
+
+ def test_translated_nested_model_attributes_with_namespace_fallback
+ I18n.backend.store_translations 'en', activemodel: { attributes: { addresses: { street: 'Cool Address Street' } } }
+ assert_equal 'Cool Address Street', Person.human_attribute_name('addresses.street')
+ end
+
+ def test_translated_model_names
+ I18n.backend.store_translations 'en', activemodel: { models: { person: 'person model' } }
+ assert_equal 'person model', Person.model_name.human
+ end
+
+ def test_translated_model_names_with_sti
+ I18n.backend.store_translations 'en', activemodel: { models: { child: 'child model' } }
+ assert_equal 'child model', Child.model_name.human
+ end
+
+ def test_translated_model_names_with_ancestors_fallback
+ I18n.backend.store_translations 'en', activemodel: { models: { person: 'person model' } }
+ assert_equal 'person model', Child.model_name.human
+ end
+
+ def test_human_does_not_modify_options
+ options = { default: 'person model' }
+ Person.model_name.human(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/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb
new file mode 100644
index 0000000000..ebfe1cf4e4
--- /dev/null
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -0,0 +1,68 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'models/topic'
+require 'models/person'
+require 'models/custom_reader'
+
+class AbsenceValidationTest < ActiveModel::TestCase
+ teardown do
+ Topic.clear_validators!
+ Person.clear_validators!
+ CustomReader.clear_validators!
+ end
+
+ def test_validates_absence_of
+ Topic.validates_absence_of(:title, :content)
+ t = Topic.new
+ t.title = "foo"
+ t.content = "bar"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:title]
+ assert_equal ["must be blank"], t.errors[:content]
+ t.title = ""
+ t.content = "something"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:content]
+ assert_equal [], t.errors[:title]
+ t.content = ""
+ assert t.valid?
+ end
+
+ def test_validates_absence_of_with_array_arguments
+ Topic.validates_absence_of %w(title content)
+ t = Topic.new
+ t.title = "foo"
+ t.content = "bar"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:title]
+ assert_equal ["must be blank"], t.errors[:content]
+ end
+
+ def test_validates_absence_of_with_custom_error_using_quotes
+ Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes"
+ p = Person.new
+ p.karma = "good"
+ assert p.invalid?
+ assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
+ end
+
+ def test_validates_absence_of_for_ruby_class
+ Person.validates_absence_of :karma
+ p = Person.new
+ p.karma = "good"
+ assert p.invalid?
+ assert_equal ["must be blank"], p.errors[:karma]
+ p.karma = nil
+ assert p.valid?
+ end
+
+ def test_validates_absence_of_for_ruby_class_with_custom_reader
+ CustomReader.validates_absence_of :karma
+ p = CustomReader.new
+ p[:karma] = "excellent"
+ assert p.invalid?
+ assert_equal ["must be blank"], p.errors[:karma]
+ p[:karma] = ""
+ assert p.valid?
+ end
+end
diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb
new file mode 100644
index 0000000000..e78aa1adaf
--- /dev/null
+++ b/activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -0,0 +1,68 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+require 'models/reply'
+require 'models/person'
+
+class AcceptanceValidationTest < ActiveModel::TestCase
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ def test_terms_of_service_agreement_no_acceptance
+ Topic.validates_acceptance_of(:terms_of_service)
+
+ t = Topic.new("title" => "We should not be confirmed")
+ assert t.valid?
+ end
+
+ def test_terms_of_service_agreement
+ Topic.validates_acceptance_of(:terms_of_service)
+
+ t = Topic.new("title" => "We should be confirmed","terms_of_service" => "")
+ assert t.invalid?
+ assert_equal ["must be accepted"], t.errors[:terms_of_service]
+
+ t.terms_of_service = "1"
+ assert t.valid?
+ end
+
+ def test_eula
+ Topic.validates_acceptance_of(:eula, message: "must be abided")
+
+ t = Topic.new("title" => "We should be confirmed","eula" => "")
+ assert t.invalid?
+ assert_equal ["must be abided"], t.errors[:eula]
+
+ t.eula = "1"
+ assert t.valid?
+ end
+
+ def test_terms_of_service_agreement_with_accept_value
+ Topic.validates_acceptance_of(:terms_of_service, accept: "I agree.")
+
+ t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
+ assert t.invalid?
+ assert_equal ["must be accepted"], t.errors[:terms_of_service]
+
+ t.terms_of_service = "I agree."
+ assert t.valid?
+ end
+
+ def test_validates_acceptance_of_for_ruby_class
+ Person.validates_acceptance_of :karma
+
+ p = Person.new
+ p.karma = ""
+
+ assert p.invalid?
+ assert_equal ["must be accepted"], p.errors[:karma]
+
+ p.karma = "1"
+ assert p.valid?
+ ensure
+ Person.clear_validators!
+ end
+end
diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb
new file mode 100644
index 0000000000..6cd0f4ed4d
--- /dev/null
+++ b/activemodel/test/cases/validations/callbacks_test.rb
@@ -0,0 +1,98 @@
+# encoding: utf-8
+require 'cases/helper'
+
+class Dog
+ include ActiveModel::Validations
+ include ActiveModel::Validations::Callbacks
+
+ attr_accessor :name, :history
+
+ def initialize
+ @history = []
+ end
+end
+
+class DogWithMethodCallbacks < Dog
+ before_validation :set_before_validation_marker
+ after_validation :set_after_validation_marker
+
+ def set_before_validation_marker; self.history << 'before_validation_marker'; end
+ def set_after_validation_marker; self.history << 'after_validation_marker' ; end
+end
+
+class DogValidatorsAreProc < Dog
+ before_validation { self.history << 'before_validation_marker' }
+ after_validation { self.history << 'after_validation_marker' }
+end
+
+class DogWithTwoValidators < Dog
+ before_validation { self.history << 'before_validation_marker1' }
+ before_validation { self.history << 'before_validation_marker2' }
+end
+
+class DogValidatorReturningFalse < Dog
+ before_validation { false }
+ before_validation { self.history << 'before_validation_marker2' }
+end
+
+class DogWithMissingName < Dog
+ before_validation { self.history << 'before_validation_marker' }
+ validates_presence_of :name
+end
+
+class DogValidatorWithIfCondition < Dog
+ before_validation :set_before_validation_marker1, if: -> { true }
+ before_validation :set_before_validation_marker2, if: -> { false }
+
+ after_validation :set_after_validation_marker1, if: -> { true }
+ after_validation :set_after_validation_marker2, if: -> { false }
+
+ def set_before_validation_marker1; self.history << 'before_validation_marker1'; end
+ def set_before_validation_marker2; self.history << 'before_validation_marker2' ; end
+
+ def set_after_validation_marker1; self.history << 'after_validation_marker1'; end
+ def set_after_validation_marker2; self.history << 'after_validation_marker2' ; end
+end
+
+
+class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase
+
+ def test_if_condition_is_respected_for_before_validation
+ d = DogValidatorWithIfCondition.new
+ d.valid?
+ assert_equal ["before_validation_marker1", "after_validation_marker1"], d.history
+ end
+
+ def test_before_validation_and_after_validation_callbacks_should_be_called
+ d = DogWithMethodCallbacks.new
+ d.valid?
+ assert_equal ['before_validation_marker', 'after_validation_marker'], d.history
+ end
+
+ def test_before_validation_and_after_validation_callbacks_should_be_called_with_proc
+ d = DogValidatorsAreProc.new
+ d.valid?
+ assert_equal ['before_validation_marker', 'after_validation_marker'], d.history
+ end
+
+ def test_before_validation_and_after_validation_callbacks_should_be_called_in_declared_order
+ d = DogWithTwoValidators.new
+ d.valid?
+ assert_equal ['before_validation_marker1', 'before_validation_marker2'], d.history
+ end
+
+ def test_further_callbacks_should_not_be_called_if_before_validation_returns_false
+ d = DogValidatorReturningFalse.new
+ output = d.valid?
+ assert_equal [], d.history
+ assert_equal false, output
+ end
+
+ def test_validation_test_should_be_done
+ d = DogWithMissingName.new
+ output = d.valid?
+ assert_equal ['before_validation_marker'], d.history
+ assert_equal false, output
+ end
+
+end
diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb
new file mode 100644
index 0000000000..1261937b56
--- /dev/null
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -0,0 +1,139 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+
+class ConditionalValidationTest < ActiveModel::TestCase
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ def test_if_validation_using_method_true
+ # When the method returns true
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ def test_unless_validation_using_method_true
+ # When the method returns true
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert_empty t.errors[:title]
+ end
+
+ def test_if_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true_but_its_not)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert_empty t.errors[:title]
+ end
+
+ def test_unless_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true_but_its_not)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ def test_if_validation_using_string_true
+ # When the evaluated string returns true
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1")
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ def test_unless_validation_using_string_true
+ # When the evaluated string returns true
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1")
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert_empty t.errors[:title]
+ end
+
+ def test_if_validation_using_string_false
+ # When the evaluated string returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false")
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert_empty t.errors[:title]
+ end
+
+ def test_unless_validation_using_string_false
+ # When the evaluated string returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false")
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ def test_if_validation_using_block_true
+ # When the block returns true
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
+ if: Proc.new { |r| r.content.size > 4 })
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ def test_unless_validation_using_block_true
+ # When the block returns true
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
+ unless: Proc.new { |r| r.content.size > 4 })
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert_empty t.errors[:title]
+ end
+
+ def test_if_validation_using_block_false
+ # When the block returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
+ if: Proc.new { |r| r.title != "uhohuhoh"})
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.valid?
+ assert_empty t.errors[:title]
+ end
+
+ def test_unless_validation_using_block_false
+ # When the block returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
+ unless: Proc.new { |r| r.title != "uhohuhoh"} )
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ # previous implementation of validates_presence_of eval'd the
+ # string with the wrong binding, this regression test is to
+ # ensure that it works correctly
+ def test_validation_with_if_as_string
+ Topic.validates_presence_of(:title)
+ Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')")
+
+ t = Topic.new
+ assert t.invalid?, "A topic without a title should not be valid"
+ assert_empty t.errors[:author_name], "A topic without an 'important' title should not require an author"
+
+ t.title = "Just a title"
+ assert t.valid?, "A topic with a basic title should be valid"
+
+ t.title = "A very important title"
+ assert t.invalid?, "A topic with an important title, but without an author, should not be valid"
+ assert t.errors[:author_name].any?, "A topic with an 'important' title should require an author"
+
+ t.author_name = "Hubert J. Farnsworth"
+ assert t.valid?, "A topic with an important title and author should be valid"
+ end
+end
diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb
new file mode 100644
index 0000000000..65a2a1eb49
--- /dev/null
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -0,0 +1,108 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+require 'models/person'
+
+class ConfirmationValidationTest < ActiveModel::TestCase
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ def test_no_title_confirmation
+ Topic.validates_confirmation_of(:title)
+
+ t = Topic.new(author_name: "Plutarch")
+ assert t.valid?
+
+ t.title_confirmation = "Parallel Lives"
+ assert t.invalid?
+
+ t.title_confirmation = nil
+ t.title = "Parallel Lives"
+ assert t.valid?
+
+ t.title_confirmation = "Parallel Lives"
+ assert t.valid?
+ end
+
+ def test_title_confirmation
+ Topic.validates_confirmation_of(:title)
+
+ t = Topic.new("title" => "We should be confirmed","title_confirmation" => "")
+ assert t.invalid?
+
+ t.title_confirmation = "We should be confirmed"
+ assert t.valid?
+ end
+
+ def test_validates_confirmation_of_for_ruby_class
+ Person.validates_confirmation_of :karma
+
+ p = Person.new
+ p.karma_confirmation = "None"
+ assert p.invalid?
+
+ assert_equal ["doesn't match Karma"], p.errors[:karma_confirmation]
+
+ p.karma = "None"
+ assert p.valid?
+ ensure
+ Person.clear_validators!
+ end
+
+ def test_title_confirmation_with_i18n_attribute
+ begin
+ @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]
+ ensure
+ I18n.load_path.replace @old_load_path
+ I18n.backend = @old_backend
+ I18n.backend.reload!
+ end
+ end
+
+ test "does not override confirmation reader if present" do
+ klass = Class.new do
+ include ActiveModel::Validations
+
+ def title_confirmation
+ "expected title"
+ end
+
+ validates_confirmation_of :title
+ end
+
+ assert_equal "expected title", klass.new.title_confirmation,
+ "confirmation validation should not override the reader"
+ end
+
+ test "does not override confirmation writer if present" do
+ klass = Class.new do
+ include ActiveModel::Validations
+
+ def title_confirmation=(value)
+ @title_confirmation = "expected title"
+ end
+
+ validates_confirmation_of :title
+ end
+
+ model = klass.new
+ model.title_confirmation = "new title"
+ assert_equal "expected title", model.title_confirmation,
+ "confirmation validation should not override the writer"
+ end
+end
diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb
new file mode 100644
index 0000000000..1ce41f9bc9
--- /dev/null
+++ b/activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -0,0 +1,92 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+require 'models/person'
+
+class ExclusionValidationTest < ActiveModel::TestCase
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ def test_validates_exclusion_of
+ Topic.validates_exclusion_of(:title, in: %w( abe monkey ))
+
+ assert Topic.new("title" => "something", "content" => "abc").valid?
+ assert Topic.new("title" => "monkey", "content" => "abc").invalid?
+ end
+
+ def test_validates_exclusion_of_with_formatted_message
+ Topic.validates_exclusion_of(:title, in: %w( abe monkey ), message: "option %{value} is restricted")
+
+ assert Topic.new("title" => "something", "content" => "abc")
+
+ t = Topic.new("title" => "monkey")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ 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 )
+
+ p = Person.new
+ p.karma = "abe"
+ assert p.invalid?
+
+ assert_equal ["is reserved"], p.errors[:karma]
+
+ p.karma = "Lifo"
+ assert p.valid?
+ ensure
+ Person.clear_validators!
+ end
+
+ def test_validates_exclusion_of_with_lambda
+ Topic.validates_exclusion_of :title, in: lambda { |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
+
+ t = Topic.new
+ t.title = "elephant"
+ t.author_name = "sikachu"
+ assert t.invalid?
+
+ 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.clear_validators!
+ end
+end
diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb
new file mode 100644
index 0000000000..0f91b73cd7
--- /dev/null
+++ b/activemodel/test/cases/validations/format_validation_test.rb
@@ -0,0 +1,149 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+require 'models/person'
+
+class PresenceValidationTest < ActiveModel::TestCase
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ def test_validate_format
+ 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"
+ assert_equal ["is bad data"], t.errors[:title]
+ assert t.errors[:content].empty?
+
+ t.title = "Validation macros rule!"
+
+ assert t.valid?
+ assert t.errors[:title].empty?
+
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) }
+ end
+
+ def test_validate_format_with_allow_blank
+ 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?
+ assert Topic.new("title" => "Validation macros rule!").valid?
+ end
+
+ # testing ticket #3142
+ def test_validate_format_numeric
+ 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"
+
+ assert_equal ["is bad data"], t.errors[:title]
+ assert t.errors[:content].empty?
+
+ t.title = "-11"
+ assert t.invalid?, "Shouldn't be valid"
+
+ t.title = "03"
+ assert t.invalid?, "Shouldn't be valid"
+
+ t.title = "z44"
+ assert t.invalid?, "Shouldn't be valid"
+
+ t.title = "5v7"
+ assert t.invalid?, "Shouldn't be valid"
+
+ t.title = "1"
+
+ assert t.valid?
+ assert t.errors[:title].empty?
+ end
+
+ def test_validate_format_with_formatted_message
+ 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")
+ t = Topic.new
+
+ t.title = "foobar"
+ t.valid?
+ assert_equal ["should not contain foo"], t.errors[:title]
+
+ t.title = "something else"
+ t.valid?
+ assert_equal [], t.errors[:title]
+ end
+
+ def test_validate_format_of_without_any_regexp_should_raise_error
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title) }
+ end
+
+ def test_validates_format_of_with_both_regexps_should_raise_error
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, with: /this/, without: /that/) }
+ end
+
+ def test_validates_format_of_when_with_isnt_a_regexp_should_raise_error
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, with: "clearly not a regexp") }
+ end
+
+ def test_validates_format_of_when_not_isnt_a_regexp_should_raise_error
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, without: "clearly not a regexp") }
+ end
+
+ def test_validates_format_of_with_lambda
+ Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ }
+
+ t = Topic.new
+ t.title = "digit"
+ t.content = "Pixies"
+ assert t.invalid?
+
+ t.content = "1234"
+ assert t.valid?
+ end
+
+ def test_validates_format_of_without_lambda
+ Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ }
+
+ t = Topic.new
+ t.title = "characters"
+ t.content = "1234"
+ assert t.invalid?
+
+ t.content = "Pixies"
+ assert t.valid?
+ end
+
+ def test_validates_format_of_for_ruby_class
+ Person.validates_format_of :karma, with: /\A\d+\Z/
+
+ p = Person.new
+ p.karma = "Pixies"
+ assert p.invalid?
+
+ assert_equal ["is invalid"], p.errors[:karma]
+
+ p.karma = "1234"
+ assert p.valid?
+ ensure
+ Person.clear_validators!
+ end
+end
diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
new file mode 100644
index 0000000000..3eeb80a48b
--- /dev/null
+++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -0,0 +1,150 @@
+require "cases/helper"
+
+require 'models/person'
+
+class I18nGenerateMessageValidationTest < ActiveModel::TestCase
+ def setup
+ Person.clear_validators!
+ @person = Person.new
+ end
+
+ # validates_inclusion_of: generate_message(attr_name, :inclusion, message: custom_message, value: value)
+ def test_generate_message_inclusion_with_default_message
+ assert_equal 'is not included in the list', @person.errors.generate_message(:title, :inclusion, value: 'title')
+ end
+
+ def test_generate_message_inclusion_with_custom_message
+ assert_equal 'custom message title', @person.errors.generate_message(:title, :inclusion, message: 'custom message %{value}', value: 'title')
+ end
+
+ # validates_exclusion_of: generate_message(attr_name, :exclusion, message: custom_message, value: value)
+ def test_generate_message_exclusion_with_default_message
+ assert_equal 'is reserved', @person.errors.generate_message(:title, :exclusion, value: 'title')
+ end
+
+ def test_generate_message_exclusion_with_custom_message
+ assert_equal 'custom message title', @person.errors.generate_message(:title, :exclusion, message: 'custom message %{value}', value: 'title')
+ end
+
+ # validates_format_of: generate_message(attr_name, :invalid, message: custom_message, value: value)
+ def test_generate_message_invalid_with_default_message
+ assert_equal 'is invalid', @person.errors.generate_message(:title, :invalid, value: 'title')
+ end
+
+ def test_generate_message_invalid_with_custom_message
+ assert_equal 'custom message title', @person.errors.generate_message(:title, :invalid, message: 'custom message %{value}', value: 'title')
+ end
+
+ # validates_confirmation_of: generate_message(attr_name, :confirmation, message: custom_message)
+ def test_generate_message_confirmation_with_default_message
+ assert_equal "doesn't match Title", @person.errors.generate_message(:title, :confirmation)
+ end
+
+ def test_generate_message_confirmation_with_custom_message
+ assert_equal 'custom message', @person.errors.generate_message(:title, :confirmation, message: 'custom message')
+ end
+
+ # validates_acceptance_of: generate_message(attr_name, :accepted, message: custom_message)
+ def test_generate_message_accepted_with_default_message
+ assert_equal "must be accepted", @person.errors.generate_message(:title, :accepted)
+ end
+
+ def test_generate_message_accepted_with_custom_message
+ assert_equal 'custom message', @person.errors.generate_message(:title, :accepted, message: 'custom message')
+ end
+
+ # add_on_empty: generate_message(attr, :empty, message: custom_message)
+ def test_generate_message_empty_with_default_message
+ assert_equal "can't be empty", @person.errors.generate_message(:title, :empty)
+ end
+
+ def test_generate_message_empty_with_custom_message
+ assert_equal 'custom message', @person.errors.generate_message(:title, :empty, message: 'custom message')
+ end
+
+ # add_on_blank: generate_message(attr, :blank, message: custom_message)
+ def test_generate_message_blank_with_default_message
+ assert_equal "can't be blank", @person.errors.generate_message(:title, :blank)
+ end
+
+ def test_generate_message_blank_with_custom_message
+ assert_equal 'custom message', @person.errors.generate_message(:title, :blank, message: 'custom message')
+ end
+
+ # validates_length_of: generate_message(attr, :too_long, message: custom_message, count: option_value.end)
+ def test_generate_message_too_long_with_default_message_plural
+ assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, count: 10)
+ end
+
+ def test_generate_message_too_long_with_default_message_singular
+ assert_equal "is too long (maximum is 1 character)", @person.errors.generate_message(:title, :too_long, count: 1)
+ end
+
+ def test_generate_message_too_long_with_custom_message
+ assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, message: 'custom message %{count}', count: 10)
+ end
+
+ # validates_length_of: generate_message(attr, :too_short, default: custom_message, count: option_value.begin)
+ def test_generate_message_too_short_with_default_message_plural
+ assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, count: 10)
+ end
+
+ def test_generate_message_too_short_with_default_message_singular
+ assert_equal "is too short (minimum is 1 character)", @person.errors.generate_message(:title, :too_short, count: 1)
+ end
+
+ def test_generate_message_too_short_with_custom_message
+ assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, message: 'custom message %{count}', count: 10)
+ end
+
+ # validates_length_of: generate_message(attr, :wrong_length, message: custom_message, count: option_value)
+ def test_generate_message_wrong_length_with_default_message_plural
+ assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, count: 10)
+ end
+
+ def test_generate_message_wrong_length_with_default_message_singular
+ assert_equal "is the wrong length (should be 1 character)", @person.errors.generate_message(:title, :wrong_length, count: 1)
+ end
+
+ def test_generate_message_wrong_length_with_custom_message
+ assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, message: 'custom message %{count}', count: 10)
+ end
+
+ # validates_numericality_of: generate_message(attr_name, :not_a_number, value: raw_value, message: custom_message)
+ def test_generate_message_not_a_number_with_default_message
+ assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, value: 'title')
+ end
+
+ def test_generate_message_not_a_number_with_custom_message
+ assert_equal 'custom message title', @person.errors.generate_message(:title, :not_a_number, message: 'custom message %{value}', value: 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, option, value: raw_value, default: custom_message)
+ def test_generate_message_greater_than_with_default_message
+ assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, value: 'title', count: 10)
+ end
+
+ def test_generate_message_greater_than_or_equal_to_with_default_message
+ assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, value: 'title', count: 10)
+ end
+
+ def test_generate_message_equal_to_with_default_message
+ assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, value: 'title', count: 10)
+ end
+
+ def test_generate_message_less_than_with_default_message
+ assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, value: 'title', count: 10)
+ end
+
+ def test_generate_message_less_than_or_equal_to_with_default_message
+ assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, value: 'title', count: 10)
+ end
+
+ def test_generate_message_odd_with_default_message
+ assert_equal "must be odd", @person.errors.generate_message(:title, :odd, value: 'title', count: 10)
+ end
+
+ def test_generate_message_even_with_default_message
+ assert_equal "must be even", @person.errors.generate_message(:title, :even, value: 'title', count: 10)
+ end
+end
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
new file mode 100644
index 0000000000..96084a32ba
--- /dev/null
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -0,0 +1,373 @@
+# -*- coding: utf-8 -*-
+
+require "cases/helper"
+require 'models/person'
+
+class I18nValidationTest < ActiveModel::TestCase
+
+ def setup
+ Person.clear_validators!
+ @person = Person.new
+
+ @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: { custom: nil } })
+ end
+
+ def teardown
+ Person.clear_validators!
+ I18n.load_path.replace @old_load_path
+ I18n.backend = @old_backend
+ I18n.backend.reload!
+ end
+
+ def test_full_message_encoding
+ I18n.backend.store_translations('en', errors: {
+ messages: { too_short: '猫舌' } })
+ Person.validates_length_of :title, within: 3..5
+ @person.valid?
+ assert_equal ['Title 猫舌'], @person.errors.full_messages
+ end
+
+ def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
+ @person.errors.add(:name, 'not found')
+ Person.expects(:human_attribute_name).with(:name, default: 'Name').returns("Person's name")
+ assert_equal ["Person's name not found"], @person.errors.full_messages
+ end
+
+ def test_errors_full_messages_uses_format
+ I18n.backend.store_translations('en', errors: { format: "Field %{attribute} %{message}" })
+ @person.errors.add('name', 'empty')
+ assert_equal ["Field Name empty"], @person.errors.full_messages
+ end
+
+ # ActiveModel::Validations
+
+ # A set of common cases for ActiveModel::Validations message generation that
+ # are used to generate tests to keep things DRY
+ #
+ COMMON_CASES = [
+ # [ case, validation_options, generate_message_options]
+ [ "given no options", {}, {}],
+ [ "given custom message", { message: "custom" }, { message: "custom" }],
+ [ "given if condition", { if: lambda { true }}, {}],
+ [ "given unless condition", { unless: lambda { false }}, {}],
+ [ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }]
+ ]
+
+ # validates_confirmation_of w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ 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, :confirmation, generate_message_options.merge(attribute: 'Title'))
+ @person.valid?
+ end
+ end
+
+ # validates_acceptance_of w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_acceptance_of on generated message #{name}" do
+ Person.validates_acceptance_of :title, validation_options.merge(allow_nil: false)
+ @person.errors.expects(:generate_message).with(:title, :accepted, generate_message_options)
+ @person.valid?
+ end
+ end
+
+ # validates_presence_of w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_presence_of on generated message #{name}" do
+ Person.validates_presence_of :title, validation_options
+ @person.errors.expects(:generate_message).with(:title, :blank, generate_message_options)
+ @person.valid?
+ end
+ end
+
+ # validates_length_of :within too short w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_length_of for :withing on generated message when too short #{name}" do
+ Person.validates_length_of :title, validation_options.merge(within: 3..5)
+ @person.errors.expects(:generate_message).with(:title, :too_short, generate_message_options.merge(count: 3))
+ @person.valid?
+ end
+ end
+
+ # validates_length_of :within too long w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_length_of for :too_long generated message #{name}" do
+ Person.validates_length_of :title, validation_options.merge(within: 3..5)
+ @person.title = 'this title is too long'
+ @person.errors.expects(:generate_message).with(:title, :too_long, generate_message_options.merge(count: 5))
+ @person.valid?
+ end
+ end
+
+ # validates_length_of :is w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_length_of for :is on generated message #{name}" do
+ Person.validates_length_of :title, validation_options.merge(is: 5)
+ @person.errors.expects(:generate_message).with(:title, :wrong_length, generate_message_options.merge(count: 5))
+ @person.valid?
+ end
+ end
+
+ # validates_format_of w/ mocha
+
+ 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: /\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?
+ end
+ end
+
+ # validates_inclusion_of w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_inclusion_of on generated message #{name}" do
+ Person.validates_inclusion_of :title, validation_options.merge(in: %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_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|
+ test "validates_exclusion_of generated message #{name}" do
+ Person.validates_exclusion_of :title, validation_options.merge(in: %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_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|
+ test "validates_numericality_of generated message #{name}" do
+ Person.validates_numericality_of :title, validation_options
+ @person.title = 'a'
+ @person.errors.expects(:generate_message).with(:title, :not_a_number, generate_message_options.merge(value: 'a'))
+ @person.valid?
+ end
+ end
+
+ # validates_numericality_of with :only_integer w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_numericality_of for :only_integer on generated message #{name}" do
+ Person.validates_numericality_of :title, validation_options.merge(only_integer: true)
+ @person.title = '0.0'
+ @person.errors.expects(:generate_message).with(:title, :not_an_integer, generate_message_options.merge(value: '0.0'))
+ @person.valid?
+ end
+ end
+
+ # validates_numericality_of :odd w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_numericality_of for :odd on generated message #{name}" do
+ Person.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true)
+ @person.title = 0
+ @person.errors.expects(:generate_message).with(:title, :odd, generate_message_options.merge(value: 0))
+ @person.valid?
+ end
+ end
+
+ # validates_numericality_of :less_than w/ mocha
+
+ COMMON_CASES.each do |name, validation_options, generate_message_options|
+ test "validates_numericality_of for :less_than on generated message #{name}" do
+ Person.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0)
+ @person.title = 1
+ @person.errors.expects(:generate_message).with(:title, :less_than, generate_message_options.merge(value: 1, count: 0))
+ @person.valid?
+ end
+ end
+
+
+ # 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: { 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[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: { 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[attribute]
+ end
+
+ # test "validates_confirmation_of finds global default key translation when blank"
+ test "#{validation} finds global default key translation when #{error_type}" do
+ I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} }
+
+ yield(@person, {})
+ @person.valid?
+ assert_equal ['global message'], @person.errors[attribute]
+ end
+ end
+
+ # validates_confirmation_of w/o mocha
+
+ set_expectations_for_validation "validates_confirmation_of", :confirmation do |person, options_to_merge|
+ Person.validates_confirmation_of :title, options_to_merge
+ person.title_confirmation = 'foo'
+ end
+
+ # validates_acceptance_of w/o mocha
+
+ set_expectations_for_validation "validates_acceptance_of", :accepted do |person, options_to_merge|
+ Person.validates_acceptance_of :title, options_to_merge.merge(allow_nil: false)
+ end
+
+ # validates_presence_of w/o mocha
+
+ set_expectations_for_validation "validates_presence_of", :blank do |person, options_to_merge|
+ Person.validates_presence_of :title, options_to_merge
+ end
+
+ # validates_length_of :within w/o mocha
+
+ set_expectations_for_validation "validates_length_of", :too_short do |person, options_to_merge|
+ Person.validates_length_of :title, options_to_merge.merge(within: 3..5)
+ end
+
+ set_expectations_for_validation "validates_length_of", :too_long do |person, options_to_merge|
+ Person.validates_length_of :title, options_to_merge.merge(within: 3..5)
+ person.title = "too long"
+ end
+
+ # validates_length_of :is w/o mocha
+
+ set_expectations_for_validation "validates_length_of", :wrong_length do |person, options_to_merge|
+ Person.validates_length_of :title, options_to_merge.merge(is: 5)
+ end
+
+ # 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: /\A[1-9][0-9]*\z/)
+ end
+
+ # validates_inclusion_of w/o mocha
+
+ set_expectations_for_validation "validates_inclusion_of", :inclusion do |person, options_to_merge|
+ Person.validates_inclusion_of :title, options_to_merge.merge(in: %w(a b c))
+ end
+
+ # validates_exclusion_of w/o mocha
+
+ set_expectations_for_validation "validates_exclusion_of", :exclusion do |person, options_to_merge|
+ Person.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c))
+ person.title = 'a'
+ end
+
+ # validates_numericality_of without :only_integer w/o mocha
+
+ set_expectations_for_validation "validates_numericality_of", :not_a_number do |person, options_to_merge|
+ Person.validates_numericality_of :title, options_to_merge
+ person.title = 'a'
+ end
+
+ # validates_numericality_of with :only_integer w/o mocha
+
+ set_expectations_for_validation "validates_numericality_of", :not_an_integer do |person, options_to_merge|
+ Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true)
+ person.title = '1.0'
+ end
+
+ # validates_numericality_of :odd w/o mocha
+
+ set_expectations_for_validation "validates_numericality_of", :odd do |person, options_to_merge|
+ Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, odd: true)
+ person.title = 0
+ end
+
+ # validates_numericality_of :less_than w/o mocha
+
+ set_expectations_for_validation "validates_numericality_of", :less_than do |person, options_to_merge|
+ Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, less_than: 0)
+ person.title = 1
+ end
+
+ # test with validates_with
+
+ def test_validations_with_message_symbol_must_translate
+ I18n.backend.store_translations 'en', errors: { messages: { custom_error: "I am a custom error" } }
+ Person.validates_presence_of :title, message: :custom_error
+ @person.title = nil
+ @person.valid?
+ assert_equal ["I am a custom error"], @person.errors[:title]
+ end
+
+ def test_validates_with_message_symbol_must_translate_per_attribute
+ I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { title: { custom_error: "I am a custom error" } } } } } }
+ Person.validates_presence_of :title, message: :custom_error
+ @person.title = nil
+ @person.valid?
+ assert_equal ["I am a custom error"], @person.errors[:title]
+ end
+
+ def test_validates_with_message_symbol_must_translate_per_model
+ I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { custom_error: "I am a custom error" } } } }
+ Person.validates_presence_of :title, message: :custom_error
+ @person.title = nil
+ @person.valid?
+ assert_equal ["I am a custom error"], @person.errors[:title]
+ end
+
+ def test_validates_with_message_string
+ Person.validates_presence_of :title, message: "I am a custom error"
+ @person.title = nil
+ @person.valid?
+ assert_equal ["I am a custom error"], @person.errors[:title]
+ end
+end
diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb
new file mode 100644
index 0000000000..3a8f3080e1
--- /dev/null
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -0,0 +1,147 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'active_support/all'
+
+require 'models/topic'
+require 'models/person'
+
+class InclusionValidationTest < ActiveModel::TestCase
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ def test_validates_inclusion_of_range
+ Topic.validates_inclusion_of(:title, in: 'aaa'..'bbb')
+ assert Topic.new("title" => "bbc", "content" => "abc").invalid?
+ assert Topic.new("title" => "aa", "content" => "abc").invalid?
+ assert Topic.new("title" => "aaab", "content" => "abc").invalid?
+ assert Topic.new("title" => "aaa", "content" => "abc").valid?
+ assert Topic.new("title" => "abc", "content" => "abc").valid?
+ assert Topic.new("title" => "bbb", "content" => "abc").valid?
+ end
+
+ def test_validates_inclusion_of_time_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.ago..Time.now)
+ assert Topic.new(title: 'aaa', created_at: 2.years.ago).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.ago).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.from_now).invalid?
+ end
+
+ def test_validates_inclusion_of_date_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.until(Date.today)..Date.today)
+ assert Topic.new(title: 'aaa', created_at: 2.years.until(Date.today)).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.until(Date.today)).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.since(Date.today)).invalid?
+ end
+
+ def test_validates_inclusion_of_date_time_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.until(DateTime.current)..DateTime.current)
+ assert Topic.new(title: 'aaa', created_at: 2.years.until(DateTime.current)).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.until(DateTime.current)).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.since(DateTime.current)).invalid?
+ end
+
+ def test_validates_inclusion_of
+ Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ))
+
+ assert Topic.new("title" => "a!", "content" => "abc").invalid?
+ assert Topic.new("title" => "a b", "content" => "abc").invalid?
+ assert Topic.new("title" => nil, "content" => "def").invalid?
+
+ t = Topic.new("title" => "a", "content" => "I know you are but what am I?")
+ assert t.valid?
+ t.title = "uhoh"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is not included in the list"], t.errors[:title]
+
+ assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) }
+ assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: 0) }
+
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: "hi!") }
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: {}) }
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: []) }
+ end
+
+ def test_validates_inclusion_of_with_allow_nil
+ Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), allow_nil: true)
+
+ assert Topic.new("title" => "a!", "content" => "abc").invalid?
+ assert Topic.new("title" => "", "content" => "abc").invalid?
+ assert Topic.new("title" => nil, "content" => "abc").valid?
+ end
+
+ def test_validates_inclusion_of_with_formatted_message
+ Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), message: "option %{value} is not in the list")
+
+ assert Topic.new("title" => "a", "content" => "abc").valid?
+
+ t = Topic.new("title" => "uhoh", "content" => "abc")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ 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 )
+
+ p = Person.new
+ p.karma = "Lifo"
+ assert p.invalid?
+
+ assert_equal ["is not included in the list"], p.errors[:karma]
+
+ p.karma = "monkey"
+ assert p.valid?
+ ensure
+ Person.clear_validators!
+ end
+
+ def test_validates_inclusion_of_with_lambda
+ Topic.validates_inclusion_of :title, in: lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
+
+ t = Topic.new
+ t.title = "wasabi"
+ t.author_name = "sikachu"
+ assert t.invalid?
+
+ 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.clear_validators!
+ end
+end
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
new file mode 100644
index 0000000000..046ffcb16f
--- /dev/null
+++ b/activemodel/test/cases/validations/length_validation_test.rb
@@ -0,0 +1,424 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+require 'models/person'
+
+class LengthValidationTest < ActiveModel::TestCase
+ def teardown
+ Topic.clear_validators!
+ end
+
+ def test_validates_length_of_with_allow_nil
+ Topic.validates_length_of( :title, is: 5, allow_nil: true )
+
+ assert Topic.new("title" => "ab").invalid?
+ assert Topic.new("title" => "").invalid?
+ assert Topic.new("title" => nil).valid?
+ assert Topic.new("title" => "abcde").valid?
+ end
+
+ def test_validates_length_of_with_allow_blank
+ Topic.validates_length_of( :title, is: 5, allow_blank: true )
+
+ assert Topic.new("title" => "ab").invalid?
+ assert Topic.new("title" => "").valid?
+ assert Topic.new("title" => nil).valid?
+ assert Topic.new("title" => "abcde").valid?
+ end
+
+ def test_validates_length_of_using_minimum
+ Topic.validates_length_of :title, minimum: 5
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "not"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title]
+
+ t.title = nil
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"]
+ end
+
+ def test_validates_length_of_using_maximum_should_allow_nil
+ Topic.validates_length_of :title, maximum: 10
+ t = Topic.new
+ assert t.valid?
+ end
+
+ def test_optionally_validates_length_of_using_minimum
+ Topic.validates_length_of :title, minimum: 5, allow_nil: true
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_maximum
+ Topic.validates_length_of :title, maximum: 5
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "notvalid"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert t.valid?
+ end
+
+ def test_optionally_validates_length_of_using_maximum
+ Topic.validates_length_of :title, maximum: 5, allow_nil: true
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_within
+ Topic.validates_length_of(:title, :content, within: 3..5)
+
+ t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long")
+ assert t.invalid?
+ assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content]
+
+ t.title = nil
+ t.content = nil
+ assert t.invalid?
+ assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
+ assert_equal ["is too short (minimum is 3 characters)"], t.errors[:content]
+
+ t.title = "abe"
+ t.content = "mad"
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_within_with_exclusive_range
+ Topic.validates_length_of(:title, within: 4...10)
+
+ t = Topic.new("title" => "9 chars!!")
+ assert t.valid?
+
+ t.title = "Now I'm 10"
+ assert t.invalid?
+ assert_equal ["is too long (maximum is 9 characters)"], t.errors[:title]
+
+ t.title = "Four"
+ assert t.valid?
+ end
+
+ def test_optionally_validates_length_of_using_within
+ Topic.validates_length_of :title, :content, within: 3..5, allow_nil: true
+
+ t = Topic.new('title' => 'abc', 'content' => 'abcd')
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_is
+ Topic.validates_length_of :title, is: 5
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "notvalid"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is the wrong length (should be 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert t.invalid?
+
+ t.title = nil
+ assert t.invalid?
+ end
+
+ def test_optionally_validates_length_of_using_is
+ Topic.validates_length_of :title, is: 5, allow_nil: true
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = nil
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_bignum
+ bigmin = 2 ** 30
+ bigmax = 2 ** 32
+ bigrange = bigmin...bigmax
+ assert_nothing_raised do
+ Topic.validates_length_of :title, is: bigmin + 5
+ Topic.validates_length_of :title, within: bigrange
+ Topic.validates_length_of :title, in: bigrange
+ Topic.validates_length_of :title, minimum: bigmin
+ Topic.validates_length_of :title, maximum: bigmax
+ end
+ end
+
+ def test_validates_length_of_nasty_params
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, is: -6) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, within: 6) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, minimum: "a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, maximum: "a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, within: "a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, is: "a") }
+ end
+
+ def test_validates_length_of_custom_errors_for_minimum_with_message
+ Topic.validates_length_of( :title, minimum: 5, message: "boo %{count}" )
+ t = Topic.new("title" => "uhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["boo 5"], t.errors[:title]
+ end
+
+ def test_validates_length_of_custom_errors_for_minimum_with_too_short
+ Topic.validates_length_of( :title, minimum: 5, too_short: "hoo %{count}" )
+ t = Topic.new("title" => "uhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors[:title]
+ end
+
+ def test_validates_length_of_custom_errors_for_maximum_with_message
+ Topic.validates_length_of( :title, maximum: 5, message: "boo %{count}" )
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["boo 5"], t.errors[:title]
+ end
+
+ def test_validates_length_of_custom_errors_for_in
+ Topic.validates_length_of(:title, in: 10..20, message: "hoo %{count}")
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 10"], t.errors["title"]
+
+ t = Topic.new("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 20"], t.errors["title"]
+ end
+
+ def test_validates_length_of_custom_errors_for_maximum_with_too_long
+ Topic.validates_length_of( :title, maximum: 5, too_long: "hoo %{count}" )
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ def test_validates_length_of_custom_errors_for_both_too_short_and_too_long
+ Topic.validates_length_of :title, minimum: 3, maximum: 5, too_short: 'too short', too_long: 'too long'
+
+ t = Topic.new(title: 'a')
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ['too short'], t.errors['title']
+
+ t = Topic.new(title: 'aaaaaa')
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ['too long'], t.errors['title']
+ end
+
+ def test_validates_length_of_custom_errors_for_is_with_message
+ Topic.validates_length_of( :title, is: 5, message: "boo %{count}" )
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["boo 5"], t.errors["title"]
+ end
+
+ def test_validates_length_of_custom_errors_for_is_with_wrong_length
+ Topic.validates_length_of( :title, is: 5, wrong_length: "hoo %{count}" )
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ def test_validates_length_of_using_minimum_utf8
+ Topic.validates_length_of :title, minimum: 5
+
+ t = Topic.new("title" => "一二三四五", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "一二三四"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"]
+ end
+
+ def test_validates_length_of_using_maximum_utf8
+ Topic.validates_length_of :title, maximum: 5
+
+ t = Topic.new("title" => "一二三四五", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "一二34五六"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors["title"]
+ end
+
+ def test_validates_length_of_using_within_utf8
+ Topic.validates_length_of(:title, :content, within: 3..5)
+
+ t = Topic.new("title" => "一二", "content" => "12三四五六七")
+ assert t.invalid?
+ assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content]
+ t.title = "一二三"
+ t.content = "12三"
+ assert t.valid?
+ end
+
+ def test_optionally_validates_length_of_using_within_utf8
+ Topic.validates_length_of :title, within: 3..5, allow_nil: true
+
+ t = Topic.new(title: "一二三四五")
+ assert t.valid?, t.errors.inspect
+
+ t = Topic.new(title: "一二三")
+ assert t.valid?, t.errors.inspect
+
+ t.title = nil
+ assert t.valid?, t.errors.inspect
+ end
+
+ def test_validates_length_of_using_is_utf8
+ Topic.validates_length_of :title, is: 5
+
+ t = Topic.new("title" => "一二345", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "一二345六"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is the wrong length (should be 5 characters)"], t.errors["title"]
+ end
+
+ def test_validates_length_of_with_block
+ Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.",
+ tokenizer: lambda {|str| str.scan(/\w+/) }
+ t = Topic.new(content: "this content should be long enough")
+ assert t.valid?
+
+ t.content = "not long enough"
+ assert t.invalid?
+ assert t.errors[:content].any?
+ assert_equal ["Your essay must be at least 5 words."], t.errors[:content]
+ end
+
+ def test_validates_length_of_for_fixnum
+ Topic.validates_length_of(:approved, is: 4)
+
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1)
+ assert t.invalid?
+ assert t.errors[:approved].any?
+
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1234)
+ assert t.valid?
+ end
+
+ def test_validates_length_of_for_ruby_class
+ Person.validates_length_of :karma, minimum: 5
+
+ p = Person.new
+ p.karma = "Pix"
+ assert p.invalid?
+
+ assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma]
+
+ p.karma = "The Smiths"
+ assert p.valid?
+ ensure
+ Person.clear_validators!
+ end
+
+ def test_validates_length_of_for_infinite_maxima
+ Topic.validates_length_of(:title, within: 5..Float::INFINITY)
+
+ t = Topic.new("title" => "1234")
+ assert t.invalid?
+ assert t.errors[:title].any?
+
+ t.title = "12345"
+ assert t.valid?
+
+ Topic.validates_length_of(:author_name, maximum: Float::INFINITY)
+
+ assert t.valid?
+
+ t.author_name = "A very long author name that should still be valid." * 100
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_maximum_should_not_allow_nil_when_nil_not_allowed
+ Topic.validates_length_of :title, maximum: 10, allow_nil: false
+ t = Topic.new
+ assert t.invalid?
+ end
+
+ def test_validates_length_of_using_maximum_should_not_allow_nil_and_empty_string_when_blank_not_allowed
+ Topic.validates_length_of :title, maximum: 10, allow_blank: false
+ t = Topic.new
+ assert t.invalid?
+
+ t.title = ""
+ assert t.invalid?
+ end
+
+ def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil
+ Topic.validates_length_of :title, minimum: 5, maximum: 10
+ t = Topic.new
+ assert t.invalid?
+ end
+
+ def test_validates_length_of_using_minimum_0_should_not_allow_nil
+ Topic.validates_length_of :title, minimum: 0
+ t = Topic.new
+ assert t.invalid?
+
+ t.title = ""
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_is_0_should_not_allow_nil
+ Topic.validates_length_of :title, is: 0
+ t = Topic.new
+ assert t.invalid?
+
+ t.title = ""
+ assert t.valid?
+ end
+
+ def test_validates_with_diff_in_option
+ Topic.validates_length_of(:title, is: 5)
+ Topic.validates_length_of(:title, is: 5, if: Proc.new { false } )
+
+ assert Topic.new("title" => "david").valid?
+ assert Topic.new("title" => "david2").invalid?
+ end
+end
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
new file mode 100644
index 0000000000..3834d327ea
--- /dev/null
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -0,0 +1,211 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+require 'models/person'
+
+require 'bigdecimal'
+
+class NumericalityValidationTest < ActiveModel::TestCase
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ NIL = [nil]
+ BLANK = ["", " ", " \t \r \n"]
+ BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significant digits
+ FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1 90.1e1 -90.1e5 -90.1e-5 90e-5)
+ INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090)
+ FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS
+ INTEGERS = [0, 10, -10] + INTEGER_STRINGS
+ BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) }
+ JUNK = ["not a number", "42 not a number", "0xdeadbeef", "0xinvalidhex", "0Xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"]
+ INFINITY = [1.0/0.0]
+
+ def test_default_validates_numericality_of
+ Topic.validates_numericality_of :approved
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
+ def test_validates_numericality_of_with_nil_allowed
+ Topic.validates_numericality_of :approved, allow_nil: true
+
+ invalid!(JUNK + BLANK)
+ valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
+ def test_validates_numericality_of_with_integer_only
+ Topic.validates_numericality_of :approved, only_integer: true
+
+ invalid!(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY)
+ valid!(INTEGERS)
+ end
+
+ def test_validates_numericality_of_with_integer_only_and_nil_allowed
+ Topic.validates_numericality_of :approved, only_integer: true, allow_nil: true
+
+ invalid!(JUNK + BLANK + FLOATS + BIGDECIMAL + INFINITY)
+ valid!(NIL + INTEGERS)
+ end
+
+ def test_validates_numericality_of_with_integer_only_and_symbol_as_value
+ Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
+ def test_validates_numericality_of_with_integer_only_and_proc_as_value
+ Topic.send(:define_method, :allow_only_integers?, lambda { false })
+ Topic.validates_numericality_of :approved, only_integer: Proc.new {|topic| topic.allow_only_integers? }
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
+ def test_validates_numericality_with_greater_than
+ Topic.validates_numericality_of :approved, greater_than: 10
+
+ invalid!([-10, 10], 'must be greater than 10')
+ valid!([11])
+ end
+
+ def test_validates_numericality_with_greater_than_or_equal
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: 10
+
+ invalid!([-9, 9], 'must be greater than or equal to 10')
+ valid!([10])
+ end
+
+ def test_validates_numericality_with_equal_to
+ Topic.validates_numericality_of :approved, equal_to: 10
+
+ invalid!([-10, 11] + INFINITY, 'must be equal to 10')
+ valid!([10])
+ end
+
+ def test_validates_numericality_with_less_than
+ Topic.validates_numericality_of :approved, less_than: 10
+
+ invalid!([10], 'must be less than 10')
+ valid!([-9, 9])
+ end
+
+ def test_validates_numericality_with_less_than_or_equal_to
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: 10
+
+ invalid!([11], 'must be less than or equal to 10')
+ valid!([-10, 10])
+ end
+
+ def test_validates_numericality_with_odd
+ Topic.validates_numericality_of :approved, odd: true
+
+ invalid!([-2, 2], 'must be odd')
+ valid!([-1, 1])
+ end
+
+ def test_validates_numericality_with_even
+ Topic.validates_numericality_of :approved, even: true
+
+ invalid!([-1, 1], 'must be even')
+ valid!([-2, 2])
+ end
+
+ def test_validates_numericality_with_greater_than_less_than_and_even
+ Topic.validates_numericality_of :approved, greater_than: 1, less_than: 4, even: true
+
+ invalid!([1, 3, 4])
+ valid!([2])
+ end
+
+ def test_validates_numericality_with_other_than
+ Topic.validates_numericality_of :approved, other_than: 0
+
+ invalid!([0, 0.0])
+ valid!([-1, 42])
+ end
+
+ def test_validates_numericality_with_proc
+ Topic.send(:define_method, :min_approved, lambda { 5 })
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new {|topic| topic.min_approved }
+
+ invalid!([3, 4])
+ valid!([5, 6])
+ ensure
+ Topic.send(:remove_method, :min_approved)
+ end
+
+ def test_validates_numericality_with_symbol
+ Topic.send(:define_method, :max_approved, lambda { 5 })
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: :max_approved
+
+ invalid!([6])
+ valid!([4, 5])
+ ensure
+ Topic.send(:remove_method, :max_approved)
+ end
+
+ def test_validates_numericality_with_numeric_message
+ Topic.validates_numericality_of :approved, less_than: 4, message: "smaller than %{count}"
+ topic = Topic.new("title" => "numeric test", "approved" => 10)
+
+ assert !topic.valid?
+ assert_equal ["smaller than 4"], topic.errors[:approved]
+
+ Topic.validates_numericality_of :approved, greater_than: 4, message: "greater than %{count}"
+ topic = Topic.new("title" => "numeric test", "approved" => 1)
+
+ assert !topic.valid?
+ assert_equal ["greater than 4"], topic.errors[:approved]
+ end
+
+ def test_validates_numericality_of_for_ruby_class
+ Person.validates_numericality_of :karma, allow_nil: false
+
+ p = Person.new
+ p.karma = "Pix"
+ assert p.invalid?
+
+ assert_equal ["is not a number"], p.errors[:karma]
+
+ p.karma = "1234"
+ assert p.valid?
+ ensure
+ Person.clear_validators!
+ end
+
+ def test_validates_numericality_with_invalid_args
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, greater_than_or_equal_to: "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, less_than_or_equal_to: "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, greater_than: "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, less_than: "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, equal_to: "foo" }
+ end
+
+ private
+
+ def invalid!(values, error = nil)
+ with_each_topic_approved_value(values) do |topic, value|
+ assert topic.invalid?, "#{value.inspect} not rejected as a number"
+ assert topic.errors[:approved].any?, "FAILED for #{value.inspect}"
+ assert_equal error, topic.errors[:approved].first if error
+ end
+ end
+
+ def valid!(values)
+ with_each_topic_approved_value(values) do |topic, value|
+ assert topic.valid?, "#{value.inspect} not accepted as a number"
+ end
+ end
+
+ def with_each_topic_approved_value(values)
+ topic = Topic.new(title: "numeric test", content: "whatever")
+ values.each do |value|
+ topic.approved = value
+ yield topic, value
+ end
+ end
+end
diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb
new file mode 100644
index 0000000000..ecf16d1e16
--- /dev/null
+++ b/activemodel/test/cases/validations/presence_validation_test.rb
@@ -0,0 +1,107 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+require 'models/person'
+require 'models/custom_reader'
+
+class PresenceValidationTest < ActiveModel::TestCase
+
+ teardown do
+ Topic.clear_validators!
+ Person.clear_validators!
+ CustomReader.clear_validators!
+ end
+
+ def test_validate_presences
+ Topic.validates_presence_of(:title, :content)
+
+ t = Topic.new
+ assert t.invalid?
+ assert_equal ["can't be blank"], t.errors[:title]
+ assert_equal ["can't be blank"], t.errors[:content]
+
+ t.title = "something"
+ t.content = " "
+
+ assert t.invalid?
+ assert_equal ["can't be blank"], t.errors[:content]
+
+ t.content = "like stuff"
+
+ assert t.valid?
+ end
+
+ def test_accepts_array_arguments
+ Topic.validates_presence_of %w(title content)
+ t = Topic.new
+ assert t.invalid?
+ assert_equal ["can't be blank"], t.errors[:title]
+ assert_equal ["can't be blank"], t.errors[:content]
+ end
+
+ def test_validates_acceptance_of_with_custom_error_using_quotes
+ Person.validates_presence_of :karma, message: "This string contains 'single' and \"double\" quotes"
+ p = Person.new
+ assert p.invalid?
+ assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
+ end
+
+ def test_validates_presence_of_for_ruby_class
+ Person.validates_presence_of :karma
+
+ p = Person.new
+ assert p.invalid?
+
+ assert_equal ["can't be blank"], p.errors[:karma]
+
+ p.karma = "Cold"
+ assert p.valid?
+ end
+
+ def test_validates_presence_of_for_ruby_class_with_custom_reader
+ CustomReader.validates_presence_of :karma
+
+ p = CustomReader.new
+ assert p.invalid?
+
+ assert_equal ["can't be blank"], p.errors[:karma]
+
+ p[:karma] = "Cold"
+ assert p.valid?
+ end
+
+ def test_validates_presence_of_with_allow_nil_option
+ Topic.validates_presence_of(:title, allow_nil: true)
+
+ t = Topic.new(title: "something")
+ assert t.valid?, t.errors.full_messages
+
+ t.title = ""
+ assert t.invalid?
+ assert_equal ["can't be blank"], t.errors[:title]
+
+ t.title = " "
+ assert t.invalid?, t.errors.full_messages
+ assert_equal ["can't be blank"], t.errors[:title]
+
+ t.title = nil
+ assert t.valid?, t.errors.full_messages
+ end
+
+ def test_validates_presence_of_with_allow_blank_option
+ Topic.validates_presence_of(:title, allow_blank: true)
+
+ t = Topic.new(title: "something")
+ assert t.valid?, t.errors.full_messages
+
+ t.title = ""
+ assert t.valid?, t.errors.full_messages
+
+ t.title = " "
+ assert t.valid?, t.errors.full_messages
+
+ t.title = nil
+ assert t.valid?, t.errors.full_messages
+ end
+end
diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb
new file mode 100644
index 0000000000..699a872e42
--- /dev/null
+++ b/activemodel/test/cases/validations/validates_test.rb
@@ -0,0 +1,159 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'models/person'
+require 'models/topic'
+require 'models/person_with_validator'
+require 'validators/email_validator'
+require 'validators/namespace/email_validator'
+
+class ValidatesTest < ActiveModel::TestCase
+ setup :reset_callbacks
+ teardown :reset_callbacks
+
+ def reset_callbacks
+ Person.clear_validators!
+ Topic.clear_validators!
+ PersonWithValidator.clear_validators!
+ end
+
+ def test_validates_with_messages_empty
+ Person.validates :title, presence: { message: "" }
+ person = Person.new
+ assert !person.valid?, 'person should not be valid.'
+ end
+
+ def test_validates_with_built_in_validation
+ Person.validates :title, numericality: true
+ person = Person.new
+ person.valid?
+ assert_equal ['is not a number'], person.errors[:title]
+ end
+
+ def test_validates_with_attribute_specified_as_string
+ Person.validates "title", numericality: true
+ person = Person.new
+ person.valid?
+ assert_equal ['is not a number'], person.errors[:title]
+
+ person = Person.new
+ person.title = 123
+ assert person.valid?
+ end
+
+ def test_validates_with_built_in_validation_and_options
+ Person.validates :salary, numericality: { message: 'my custom message' }
+ person = Person.new
+ person.valid?
+ assert_equal ['my custom message'], person.errors[:salary]
+ end
+
+ def test_validates_with_validator_class
+ Person.validates :karma, email: true
+ person = Person.new
+ person.valid?
+ assert_equal ['is not an email'], person.errors[:karma]
+ end
+
+ def test_validates_with_namespaced_validator_class
+ Person.validates :karma, :'namespace/email' => true
+ person = Person.new
+ person.valid?
+ assert_equal ['is not an email'], person.errors[:karma]
+ end
+
+ def test_validates_with_if_as_local_conditions
+ Person.validates :karma, presence: true, email: { unless: :condition_is_true }
+ person = Person.new
+ person.valid?
+ assert_equal ["can't be blank"], person.errors[:karma]
+ end
+
+ def test_validates_with_if_as_shared_conditions
+ Person.validates :karma, presence: true, email: true, if: :condition_is_true
+ person = Person.new
+ person.valid?
+ assert_equal ["can't be blank", "is not an email"], person.errors[:karma].sort
+ end
+
+ def test_validates_with_unless_shared_conditions
+ Person.validates :karma, presence: true, email: true, unless: :condition_is_true
+ person = Person.new
+ assert person.valid?
+ end
+
+ def test_validates_with_allow_nil_shared_conditions
+ Person.validates :karma, length: { minimum: 20 }, email: true, allow_nil: true
+ person = Person.new
+ assert person.valid?
+ end
+
+ def test_validates_with_regexp
+ Person.validates :karma, format: /positive|negative/
+ person = Person.new
+ assert person.invalid?
+ assert_equal ['is invalid'], person.errors[:karma]
+ person.karma = "positive"
+ assert person.valid?
+ end
+
+ def test_validates_with_array
+ Person.validates :gender, inclusion: %w(m f)
+ person = Person.new
+ assert person.invalid?
+ assert_equal ['is not included in the list'], person.errors[:gender]
+ person.gender = "m"
+ assert person.valid?
+ end
+
+ def test_validates_with_range
+ Person.validates :karma, length: 6..20
+ person = Person.new
+ assert person.invalid?
+ assert_equal ['is too short (minimum is 6 characters)'], person.errors[:karma]
+ person.karma = 'something'
+ assert person.valid?
+ end
+
+ def test_validates_with_validator_class_and_options
+ Person.validates :karma, email: { message: 'my custom message' }
+ person = Person.new
+ person.valid?
+ assert_equal ['my custom message'], person.errors[:karma]
+ end
+
+ def test_validates_with_unknown_validator
+ assert_raise(ArgumentError) { Person.validates :karma, unknown: true }
+ end
+
+ def test_validates_with_included_validator
+ PersonWithValidator.validates :title, presence: true
+ person = PersonWithValidator.new
+ person.valid?
+ assert_equal ['Local validator'], person.errors[:title]
+ end
+
+ def test_validates_with_included_validator_and_options
+ PersonWithValidator.validates :title, presence: { custom: ' please' }
+ person = PersonWithValidator.new
+ person.valid?
+ assert_equal ['Local validator please'], person.errors[:title]
+ end
+
+ def test_validates_with_included_validator_and_wildcard_shortcut
+ # Shortcut for PersonWithValidator.validates :title, like: { with: "Mr." }
+ PersonWithValidator.validates :title, like: "Mr."
+ person = PersonWithValidator.new
+ person.title = "Ms. Pacman"
+ person.valid?
+ assert_equal ['does not appear to be like Mr.'], person.errors[:title]
+ end
+
+ def test_defining_extra_default_keys_for_validates
+ Topic.validates :title, confirmation: true, message: 'Y U NO CONFIRM'
+ topic = Topic.new
+ topic.title = "What's happening"
+ topic.title_confirmation = "Not this"
+ assert !topic.valid?
+ assert_equal ['Y U NO CONFIRM'], topic.errors[:title_confirmation]
+ end
+end
diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb
new file mode 100644
index 0000000000..005bf118c6
--- /dev/null
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -0,0 +1,50 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+
+class ValidationsContextTest < ActiveModel::TestCase
+ def teardown
+ Topic.clear_validators!
+ end
+
+ ERROR_MESSAGE = "Validation error from validator"
+
+ class ValidatorThatAddsErrors < ActiveModel::Validator
+ def validate(record)
+ record.errors[:base] << ERROR_MESSAGE
+ end
+ end
+
+ test "with a class that adds errors on create and validating a new model with no arguments" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: :create)
+ topic = Topic.new
+ assert topic.valid?, "Validation doesn't run on valid? if 'on' is set to create"
+ end
+
+ test "with a class that adds errors on update and validating a new model" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: :update)
+ topic = Topic.new
+ assert topic.valid?(:create), "Validation doesn't run on create if 'on' is set to update"
+ end
+
+ test "with a class that adds errors on create and validating a new model" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: :create)
+ topic = Topic.new
+ assert topic.invalid?(:create), "Validation does run on create if 'on' is set to create"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ end
+
+ test "with a class that adds errors on multiple contexts and validating a new model" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: [:context1, :context2])
+
+ topic = Topic.new
+ assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2"
+
+ assert topic.invalid?(:context1), "Validation did not run on context1 when 'on' is set to context1 and context2"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+
+ assert topic.invalid?(:context2), "Validation did not run on context2 when 'on' is set to context1 and context2"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ end
+end
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
new file mode 100644
index 0000000000..736c2deea8
--- /dev/null
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -0,0 +1,172 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+
+class ValidatesWithTest < ActiveModel::TestCase
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ ERROR_MESSAGE = "Validation error from validator"
+ OTHER_ERROR_MESSAGE = "Validation error from other validator"
+
+ class ValidatorThatAddsErrors < ActiveModel::Validator
+ def validate(record)
+ record.errors[:base] << ERROR_MESSAGE
+ end
+ end
+
+ class OtherValidatorThatAddsErrors < ActiveModel::Validator
+ def validate(record)
+ record.errors[:base] << OTHER_ERROR_MESSAGE
+ end
+ end
+
+ class ValidatorThatDoesNotAddErrors < ActiveModel::Validator
+ def validate(record)
+ end
+ end
+
+ class ValidatorThatValidatesOptions < ActiveModel::Validator
+ def validate(record)
+ if options[:field] == :first_name
+ record.errors[:base] << ERROR_MESSAGE
+ end
+ end
+ end
+
+ class ValidatorPerEachAttribute < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ record.errors[attribute] << "Value is #{value}"
+ end
+ end
+
+ class ValidatorCheckValidity < ActiveModel::EachValidator
+ def check_validity!
+ raise "boom!"
+ end
+ end
+
+ test "validation with class that adds errors" do
+ Topic.validates_with(ValidatorThatAddsErrors)
+ topic = Topic.new
+ assert topic.invalid?, "A class that adds errors causes the record to be invalid"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ end
+
+ test "with a class that returns valid" do
+ Topic.validates_with(ValidatorThatDoesNotAddErrors)
+ topic = Topic.new
+ assert topic.valid?, "A class that does not add errors does not cause the record to be invalid"
+ end
+
+ test "with multiple classes" do
+ Topic.validates_with(ValidatorThatAddsErrors, OtherValidatorThatAddsErrors)
+ topic = Topic.new
+ assert topic.invalid?
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert topic.errors[:base].include?(OTHER_ERROR_MESSAGE)
+ end
+
+ test "with if statements that return false" do
+ Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2")
+ topic = Topic.new
+ assert topic.valid?
+ end
+
+ test "with if statements that return true" do
+ Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1")
+ topic = Topic.new
+ assert topic.invalid?
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ end
+
+ test "with unless statements that return true" do
+ Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1")
+ topic = Topic.new
+ assert topic.valid?
+ end
+
+ test "with unless statements that returns false" do
+ Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2")
+ topic = Topic.new
+ assert topic.invalid?
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ end
+
+ test "passes all configuration options to the validator class" do
+ topic = Topic.new
+ validator = mock()
+ validator.expects(:new).with(foo: :bar, if: "1 == 1", class: Topic).returns(validator)
+ validator.expects(:validate).with(topic)
+
+ Topic.validates_with(validator, if: "1 == 1", foo: :bar)
+ assert topic.valid?
+ end
+
+ test "validates_with with options" do
+ Topic.validates_with(ValidatorThatValidatesOptions, field: :first_name)
+ topic = Topic.new
+ assert topic.invalid?
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ end
+
+ test "validates_with each validator" do
+ Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content])
+ topic = Topic.new title: "Title", content: "Content"
+ assert topic.invalid?
+ assert_equal ["Value is Title"], topic.errors[:title]
+ assert_equal ["Value is Content"], topic.errors[:content]
+ end
+
+ test "each validator checks validity" do
+ assert_raise RuntimeError do
+ Topic.validates_with(ValidatorCheckValidity, attributes: [:title])
+ end
+ end
+
+ test "each validator expects attributes to be given" do
+ assert_raise ArgumentError do
+ Topic.validates_with(ValidatorPerEachAttribute)
+ end
+ end
+
+ test "each validator skip nil values if :allow_nil is set to true" do
+ Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_nil: true)
+ topic = Topic.new content: ""
+ assert topic.invalid?
+ assert topic.errors[:title].empty?
+ assert_equal ["Value is "], topic.errors[:content]
+ end
+
+ test "each validator skip blank values if :allow_blank is set to true" do
+ Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_blank: true)
+ topic = Topic.new content: ""
+ assert topic.valid?
+ assert topic.errors[:title].empty?
+ assert topic.errors[:content].empty?
+ end
+
+ test "validates_with can validate with an instance method" do
+ Topic.validates :title, with: :my_validation
+
+ topic = Topic.new title: "foo"
+ assert topic.valid?
+ assert topic.errors[:title].empty?
+
+ topic = Topic.new
+ assert !topic.valid?
+ assert_equal ['is missing'], topic.errors[:title]
+ end
+
+ test "optionally pass in the attribute being validated when validating with an instance method" do
+ Topic.validates :title, :content, with: :my_validation_with_arg
+
+ topic = Topic.new title: "foo"
+ assert !topic.valid?
+ assert topic.errors[:title].empty?
+ assert_equal ['is missing'], topic.errors[:content]
+ end
+end
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
new file mode 100644
index 0000000000..ba0aacc2a5
--- /dev/null
+++ b/activemodel/test/cases/validations_test.rb
@@ -0,0 +1,392 @@
+# encoding: utf-8
+require 'cases/helper'
+
+require 'models/topic'
+require 'models/reply'
+require 'models/custom_reader'
+
+require 'active_support/json'
+require 'active_support/xml_mini'
+
+class ValidationsTest < ActiveModel::TestCase
+ class CustomStrictValidationException < StandardError; end
+
+ def teardown
+ Topic.clear_validators!
+ end
+
+ def test_single_field_validation
+ r = Reply.new
+ r.title = "There's no content!"
+ assert r.invalid?, "A reply without content shouldn't be savable"
+ assert r.after_validation_performed, "after_validation callback should be called"
+
+ r.content = "Messa content!"
+ assert r.valid?, "A reply with content should be savable"
+ assert r.after_validation_performed, "after_validation callback should be called"
+ end
+
+ def test_single_attr_validation_and_error_msg
+ r = Reply.new
+ r.title = "There's no content!"
+ assert r.invalid?
+ assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid"
+ assert_equal ["is Empty"], r.errors["content"], "A reply without content should contain an error"
+ assert_equal 1, r.errors.count
+ end
+
+ def test_double_attr_validation_and_error_msg
+ r = Reply.new
+ assert r.invalid?
+
+ assert r.errors[:title].any?, "A reply without title should mark that attribute as invalid"
+ assert_equal ["is Empty"], r.errors["title"], "A reply without title should contain an error"
+
+ assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid"
+ assert_equal ["is Empty"], r.errors["content"], "A reply without content should contain an error"
+
+ assert_equal 2, r.errors.count
+ end
+
+ def test_single_error_per_attr_iteration
+ r = Reply.new
+ r.valid?
+
+ errors = r.errors.collect {|attr, messages| [attr.to_s, messages]}
+
+ assert errors.include?(["title", "is Empty"])
+ assert errors.include?(["content", "is Empty"])
+ end
+
+ def test_multiple_errors_per_attr_iteration_with_full_error_composition
+ r = Reply.new
+ r.title = ""
+ r.content = ""
+ r.valid?
+
+ errors = r.errors.to_a
+
+ assert_equal "Content is Empty", errors[0]
+ assert_equal "Title is Empty", errors[1]
+ assert_equal 2, r.errors.count
+ end
+
+ def test_errors_on_nested_attributes_expands_name
+ t = Topic.new
+ t.errors["replies.name"] << "can't be blank"
+ assert_equal ["Replies name can't be blank"], t.errors.full_messages
+ end
+
+ def test_errors_on_base
+ r = Reply.new
+ r.content = "Mismatch"
+ r.valid?
+ r.errors.add(:base, "Reply is not dignifying")
+
+ errors = r.errors.to_a.inject([]) { |result, error| result + [error] }
+
+ assert_equal ["Reply is not dignifying"], r.errors[:base]
+
+ assert errors.include?("Title is Empty")
+ assert errors.include?("Reply is not dignifying")
+ assert_equal 2, r.errors.count
+ end
+
+ def test_errors_on_base_with_symbol_message
+ r = Reply.new
+ r.content = "Mismatch"
+ r.valid?
+ r.errors.add(:base, :invalid)
+
+ errors = r.errors.to_a.inject([]) { |result, error| result + [error] }
+
+ assert_equal ["is invalid"], r.errors[:base]
+
+ assert errors.include?("Title is Empty")
+ assert errors.include?("is invalid")
+
+ assert_equal 2, r.errors.count
+ end
+
+ def test_errors_empty_after_errors_on_check
+ t = Topic.new
+ assert t.errors[:id].empty?
+ assert t.errors.empty?
+ end
+
+ def test_validates_each
+ hits = 0
+ Topic.validates_each(:title, :content, [:title, :content]) do |record, attr|
+ record.errors.add attr, 'gotcha'
+ hits += 1
+ end
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.invalid?
+ assert_equal 4, hits
+ assert_equal %w(gotcha gotcha), t.errors[:title]
+ assert_equal %w(gotcha gotcha), t.errors[:content]
+ end
+
+ def test_validates_each_custom_reader
+ hits = 0
+ CustomReader.validates_each(:title, :content, [:title, :content]) do |record, attr|
+ record.errors.add attr, 'gotcha'
+ hits += 1
+ end
+ t = CustomReader.new("title" => "valid", "content" => "whatever")
+ assert t.invalid?
+ assert_equal 4, hits
+ assert_equal %w(gotcha gotcha), t.errors[:title]
+ assert_equal %w(gotcha gotcha), t.errors[:content]
+ ensure
+ CustomReader.clear_validators!
+ end
+
+ def test_validate_block
+ Topic.validate { errors.add("title", "will never be valid") }
+ t = Topic.new("title" => "Title", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["will never be valid"], t.errors["title"]
+ end
+
+ def test_validate_block_with_params
+ Topic.validate { |topic| topic.errors.add("title", "will never be valid") }
+ t = Topic.new("title" => "Title", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["will never be valid"], t.errors["title"]
+ end
+
+ def test_invalid_validator
+ Topic.validate :i_dont_exist
+ assert_raises(NoMethodError) do
+ t = Topic.new
+ t.valid?
+ end
+ end
+
+ def test_invalid_options_to_validate
+ assert_raises(ArgumentError) do
+ # A common mistake -- we meant to call 'validates'
+ Topic.validate :title, presence: true
+ end
+ end
+
+ def test_errors_conversions
+ Topic.validates_presence_of %w(title content)
+ t = Topic.new
+ assert t.invalid?
+
+ xml = t.errors.to_xml
+ assert_match %r{<errors>}, xml
+ assert_match %r{<error>Title can't be blank</error>}, xml
+ assert_match %r{<error>Content can't be blank</error>}, xml
+
+ hash = {}
+ hash[:title] = ["can't be blank"]
+ hash[:content] = ["can't be blank"]
+ assert_equal t.errors.to_json, hash.to_json
+ end
+
+ def test_validation_order
+ Topic.validates_presence_of :title
+ Topic.validates_length_of :title, minimum: 2
+
+ t = Topic.new("title" => "")
+ assert t.invalid?
+ assert_equal "can't be blank", t.errors["title"].first
+ Topic.validates_presence_of :title, :author_name
+ Topic.validate {errors.add('author_email_address', 'will never be valid')}
+ Topic.validates_length_of :title, :content, minimum: 2
+
+ t = Topic.new title: ''
+ assert t.invalid?
+
+ assert_equal :title, key = t.errors.keys[0]
+ assert_equal "can't be blank", t.errors[key][0]
+ assert_equal 'is too short (minimum is 2 characters)', t.errors[key][1]
+ assert_equal :author_name, key = t.errors.keys[1]
+ assert_equal "can't be blank", t.errors[key][0]
+ assert_equal :author_email_address, key = t.errors.keys[2]
+ assert_equal 'will never be valid', t.errors[key][0]
+ assert_equal :content, key = t.errors.keys[3]
+ assert_equal 'is too short (minimum is 2 characters)', t.errors[key][0]
+ end
+
+ def test_validation_with_if_and_on
+ Topic.validates_presence_of :title, if: Proc.new{|x| x.author_name = "bad"; true }, on: :update
+
+ t = Topic.new(title: "")
+
+ # If block should not fire
+ assert t.valid?
+ assert t.author_name.nil?
+
+ # If block should fire
+ assert t.invalid?(:update)
+ assert t.author_name == "bad"
+ end
+
+ def test_invalid_should_be_the_opposite_of_valid
+ Topic.validates_presence_of :title
+
+ t = Topic.new
+ assert t.invalid?
+ assert t.errors[:title].any?
+
+ t.title = 'Things are going to change'
+ assert !t.invalid?
+ end
+
+ def test_validation_with_message_as_proc
+ Topic.validates_presence_of(:title, message: proc { "no blanks here".upcase })
+
+ t = Topic.new
+ assert t.invalid?
+ assert_equal ["NO BLANKS HERE"], t.errors[:title]
+ end
+
+ def test_list_of_validators_for_model
+ Topic.validates_presence_of :title
+ Topic.validates_length_of :title, minimum: 2
+
+ assert_equal 2, Topic.validators.count
+ assert_equal [:presence, :length], Topic.validators.map(&:kind)
+ end
+
+ def test_list_of_validators_on_an_attribute
+ Topic.validates_presence_of :title, :content
+ Topic.validates_length_of :title, minimum: 2
+
+ assert_equal 2, Topic.validators_on(:title).count
+ assert_equal [:presence, :length], Topic.validators_on(:title).map(&:kind)
+ assert_equal 1, Topic.validators_on(:content).count
+ assert_equal [:presence], Topic.validators_on(:content).map(&:kind)
+ end
+
+ def test_accessing_instance_of_validator_on_an_attribute
+ Topic.validates_length_of :title, minimum: 10
+ assert_equal 10, Topic.validators_on(:title).first.options[:minimum]
+ end
+
+ def test_list_of_validators_on_multiple_attributes
+ Topic.validates :title, length: { minimum: 10 }
+ Topic.validates :author_name, presence: true, format: /a/
+
+ validators = Topic.validators_on(:title, :author_name)
+
+ assert_equal [
+ ActiveModel::Validations::FormatValidator,
+ ActiveModel::Validations::LengthValidator,
+ ActiveModel::Validations::PresenceValidator
+ ], validators.map { |v| v.class }.sort_by { |c| c.to_s }
+ end
+
+ def test_list_of_validators_will_be_empty_when_empty
+ Topic.validates :title, length: { minimum: 10 }
+ assert_equal [], Topic.validators_on(:author_name)
+ end
+
+ def test_validations_on_the_instance_level
+ Topic.validates :title, :author_name, presence: true
+ Topic.validates :content, length: { minimum: 10 }
+
+ topic = Topic.new
+ assert topic.invalid?
+ assert_equal 3, topic.errors.size
+
+ topic.title = 'Some Title'
+ topic.author_name = 'Some Author'
+ topic.content = 'Some Content Whose Length is more than 10.'
+ assert topic.valid?
+ end
+
+ def test_validate
+ Topic.validate do
+ validates_presence_of :title, :author_name
+ validates_length_of :content, minimum: 10
+ end
+
+ topic = Topic.new
+ assert_empty topic.errors
+
+ topic.validate
+ assert_not_empty topic.errors
+ end
+
+ def test_strict_validation_in_validates
+ Topic.validates :title, strict: true, presence: true
+ assert_raises ActiveModel::StrictValidationFailed do
+ Topic.new.valid?
+ end
+ end
+
+ def test_strict_validation_not_fails
+ Topic.validates :title, strict: true, presence: true
+ assert Topic.new(title: "hello").valid?
+ end
+
+ def test_strict_validation_particular_validator
+ Topic.validates :title, presence: { strict: true }
+ assert_raises ActiveModel::StrictValidationFailed do
+ Topic.new.valid?
+ end
+ end
+
+ def test_strict_validation_in_custom_validator_helper
+ Topic.validates_presence_of :title, strict: true
+ assert_raises ActiveModel::StrictValidationFailed do
+ Topic.new.valid?
+ 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
+ Topic.new.valid?
+ 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
+
+ exception = assert_raises(ActiveModel::StrictValidationFailed) do
+ Topic.new.valid?
+ end
+ assert_equal "Title can't be blank", exception.message
+ end
+
+ def test_does_not_modify_options_argument
+ options = { presence: true }
+ 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" => "Literature")
+ 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/config.rb b/activemodel/test/config.rb
new file mode 100644
index 0000000000..0b577a9936
--- /dev/null
+++ b/activemodel/test/config.rb
@@ -0,0 +1,3 @@
+TEST_ROOT = File.expand_path(File.dirname(__FILE__))
+FIXTURES_ROOT = TEST_ROOT + "/fixtures"
+SCHEMA_FILE = TEST_ROOT + "/schema.rb"
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/blog_post.rb b/activemodel/test/models/blog_post.rb
new file mode 100644
index 0000000000..46eba857df
--- /dev/null
+++ b/activemodel/test/models/blog_post.rb
@@ -0,0 +1,9 @@
+module Blog
+ def self.use_relative_model_naming?
+ true
+ end
+
+ class Post
+ extend ActiveModel::Naming
+ end
+end
diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb
new file mode 100644
index 0000000000..c25be28e1d
--- /dev/null
+++ b/activemodel/test/models/contact.rb
@@ -0,0 +1,26 @@
+class Contact
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ attr_accessor :id, :name, :age, :created_at, :awesome, :preferences
+
+ def social
+ %w(twitter github)
+ end
+
+ def network
+ { git: :github }
+ end
+
+ def initialize(options = {})
+ options.each { |name, value| send("#{name}=", value) }
+ end
+
+ def pseudonyms
+ nil
+ end
+
+ def persisted?
+ id
+ end
+end
diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb
new file mode 100644
index 0000000000..2fbcf79c3d
--- /dev/null
+++ b/activemodel/test/models/custom_reader.rb
@@ -0,0 +1,15 @@
+class CustomReader
+ include ActiveModel::Validations
+
+ def initialize(data = {})
+ @data = data
+ end
+
+ def []=(key, value)
+ @data[key] = value
+ end
+
+ def read_attribute_for_validation(key)
+ @data[key]
+ end
+end \ No newline at end of file
diff --git a/activemodel/test/models/helicopter.rb b/activemodel/test/models/helicopter.rb
new file mode 100644
index 0000000000..933f3c463a
--- /dev/null
+++ b/activemodel/test/models/helicopter.rb
@@ -0,0 +1,7 @@
+class Helicopter
+ include ActiveModel::Conversion
+end
+
+class Helicopter::Comanche
+ include ActiveModel::Conversion
+end
diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb
new file mode 100644
index 0000000000..e896e90f98
--- /dev/null
+++ b/activemodel/test/models/person.rb
@@ -0,0 +1,17 @@
+class Person
+ include ActiveModel::Validations
+ extend ActiveModel::Translation
+
+ attr_accessor :title, :karma, :salary, :gender
+
+ def condition_is_true
+ true
+ end
+end
+
+class Person::Gender
+ extend ActiveModel::Translation
+end
+
+class Child < Person
+end
diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb
new file mode 100644
index 0000000000..505ed880c1
--- /dev/null
+++ b/activemodel/test/models/person_with_validator.rb
@@ -0,0 +1,24 @@
+class PersonWithValidator
+ include ActiveModel::Validations
+
+ class PresenceValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank?
+ end
+ end
+
+ class LikeValidator < ActiveModel::EachValidator
+ def initialize(options)
+ @with = options[:with]
+ super
+ end
+
+ def validate_each(record, attribute, value)
+ unless value[@with]
+ record.errors.add attribute, "does not appear to be like #{@with}"
+ end
+ end
+ end
+
+ attr_accessor :title, :karma
+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/reply.rb b/activemodel/test/models/reply.rb
new file mode 100644
index 0000000000..b77910e671
--- /dev/null
+++ b/activemodel/test/models/reply.rb
@@ -0,0 +1,32 @@
+require 'models/topic'
+
+class Reply < Topic
+ validate :errors_on_empty_content
+ validate :title_is_wrong_create, on: :create
+
+ validate :check_empty_title
+ validate :check_content_mismatch, on: :create
+ validate :check_wrong_update, on: :update
+
+ def check_empty_title
+ errors[:title] << "is Empty" unless title && title.size > 0
+ end
+
+ def errors_on_empty_content
+ errors[:content] << "is Empty" unless content && content.size > 0
+ end
+
+ def check_content_mismatch
+ if title && content && content == "Mismatch"
+ errors[:title] << "is Content Mismatch"
+ end
+ end
+
+ def title_is_wrong_create
+ errors[:title] << "is Wrong Create" if title && title == "Wrong Create"
+ end
+
+ def check_wrong_update
+ errors[:title] << "is Wrong Update" if title && title == "Wrong Update"
+ end
+end
diff --git a/activemodel/test/models/sheep.rb b/activemodel/test/models/sheep.rb
new file mode 100644
index 0000000000..7aba055c4f
--- /dev/null
+++ b/activemodel/test/models/sheep.rb
@@ -0,0 +1,3 @@
+class Sheep
+ extend ActiveModel::Naming
+end
diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb
new file mode 100644
index 0000000000..1411a093e9
--- /dev/null
+++ b/activemodel/test/models/topic.rb
@@ -0,0 +1,40 @@
+class Topic
+ include ActiveModel::Validations
+ include ActiveModel::Validations::Callbacks
+
+ def self._validates_default_keys
+ super | [ :message ]
+ end
+
+ attr_accessor :title, :author_name, :content, :approved, :created_at
+ attr_accessor :after_validation_performed
+
+ after_validation :perform_after_validation
+
+ def initialize(attributes = {})
+ attributes.each do |key, value|
+ send "#{key}=", value
+ end
+ end
+
+ def condition_is_true
+ true
+ end
+
+ def condition_is_true_but_its_not
+ false
+ end
+
+ def perform_after_validation
+ self.after_validation_performed = true
+ end
+
+ def my_validation
+ errors.add :title, "is missing" unless title
+ end
+
+ def my_validation_with_arg(attr)
+ errors.add attr, "is missing" unless send(attr)
+ end
+
+end
diff --git a/activemodel/test/models/track_back.rb b/activemodel/test/models/track_back.rb
new file mode 100644
index 0000000000..768c96ecf0
--- /dev/null
+++ b/activemodel/test/models/track_back.rb
@@ -0,0 +1,11 @@
+class Post
+ class TrackBack
+ def to_model
+ NamedTrackBack.new
+ end
+ end
+
+ class NamedTrackBack
+ extend ActiveModel::Naming
+ end
+end \ No newline at end of file
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
new file mode 100644
index 0000000000..1ec6001c48
--- /dev/null
+++ b/activemodel/test/models/user.rb
@@ -0,0 +1,10 @@
+class User
+ extend ActiveModel::Callbacks
+ include ActiveModel::SecurePassword
+
+ define_model_callbacks :create
+
+ has_secure_password
+
+ attr_accessor :password_digest
+end
diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb
new file mode 100644
index 0000000000..22ad1a3c3d
--- /dev/null
+++ b/activemodel/test/models/visitor.rb
@@ -0,0 +1,10 @@
+class Visitor
+ extend ActiveModel::Callbacks
+ include ActiveModel::SecurePassword
+
+ define_model_callbacks :create
+
+ has_secure_password(validations: false)
+
+ attr_accessor :password_digest, :password_confirmation
+end
diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb
new file mode 100644
index 0000000000..cff47ac230
--- /dev/null
+++ b/activemodel/test/validators/email_validator.rb
@@ -0,0 +1,6 @@
+class EmailValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ record.errors[attribute] << (options[:message] || "is not an email") unless
+ value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
+ end
+end \ No newline at end of file
diff --git a/activemodel/test/validators/namespace/email_validator.rb b/activemodel/test/validators/namespace/email_validator.rb
new file mode 100644
index 0000000000..57e2793ce2
--- /dev/null
+++ b/activemodel/test/validators/namespace/email_validator.rb
@@ -0,0 +1,6 @@
+require 'validators/email_validator'
+
+module Namespace
+ class EmailValidator < ::EmailValidator
+ end
+end