diff options
Diffstat (limited to 'activemodel')
-rw-r--r-- | activemodel/CHANGELOG.md | 12 | ||||
-rw-r--r-- | activemodel/README.rdoc | 8 | ||||
-rw-r--r-- | activemodel/lib/active_model/callbacks.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/dirty.rb | 17 | ||||
-rw-r--r-- | activemodel/lib/active_model/errors.rb | 53 | ||||
-rw-r--r-- | activemodel/lib/active_model/locale/en.yml | 1 | ||||
-rw-r--r-- | activemodel/lib/active_model/naming.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations.rb | 33 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/format.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/length.rb | 10 | ||||
-rw-r--r-- | activemodel/lib/active_model/validator.rb | 2 | ||||
-rw-r--r-- | activemodel/test/cases/errors_test.rb | 64 | ||||
-rw-r--r-- | activemodel/test/cases/serializers/json_serialization_test.rb | 4 | ||||
-rw-r--r-- | activemodel/test/cases/validations/i18n_generate_message_validation_test.rb | 2 | ||||
-rw-r--r-- | activemodel/test/cases/validations_test.rb | 19 |
15 files changed, 178 insertions, 53 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index c303bf5684..80b42859e2 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,10 +1,20 @@ +* Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` + with no replacement. + + *Wojciech Wnętrzak* + +* Deprecate `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and + `ActiveModel::Errors#[]=` methods that have inconsistent behaviour. + + *Wojciech Wnętrzak* + * Allow symbol as values for `tokenize` of `LengthValidator` *Kensuke Naito* * Assigning an unknown attribute key to an `ActiveModel` instance during initialization will now raise `ActiveModel::AttributeAssignment::UnknownAttributeError` instead of - `NoMethodError` + `NoMethodError`. Example: diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index f6beff14e1..4920666f27 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -49,7 +49,7 @@ behavior out of the box: send("#{attr}=", nil) end end - + person = Person.new person.clear_name person.clear_age @@ -132,7 +132,7 @@ behavior out of the box: "Name" end end - + person = Person.new person.name = nil person.validate! @@ -216,10 +216,10 @@ behavior out of the box: {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? + record.errors.add(:name, "must exist") if record.name.blank? end end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 6214802074..2cf39b68fb 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -49,7 +49,7 @@ module ActiveModel # puts 'block successfully called.' # end # - # You can choose not to have all three callbacks by passing a hash to the + # You can choose to have only specific callbacks by passing a hash to the # +define_model_callbacks+ method. # # define_model_callbacks :create, only: [:after, :before] diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index ca5dac272f..c03e5fac79 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -118,7 +118,7 @@ module ActiveModel attribute_method_affix prefix: 'restore_', suffix: '!' end - # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. + # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise. # # person.changed? # => false # person.name = 'bob' @@ -166,7 +166,7 @@ module ActiveModel @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new end - # Handle <tt>*_changed?</tt> for +method_missing+. + # Handles <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr, options = {}) #:nodoc: result = changes_include?(attr) result &&= options[:to] == __send__(attr) if options.key?(:to) @@ -174,7 +174,7 @@ module ActiveModel result end - # Handle <tt>*_was</tt> for +method_missing+. + # Handles <tt>*_was</tt> for +method_missing+. def attribute_was(attr) # :nodoc: attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end @@ -186,6 +186,7 @@ module ActiveModel private + # Returns +true+ if attr_name is changed, +false+ otherwise. def changes_include?(attr_name) attributes_changed_by_setter.include?(attr_name) end @@ -197,18 +198,18 @@ module ActiveModel @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end - # Clear all dirty data: current changes and previous changes. + # Clears all dirty data: current changes and previous changes. def clear_changes_information # :doc: @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end - # Handle <tt>*_change</tt> for +method_missing+. + # Handles <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+. + # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) return if attribute_changed?(attr) @@ -221,7 +222,7 @@ module ActiveModel set_attribute_was(attr, value) end - # Handle <tt>restore_*!</tt> for +method_missing+. + # Handles <tt>restore_*!</tt> for +method_missing+. def restore_attribute!(attr) if attribute_changed?(attr) __send__("#{attr}=", changed_attributes[attr]) @@ -230,7 +231,7 @@ module ActiveModel end # This is necessary because `changed_attributes` might be overridden in - # other implemntations (e.g. in `ActiveRecord`) + # other implementations (e.g. in `ActiveRecord`) alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc: # Force an attribute to have a particular "before" value diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index a809c72ccd..8334747615 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/deep_dup' +require 'active_support/deprecation' module ActiveModel # == Active \Model \Errors @@ -72,7 +73,7 @@ module ActiveModel # end def initialize(base) @base = base - @messages = {} + @messages = Hash.new { |messages, attribute| messages[attribute] = [] } @details = Hash.new { |details, attribute| details[attribute] = [] } end @@ -110,8 +111,14 @@ module ActiveModel # # person.errors.messages # => {:name=>["cannot be nil"]} # person.errors.get(:name) # => ["cannot be nil"] - # person.errors.get(:age) # => nil + # person.errors.get(:age) # => [] def get(key) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#get is deprecated and will be removed in Rails 5.1. + + To achieve the same use model.errors[:#{key}]. + MESSAGE + messages[key] end @@ -121,6 +128,12 @@ module ActiveModel # person.errors.set(:name, ["can't be nil"]) # person.errors.get(:name) # => ["can't be nil"] def set(key, value) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1. + + Use model.errors.add(:#{key}, #{value.inspect}) instead. + MESSAGE + messages[key] = value end @@ -128,7 +141,7 @@ module ActiveModel # # person.errors.get(:name) # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] - # person.errors.get(:name) # => nil + # person.errors.get(:name) # => [] def delete(key) messages.delete(key) details.delete(key) @@ -140,7 +153,7 @@ module ActiveModel # person.errors[:name] # => ["cannot be nil"] # person.errors['name'] # => ["cannot be nil"] def [](attribute) - get(attribute.to_sym) || set(attribute.to_sym, []) + messages[attribute.to_sym] end # Adds to the supplied attribute the supplied error message. @@ -148,7 +161,13 @@ module ActiveModel # person.errors[:name] = "must be set" # person.errors[:name] # => ['must be set'] def []=(attribute, error) - self[attribute] << error + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#[]= is deprecated and will be removed in Rails 5.1. + + Use model.errors.add(:#{attribute}, #{error.inspect}) instead. + MESSAGE + + messages[attribute.to_sym] << error end # Iterates through each error key, value pair in the error messages hash. @@ -167,7 +186,7 @@ module ActiveModel # end def each messages.each_key do |attribute| - self[attribute].each { |error| yield attribute, error } + messages[attribute].each { |error| yield attribute, error } end end @@ -317,8 +336,8 @@ module ActiveModel raise exception, full_message(attribute, message) end - details[attribute.to_sym] << detail - self[attribute] << message + details[attribute.to_sym] << detail + messages[attribute.to_sym] << message end # Will add an error message to each of the attributes in +attributes+ @@ -328,6 +347,14 @@ module ActiveModel # person.errors.messages # # => {:name=>["can't be empty"]} def add_on_empty(attributes, options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1 + + To achieve the same use: + + errors.add(attribute, :empty, options) if value.nil? || value.empty? + MESSAGE + Array(attributes).each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) is_empty = value.respond_to?(:empty?) ? value.empty? : false @@ -342,6 +369,14 @@ module ActiveModel # person.errors.messages # # => {:name=>["can't be blank"]} def add_on_blank(attributes, options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1 + + To achieve the same use: + + errors.add(attribute, :empty, options) if value.blank? + MESSAGE + Array(attributes).each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) add(attribute, :blank, options) if value.blank? @@ -384,7 +419,7 @@ module ActiveModel # 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) } + messages[attribute].map { |message| full_message(attribute, message) } end # Returns a full message for a given attribute. diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index b11c8f53b4..061e35dd1e 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -6,6 +6,7 @@ en: # The values :model, :attribute and :value are always available for interpolation # The value :count is available when applicable. Can be used for pluralization. messages: + model_invalid: "Validation failed: %{errors}" inclusion: "is not included in the list" exclusion: "is reserved" invalid: "is invalid" diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 2bc3eeaa19..22010b517c 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -130,7 +130,7 @@ module ActiveModel # # Equivalent to +to_s+. delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, - :to_str, to: :name + :to_str, :as_json, 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 diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 6a2668b8f7..176d4c0607 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -371,6 +371,15 @@ module ActiveModel !valid?(context) end + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, raises +ValidationError+ otherwise. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. + def validate!(context = nil) + valid?(context) || raise_validation_error + 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 @@ -395,6 +404,30 @@ module ActiveModel _run_validate_callbacks errors.empty? end + + def raise_validation_error + raise(ValidationError.new(self)) + end + end + + # = Active Model ValidationError + # + # Raised by <tt>validate!</tt> when the model is invalid. Use the + # +model+ method to retrieve the record which did not validate. + # + # begin + # complex_operation_that_internally_calls_validate! + # rescue ActiveModel::ValidationError => invalid + # puts invalid.model.errors + # end + class ValidationError < StandardError + attr_reader :model + + def initialize(model) + @model = model + errors = @model.errors.full_messages.join(", ") + super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid")) + end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 02478dd5b6..46a2e54fba 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -77,7 +77,7 @@ module ActiveModel # 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 + # 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 diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index c63a9d74b3..23201b264a 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -112,10 +112,12 @@ module ActiveModel # * <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 a method, proc or string to how to split up the attribute string. - # (e.g. <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> or <tt>tokenizer: :word_tokenizer</tt> to count words - # as in above example). Defaults to <tt>->(value) { value.split(//) }</tt> - # which counts individual characters. + # * <tt>:tokenizer</tt> - A method (as a symbol), proc or string to + # specify how to split up the attribute string. (e.g. + # <tt>tokenizer: :word_tokenizer</tt> to call the +word_tokenizer+ method + # or <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+. diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index b98585912e..1d2888a818 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -15,7 +15,7 @@ module ActiveModel # class MyValidator < ActiveModel::Validator # def validate(record) # if some_complex_logic - # record.errors[:base] = "This record is invalid" + # record.errors.add(:base, "This record is invalid") # end # end # diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 17dbb817d0..da142ea2c0 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -29,28 +29,28 @@ class ErrorsTest < ActiveModel::TestCase def test_delete errors = ActiveModel::Errors.new(self) - errors[:foo] = 'omg' + errors[:foo] << 'omg' errors.delete(:foo) assert_empty errors[:foo] end def test_include? errors = ActiveModel::Errors.new(self) - errors[:foo] = 'omg' + errors[:foo] << 'omg' assert errors.include?(:foo), 'errors should include :foo' end def test_dup errors = ActiveModel::Errors.new(self) - errors[:foo] = 'bar' + errors[:foo] << 'bar' errors_dup = errors.dup - errors_dup[:bar] = 'omg' + 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' + errors[:foo] << 'omg' assert_equal true, errors.has_key?(:foo), 'errors should have key :foo' end @@ -61,7 +61,7 @@ class ErrorsTest < ActiveModel::TestCase def test_key? errors = ActiveModel::Errors.new(self) - errors[:foo] = 'omg' + errors[:foo] << 'omg' assert_equal true, errors.key?(:foo), 'errors should have key :foo' end @@ -81,37 +81,41 @@ class ErrorsTest < ActiveModel::TestCase test "get returns the errors for the provided key" do errors = ActiveModel::Errors.new(self) - errors[:foo] = "omg" + errors[:foo] << "omg" - assert_equal ["omg"], errors.get(:foo) + assert_deprecated do + assert_equal ["omg"], errors.get(:foo) + end end test "sets the error with the provided key" do errors = ActiveModel::Errors.new(self) - errors.set(:foo, "omg") + assert_deprecated do + errors.set(:foo, "omg") + end assert_equal({ foo: "omg" }, errors.messages) end test "error access is indifferent" do errors = ActiveModel::Errors.new(self) - errors[:foo] = "omg" + 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") + errors.messages[:foo] = "omg" + errors.messages[: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") + errors.messages[:foo] << "omg" + errors.messages[:baz] << "zomg" assert_equal [:foo, :baz], errors.keys end @@ -133,7 +137,9 @@ class ErrorsTest < ActiveModel::TestCase test "assign error" do person = Person.new - person.errors[:name] = 'should not be nil' + assert_deprecated do + person.errors[:name] = 'should not be nil' + end assert_equal ["should not be nil"], person.errors[:name] end @@ -282,46 +288,60 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message" do person = Person.new person.errors.expects(:generate_message).with(:name, :empty, {}) - person.errors.add_on_empty :name + assert_deprecated do + person.errors.add_on_empty :name + 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, {}) - person.errors.add_on_empty [:name, :age] + assert_deprecated do + person.errors.add_on_empty [:name, :age] + 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' }) - person.errors.add_on_empty :name, message: 'custom' + assert_deprecated do + person.errors.add_on_empty :name, message: 'custom' + 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, {}) - person.errors.add_on_empty :name + assert_deprecated do + person.errors.add_on_empty :name + end 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 + assert_deprecated do + person.errors.add_on_blank :name + 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, {}) - person.errors.add_on_blank [:name, :age] + assert_deprecated do + person.errors.add_on_blank [:name, :age] + 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' }) - person.errors.add_on_blank :name, message: 'custom' + assert_deprecated do + person.errors.add_on_blank :name, message: 'custom' + end end test "details returns added error detail" do diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index e2eb91eeb0..d765a47636 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -195,4 +195,8 @@ class JsonSerializationTest < ActiveModel::TestCase assert_no_match %r{"awesome":}, json assert_no_match %r{"preferences":}, json end + + test "Class.model_name should be json encodable" do + assert_match %r{"Contact"}, Contact.model_name.to_json + 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 index 3eeb80a48b..da63df9152 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -62,7 +62,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase assert_equal 'custom message', @person.errors.generate_message(:title, :empty, message: 'custom message') end - # add_on_blank: generate_message(attr, :blank, message: custom_message) + # validates_presence_of: 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 diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index dc125bc884..f0317ad219 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -351,6 +351,25 @@ class ValidationsTest < ActiveModel::TestCase assert_not_empty topic.errors end + def test_validate_with_bang + Topic.validates :title, presence: true + + assert_raise(ActiveModel::ValidationError) do + Topic.new.validate! + end + end + + def test_validate_with_bang_and_context + Topic.validates :title, presence: true, on: :context + + assert_raise(ActiveModel::ValidationError) do + Topic.new.validate!(:context) + end + + t = Topic.new(title: "Valid title") + assert t.validate!(:context) + end + def test_strict_validation_in_validates Topic.validates :title, strict: true, presence: true assert_raises ActiveModel::StrictValidationFailed do |