diff options
Diffstat (limited to 'activemodel')
33 files changed, 310 insertions, 700 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 3c1510b31d..bf0120122d 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,8 +1,34 @@ +* Validate multiple contexts on `valid?` and `invalid?` at once. + + Example: + + class Person + include ActiveModel::Validations + + attr_reader :name, :title + validates_presence_of :name, on: :create + validates_presence_of :title, on: :update + end + + person = Person.new + person.valid?([:create, :update]) # => false + person.errors.messages # => {:name=>["can't be blank"], :title=>["can't be blank"]} + + *Dmitry Polushkin* + +* Add case_sensitive option for confirmation validator in models. + + *Akshat Sharma* + * Ensure `method_missing` is called for methods passed to `ActiveModel::Serialization#serializable_hash` that don't exist. *Jay Elaraj* +* Remove `ActiveModel::Serializers::Xml` from core. + + *Zachary Scott* + * Add `ActiveModel::Dirty#[attr_name]_previously_changed?` and `ActiveModel::Dirty#[attr_name]_previous_change` to improve access to recorded changes after the model has been saved. @@ -23,7 +49,7 @@ *Wojciech Wnętrzak* * Deprecate `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and - `ActiveModel::Errors#[]=` methods that have inconsistent behaviour. + `ActiveModel::Errors#[]=` methods that have inconsistent behavior. *Wojciech Wnętrzak* diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 4920666f27..20414c1d61 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -154,8 +154,8 @@ behavior out of the box: * Making objects serializable - ActiveModel::Serialization provides a standard interface for your object - to provide +to_json+ or +to_xml+ serialization. + <tt>ActiveModel::Serialization</tt> provides a standard interface for your object + to provide +to_json+ serialization. class SerialPerson include ActiveModel::Serialization @@ -177,13 +177,6 @@ behavior out of the box: 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 @@ -242,7 +235,7 @@ behavior out of the box: The latest version of Active Model can be installed with RubyGems: - % [sudo] gem install activemodel + % gem install activemodel Source code can be downloaded as part of the Rails project on GitHub diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 7256285a41..5a67f0a151 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,7 +1,7 @@ -dir = File.dirname(__FILE__) - require 'rake/testtask' +dir = File.dirname(__FILE__) + task :default => :test Rake::TestTask.new do |t| @@ -19,18 +19,3 @@ namespace :test do 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/bin/test b/activemodel/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/activemodel/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 8aa1b6f664..4e1b3f7495 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -58,7 +58,6 @@ module ActiveModel eager_autoload do autoload :JSON - autoload :Xml end end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index ff7280f9e5..77e4ce3afe 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -225,7 +225,7 @@ module ActiveModel end # Declares the attributes that should be prefixed and suffixed by - # ActiveModel::AttributeMethods. + # <tt>ActiveModel::AttributeMethods</tt>. # # To use, pass attribute names (as strings or symbols). Be sure to declare # +define_attribute_methods+ after you define any prefix, suffix or affix @@ -253,7 +253,7 @@ module ActiveModel end # Declares an attribute that should be prefixed and suffixed by - # ActiveModel::AttributeMethods. + # <tt>ActiveModel::AttributeMethods</tt>. # # To use, pass an attribute name (as string or symbol). Be sure to declare # +define_attribute_method+ after you define any prefix, suffix or affix @@ -342,7 +342,7 @@ module ActiveModel private # The methods +method_missing+ and +respond_to?+ of this module are # invoked often in a typical rails, both of which invoke the method - # +match_attribute_method?+. The latter method iterates through an + # +matched_attribute_method+. The latter method iterates through an # array doing regular expression matches, which results in a lot of # object creations. Most of the time it returns a +nil+ match. As the # match result is always the same given a +method_name+, this cache is @@ -372,7 +372,7 @@ module ActiveModel "define_method(:'#{name}') do |*args|" end - extra = (extra.map!(&:inspect) << "*args").join(", ") + extra = (extra.map!(&:inspect) << "*args").join(", ".freeze) target = if send =~ CALL_COMPILABLE_REGEXP "#{"self." unless include_private}#{send}(#{extra})" @@ -429,7 +429,7 @@ module ActiveModel if respond_to_without_attributes?(method, true) super else - match = match_attribute_method?(method.to_s) + match = matched_attribute_method(method.to_s) match ? attribute_missing(match, *args, &block) : super end end @@ -454,7 +454,7 @@ module ActiveModel # but found among all methods. Which means that the given method is private. false else - !match_attribute_method?(method.to_s).nil? + !matched_attribute_method(method.to_s).nil? end end @@ -466,7 +466,7 @@ module ActiveModel private # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. - def match_attribute_method?(method_name) + def matched_attribute_method(method_name) matches = self.class.send(:attribute_method_matchers_matching, method_name) matches.detect { |match| attribute_method?(match.attr_name) } end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index c0fc507286..0169c20e0b 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -102,10 +102,10 @@ module ActiveModel # person.changes # => {"name" => ["Bill", "Bob"]} # # If an attribute is modified in-place then make use of - # +[attribute_name]_will_change!+ to mark that the attribute is changing. + # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing. # Otherwise \Active \Model can't track changes to in-place attributes. Note # that Active Record can detect in-place modifications automatically. You do - # not need to call +[attribute_name]_will_change!+ on Active Record models. + # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models. # # person.name_will_change! # person.name_change # => ["Bill", "Bill"] diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 287a2559d2..29e0c977ce 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -43,11 +43,11 @@ module ActiveModel # end # end # - # The last three methods are required in your object for Errors to be + # The last three methods are required in your object for +Errors+ to be # able to generate error messages correctly and also handle multiple - # languages. Of course, if you extend your object with ActiveModel::Translation + # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt> # you will not need to implement the last two. Likewise, using - # ActiveModel::Validations will handle the validation related methods + # <tt>ActiveModel::Validations</tt> will handle the validation related methods # for you. # # The above allows you to do: diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 1f1749af4e..213f2d5b6a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -192,7 +192,7 @@ module ActiveModel private def _singularize(string) - ActiveSupport::Inflector.underscore(string).tr('/', '_') + ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze) end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index c06a64c75f..341651d00d 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -31,16 +31,14 @@ module ActiveModel # of the attributes hash's keys. In order to override this behavior, take a look # at the private method +read_attribute_for_serialization+. # - # Most of the time though, either the JSON or XML serializations are needed. - # Both of these modules automatically include the - # <tt>ActiveModel::Serialization</tt> module, so there is no need to - # explicitly include it. + # The JSON serialization is provided by default when you 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: + # A minimal implementation including JSON would be: # # class Person # include ActiveModel::Serializers::JSON - # include ActiveModel::Serializers::Xml # # attr_accessor :name # @@ -55,13 +53,11 @@ module ActiveModel # person.serializable_hash # => {"name"=>nil} # person.as_json # => {"name"=>nil} # person.to_json # => "{\"name\":null}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # person.as_json # => {"name"=>"Bob"} # person.to_json # => "{\"name\":\"Bob\"}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and # <tt>:include</tt>. The following are all valid examples: @@ -94,6 +90,37 @@ module ActiveModel # person.serializable_hash(except: :name) # => {"age"=>22} # person.serializable_hash(methods: :capitalized_name) # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"} + # + # Example with <tt>:include</tt> option + # + # class User + # include ActiveModel::Serializers::JSON + # attr_accessor :name, :notes # Emulate has_many :notes + # def attributes + # {'name' => nil} + # end + # end + # + # class Note + # include ActiveModel::Serializers::JSON + # attr_accessor :title, :text + # def attributes + # {'title' => nil, 'text' => nil} + # end + # end + # + # note = Note.new + # note.title = 'Battle of Austerlitz' + # note.text = 'Some text here' + # + # user = User.new + # user.name = 'Napoleon' + # user.notes = [note] + # + # user.serializable_hash + # # => {"name" => "Napoleon"} + # user.serializable_hash(include: { notes: { only: 'title' }}) + # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]} def serializable_hash(options = nil) options ||= {} diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb deleted file mode 100644 index e33c766627..0000000000 --- a/activemodel/lib/active_model/serializers/xml.rb +++ /dev/null @@ -1,238 +0,0 @@ -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/array/conversions' -require 'active_support/core_ext/hash/conversions' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/time/acts_like' - -module ActiveModel - module Serializers - # == \Active \Model XML Serializer - module Xml - extend ActiveSupport::Concern - include ActiveModel::Serialization - - included do - extend ActiveModel::Naming - end - - class Serializer #:nodoc: - class Attribute #:nodoc: - attr_reader :name, :value, :type - - def initialize(name, serializable, value) - @name, @serializable = name, serializable - - if value.acts_like?(:time) && value.respond_to?(:in_time_zone) - value = value.in_time_zone - end - - @value = value - @type = compute_type - end - - def decorations - decorations = {} - decorations[:encoding] = 'base64' if type == :binary - decorations[:type] = (type == :string) ? nil : type - decorations[:nil] = true if value.nil? - decorations - end - - protected - - def compute_type - return if value.nil? - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] - type ||= :string if value.respond_to?(:to_str) - type ||= :yaml - type - end - end - - class MethodAttribute < Attribute #:nodoc: - end - - attr_reader :options - - def initialize(serializable, options = nil) - @serializable = serializable - @options = options ? options.dup : {} - end - - def serializable_hash - @serializable.serializable_hash(@options.except(:include)) - end - - def serializable_collection - methods = Array(options[:methods]).map(&:to_s) - serializable_hash.map do |name, value| - name = name.to_s - if methods.include?(name) - self.class::MethodAttribute.new(name, @serializable, value) - else - self.class::Attribute.new(name, @serializable, value) - end - end - end - - def serialize - require 'builder' unless defined? ::Builder - - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - - @builder = options[:builder] - @builder.instruct! unless options[:skip_instruct] - - root = (options[:root] || @serializable.model_name.element).to_s - root = ActiveSupport::XmlMini.rename_key(root, options) - - args = [root] - args << { xmlns: options[:namespace] } if options[:namespace] - args << { type: options[:type] } if options[:type] && !options[:skip_types] - - @builder.tag!(*args) do - add_attributes_and_methods - add_includes - add_extra_behavior - add_procs - yield @builder if block_given? - end - end - - private - - def add_extra_behavior - end - - def add_attributes_and_methods - serializable_collection.each do |attribute| - key = ActiveSupport::XmlMini.rename_key(attribute.name, options) - ActiveSupport::XmlMini.to_tag(key, attribute.value, - options.merge(attribute.decorations)) - end - end - - def add_includes - @serializable.send(:serializable_add_includes, options) do |association, records, opts| - add_associations(association, records, opts) - end - end - - # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. - def add_associations(association, records, opts) - merged_options = opts.merge(options.slice(:builder, :indent)) - merged_options[:skip_instruct] = true - - [:skip_types, :dasherize, :camelize].each do |key| - merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil? - end - - if records.respond_to?(:to_ary) - records = records.to_ary - - tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) - type = options[:skip_types] ? { } : { type: "array" } - association_name = association.to_s.singularize - merged_options[:root] = association_name - - if records.empty? - @builder.tag!(tag, type) - else - @builder.tag!(tag, type) do - records.each do |record| - if options[:skip_types] - record_type = {} - else - record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name - record_type = { type: record_class } - end - - record.to_xml merged_options.merge(record_type) - end - end - end - else - merged_options[:root] = association.to_s - - unless records.class.to_s.underscore == association.to_s - merged_options[:type] = records.class.name - end - - records.to_xml merged_options - end - end - - def add_procs - if procs = options.delete(:procs) - Array(procs).each do |proc| - if proc.arity == 1 - proc.call(options) - else - proc.call(options, @serializable) - end - end - end - end - end - - # Returns XML representing the model. Configuration can be - # passed through +options+. - # - # Without any +options+, the returned XML string will include all the - # model's attributes. - # - # user = User.find(1) - # user.to_xml - # - # <?xml version="1.0" encoding="UTF-8"?> - # <user> - # <id type="integer">1</id> - # <name>David</name> - # <age type="integer">16</age> - # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> - # </user> - # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the - # attributes included, and work similar to the +attributes+ method. - # - # To include the result of some method calls on the model use <tt>:methods</tt>. - # - # To include associations use <tt>:include</tt>. - # - # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt> - def to_xml(options = {}, &block) - Serializer.new(self, options).serialize(&block) - end - - # Sets the model +attributes+ from an XML string. Returns +self+. - # - # class Person - # include ActiveModel::Serializers::Xml - # - # attr_accessor :name, :age, :awesome - # - # def attributes=(hash) - # hash.each do |key, value| - # instance_variable_set("@#{key}", value) - # end - # end - # - # def attributes - # instance_values - # end - # end - # - # xml = { name: 'bob', age: 22, awesome:true }.to_xml - # person = Person.new - # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob"> - # person.name # => "bob" - # person.age # => 22 - # person.awesome # => true - def from_xml(xml) - self.attributes = Hash.from_xml(xml).values.first - self - end - end - end -end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index c1019169e1..f23c920d87 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -87,8 +87,7 @@ module ActiveModel validates_with BlockValidator, _merge_attributes(attr_names), &block end - # :nodoc: - VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze + VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc: # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldy and @@ -130,6 +129,9 @@ module ActiveModel # end # end # + # Note that the return value of validation methods is not relevant. + # It's not possible to halt the validate callback chain. + # # Options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. # Runs in all validation contexts by default (nil). You can pass a symbol @@ -160,7 +162,7 @@ module ActiveModel options = options.dup options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - Array(options[:on]).include?(o.validation_context) + !(Array(options[:on]) & Array(o.validation_context)).empty? } end @@ -402,7 +404,7 @@ module ActiveModel protected def run_validations! #:nodoc: - run_callbacks :validate + _run_validate_callbacks errors.empty? end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index ee160fb483..1bcfedb35d 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -42,9 +42,10 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <tt>:accept</tt> - Specifies value that is considered accepted. - # The default value is a string "1", which makes it easy to relate to - # an HTML checkbox. This should be set to +true+ if you are validating + # * <tt>:accept</tt> - Specifies a value that is considered accepted. + # Also accepts an array of possible values. The default value is + # an array ["1", true], which makes it easy to relate to an HTML + # checkbox. This should be set to, or include, +true+ if you are validating # a database column, since the attribute is typecast from "1" to +true+ # before validation. # diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index b4301c23e4..4b58ef66e3 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -109,7 +109,7 @@ module ActiveModel # Overwrite run validations to include callbacks. def run_validations! #:nodoc: - run_callbacks(:validation) { super } + _run_validation_callbacks { super } end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 1b11c28087..bb2f0bd064 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -3,14 +3,16 @@ module ActiveModel module Validations class ConfirmationValidator < EachValidator # :nodoc: def initialize(options) - super + super({ case_sensitive: true }.merge!(options)) setup!(options[:class]) end def validate_each(record, attribute, value) - if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) - human_attribute_name = record.class.human_attribute_name(attribute) - record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name)) + if (confirmed = record.send("#{attribute}_confirmation")) + unless confimation_value_equal?(record, attribute, value, confirmed) + human_attribute_name = record.class.human_attribute_name(attribute) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name)) + end end end @@ -24,6 +26,14 @@ module ActiveModel :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=") end.compact) end + + def confimation_value_equal?(record, attribute, value, confirmed) + if !options[:case_sensitive] && value.is_a?(String) + value.casecmp(confirmed) == 0 + else + value == confirmed + end + end end module HelperMethods @@ -55,6 +65,8 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "doesn't match # <tt>%{translated_attribute_name}</tt>"). + # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by + # non-text columns (+true+ by default). # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index f342d27275..6f4276cc2a 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -29,7 +29,9 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of items that the value shouldn't # be part of. This can be supplied as a proc, lambda or symbol which returns an - # enumerable. If the enumerable is a range the test is performed with + # enumerable. If the enumerable is a numerical, time or datetime range the test + # is performed with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When + # using a proc or lambda the instance under validation is passed as an argument. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is diff --git a/activemodel/lib/active_model/validations/helper_methods.rb b/activemodel/lib/active_model/validations/helper_methods.rb new file mode 100644 index 0000000000..2176115334 --- /dev/null +++ b/activemodel/lib/active_model/validations/helper_methods.rb @@ -0,0 +1,13 @@ +module ActiveModel + module Validations + module HelperMethods # :nodoc: + private + def _merge_attributes(attr_names) + options = attr_names.extract_options!.symbolize_keys + attr_names.flatten! + options[:attributes] = attr_names + options + end + end + end +end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index c84025f083..03e0ef56d8 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -28,9 +28,9 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of available items. This can be # supplied as a proc, lambda or symbol which returns an enumerable. If the - # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>, - # otherwise with <tt>include?</tt>. When using a proc or lambda the instance - # under validation is passed as an argument. + # enumerable is a numerical, time or datetime range the test is performed + # with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When using + # a proc or lambda the instance under validation is passed as an argument. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # * <tt>:message</tt> - Specifies a custom error message (default is: "is # not included in the list"). diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index c22a58f9e1..910cca2f49 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,8 +1,6 @@ require "active_support/core_ext/string/strip" module ActiveModel - - # == Active \Model Length Validator module Validations class LengthValidator < EachValidator # :nodoc: MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze @@ -100,8 +98,9 @@ module ActiveModel module HelperMethods - # Validates that the specified attribute matches the length restrictions - # supplied. Only one option can be used at a time: + # Validates that the specified attributes match the length restrictions + # supplied. Only one constraint option can be used at a time apart from + # +:minimum+ and +:maximum+ that can be combined together: # # class Person < ActiveRecord::Base # validates_length_of :first_name, maximum: 30 @@ -120,14 +119,18 @@ module ActiveModel # end # end # - # Configuration options: + # Constraint options: + # # * <tt>:minimum</tt> - The minimum size of the attribute. # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by - # default if not used with :minimum. + # default if not used with +:minimum+. # * <tt>:is</tt> - The exact size of the attribute. # * <tt>:within</tt> - A range specifying the minimum and maximum size of # the attribute. # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>. + # + # Other options: + # # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. # * <tt>:too_long</tt> - The error message if the attribute goes over the diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index bda436d8d0..1da4df21e7 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -115,7 +115,7 @@ module ActiveModel key = "#{key.to_s.camelize}Validator" begin - validator = key.include?('::') ? key.constantize : const_get(key) + validator = key.include?('::'.freeze) ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index a2531327bf..6de01b3392 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -1,15 +1,5 @@ module ActiveModel module Validations - module HelperMethods - private - def _merge_attributes(attr_names) - options = attr_names.extract_options!.symbolize_keys - attr_names.flatten! - options[:attributes] = attr_names - options - end - end - class WithValidator < EachValidator # :nodoc: def validate_each(record, attr, val) method_name = options[:with] diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index 3b01644dd1..64a85e01eb 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -58,8 +58,6 @@ class AttributeAssignmentTest < ActiveModel::TestCase end test "assign private attribute" do - rubinius_skip "https://github.com/rubinius/rubinius/issues/3328" - model = Model.new assert_raises(ActiveModel::UnknownAttributeError) do model.assign_attributes(metadata: { a: 1 }) diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index d10c20caad..f6d171bec6 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -299,60 +299,67 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :empty, {}) - assert_deprecated do - person.errors.add_on_empty :name + assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do + assert_deprecated do + person.errors.add_on_empty :name + end end 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, {}) - assert_deprecated do - person.errors.add_on_empty [:name, :age] + expected_calls = [ [:name, :empty, {}], [:age, :empty, {}] ] + assert_called_with(person.errors, :generate_message, expected_calls) do + assert_deprecated do + person.errors.add_on_empty [:name, :age] + end end 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' }) - assert_deprecated do - person.errors.add_on_empty :name, message: 'custom' + assert_called_with(person.errors, :generate_message, [:name, :empty, { message: 'custom' }]) do + assert_deprecated do + person.errors.add_on_empty :name, message: 'custom' + end end 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, {}) - assert_deprecated do - person.errors.add_on_empty :name + assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do + assert_deprecated do + person.errors.add_on_empty :name + end end end test "add_on_blank generates message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :blank, {}) - assert_deprecated do - person.errors.add_on_blank :name + assert_called_with(person.errors, :generate_message, [:name, :blank, {}]) do + assert_deprecated do + person.errors.add_on_blank :name + end end 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, {}) - assert_deprecated do - person.errors.add_on_blank [:name, :age] + expected_calls = [ [:name, :blank, {}], [:age, :blank, {}] ] + assert_called_with(person.errors, :generate_message, expected_calls) do + assert_deprecated do + person.errors.add_on_blank [:name, :age] + end end 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' }) - assert_deprecated do - person.errors.add_on_blank :name, message: 'custom' + assert_called_with(person.errors, :generate_message, [:name, :blank, { message: 'custom' }]) do + assert_deprecated do + person.errors.add_on_blank :name, message: 'custom' + end end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 2b9de5e5d2..27fdbc739c 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -1,6 +1,5 @@ require File.expand_path('../../../../load_paths', __FILE__) -require 'config' require 'active_model' require 'active_support/core_ext/string/access' @@ -11,8 +10,7 @@ ActiveSupport::Deprecation.debug = true I18n.enforce_available_locales = false require 'active_support/testing/autorun' - -require 'mocha/setup' # FIXME: stop using mocha +require 'active_support/testing/method_call_assertions' # Skips the current run on Rubinius using Minitest::Assertions#skip def rubinius_skip(message = '') @@ -22,3 +20,7 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +class ActiveModel::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb deleted file mode 100644 index 22fca5bd17..0000000000 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ /dev/null @@ -1,250 +0,0 @@ -require 'cases/helper' -require 'models/contact' -require 'active_support/core_ext/object/instance_variables' -require 'ostruct' - -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/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index 9c2114d83d..d3995ad5af 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -50,6 +50,20 @@ class AcceptanceValidationTest < ActiveModel::TestCase assert t.valid? end + def test_terms_of_service_agreement_with_multiple_accept_values + Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."]) + + 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? + + t.terms_of_service = "I concur." + assert t.valid? + end + def test_validates_acceptance_of_for_ruby_class Person.validates_acceptance_of :karma diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index c1431548f7..c56bf1c0ad 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -104,4 +104,18 @@ class ConfirmationValidationTest < ActiveModel::TestCase assert_equal "expected title", model.title_confirmation, "confirmation validation should not override the writer" end + + def test_title_confirmation_with_case_sensitive_option_true + Topic.validates_confirmation_of(:title, case_sensitive: true) + + t = Topic.new(title: "title", title_confirmation: "Title") + assert t.invalid? + end + + def test_title_confirmation_with_case_sensitive_option_false + Topic.validates_confirmation_of(:title, case_sensitive: false) + + t = Topic.new(title: "title", title_confirmation: "Title") + assert t.valid? + end end diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index b269c3691a..005bc15df5 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_support/core_ext/numeric/time' require 'models/topic' require 'models/person' @@ -64,6 +65,22 @@ class ExclusionValidationTest < ActiveModel::TestCase assert t.valid? end + def test_validates_exclusion_of_with_range + Topic.validates_exclusion_of :content, in: ("a".."g") + + assert Topic.new(content: 'g').invalid? + assert Topic.new(content: 'h').valid? + end + + def test_validates_exclusion_of_with_time_range + Topic.validates_exclusion_of :created_at, in: 6.days.ago..2.days.ago + + assert Topic.new(created_at: 5.days.ago).invalid? + assert Topic.new(created_at: 3.days.ago).invalid? + assert Topic.new(created_at: 7.days.ago).valid? + assert Topic.new(created_at: 1.day.ago).valid? + end + def test_validates_inclusion_of_with_symbol Person.validates_exclusion_of :karma, in: :reserved_karmas diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 70ee7afecc..09d7226b5a 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -30,8 +30,9 @@ class I18nValidationTest < ActiveModel::TestCase 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 + assert_called_with(Person, :human_attribute_name, [:name, default: 'Name'], returns: "Person's name") do + assert_equal ["Person's name not found"], @person.errors.full_messages + end end def test_errors_full_messages_uses_format @@ -54,176 +55,175 @@ class I18nValidationTest < ActiveModel::TestCase [ "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? + call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :accepted, generate_message_options] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :blank, generate_message_options] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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 + test "validates_length_of for :within 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? + call = [:title, :too_short, generate_message_options.merge(count: 3)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :too_long, generate_message_options.merge(count: 5)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :wrong_length, generate_message_options.merge(count: 5)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :invalid, generate_message_options.merge(value: '72x')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :inclusion, generate_message_options.merge(value: 'z')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :inclusion, generate_message_options.merge(value: 'z')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :exclusion, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :exclusion, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :not_a_number, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :not_an_integer, generate_message_options.merge(value: '0.0')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :odd, generate_message_options.merge(value: 0)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end 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? + call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - - # To make things DRY this macro is defined to define 3 tests for every validation case. + # To make things DRY this macro is created 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'}} @@ -233,7 +233,6 @@ class I18nValidationTest < ActiveModel::TestCase 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'} } @@ -243,7 +242,6 @@ class I18nValidationTest < ActiveModel::TestCase 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'} } @@ -253,27 +251,19 @@ class I18nValidationTest < ActiveModel::TestCase 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 @@ -283,61 +273,43 @@ class I18nValidationTest < ActiveModel::TestCase 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 diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb index 150dce379f..b901a1523e 100644 --- a/activemodel/test/cases/validations/validations_context_test.rb +++ b/activemodel/test/cases/validations/validations_context_test.rb @@ -8,6 +8,7 @@ class ValidationsContextTest < ActiveModel::TestCase end ERROR_MESSAGE = "Validation error from validator" + ANOTHER_ERROR_MESSAGE = "Another validation error from validator" class ValidatorThatAddsErrors < ActiveModel::Validator def validate(record) @@ -15,6 +16,12 @@ class ValidationsContextTest < ActiveModel::TestCase end end + class AnotherValidatorThatAddsErrors < ActiveModel::Validator + def validate(record) + record.errors[:base] << ANOTHER_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 @@ -46,4 +53,16 @@ class ValidationsContextTest < ActiveModel::TestCase 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 + + test "with a class that validating a model for a multiple contexts" do + Topic.validates_with(ValidatorThatAddsErrors, on: :context1) + Topic.validates_with(AnotherValidatorThatAddsErrors, on: :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, :context2]), "Validation did not run on context1 when 'on' is set to context1 and context2" + assert topic.errors[:base].include?(ERROR_MESSAGE) + assert topic.errors[:base].include?(ANOTHER_ERROR_MESSAGE) + end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 01804032f0..03c7943308 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -97,12 +97,14 @@ class ValidatesWithTest < ActiveModel::TestCase 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) + validator = Minitest::Mock.new + validator.expect(:new, validator, [{foo: :bar, if: "1 == 1", class: Topic}]) + validator.expect(:validate, nil, [topic]) + validator.expect(:is_a?, false, [Symbol]) Topic.validates_with(validator, if: "1 == 1", foo: :bar) assert topic.valid? + validator.verify end test "validates_with with options" do diff --git a/activemodel/test/config.rb b/activemodel/test/config.rb deleted file mode 100644 index 0b577a9936..0000000000 --- a/activemodel/test/config.rb +++ /dev/null @@ -1,3 +0,0 @@ -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/contact.rb b/activemodel/test/models/contact.rb index bcfd267a34..113ab0bc1f 100644 --- a/activemodel/test/models/contact.rb +++ b/activemodel/test/models/contact.rb @@ -4,7 +4,6 @@ class Contact include ActiveModel::Validations include ActiveModel::Serializers::JSON - include ActiveModel::Serializers::Xml attr_accessor :id, :name, :age, :created_at, :awesome, :preferences attr_accessor :address, :friends, :contact |