Skip to content

Commit 70a7dcc

Browse files
authored
Merge pull request rails#43495 from DRBragg/drbragg/except_option_for_validations
Add `:except_on` option for validations
2 parents 7488910 + 372c642 commit 70a7dcc

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

activemodel/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
* Add `:except_on` option for validations. Grants the ability to _skip_ validations in specified contexts.
2+
3+
```ruby
4+
class User < ApplicationRecord
5+
#...
6+
validates :birthday, presence: { except_on: :admin }
7+
#...
8+
end
9+
10+
user = User.new(attributes except birthday)
11+
user.save(context: :admin)
12+
```
13+
14+
*Drew Bragg*
15+
116
## Rails 8.0.0.beta1 (September 26, 2024) ##
217

318
* Make `ActiveModel::Serialization#read_attribute_for_serialization` public

activemodel/lib/active_model/validations.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ module ClassMethods
6969
# or an array of symbols. (e.g. <tt>on: :create</tt> or
7070
# <tt>on: :custom_validation_context</tt> or
7171
# <tt>on: [:create, :custom_validation_context]</tt>)
72+
# * <tt>:except_on</tt> - Specifies the contexts where this validation is not active.
73+
# Runs in all validation contexts by default +nil+. You can pass a symbol
74+
# or an array of symbols. (e.g. <tt>except: :create</tt> or
75+
# <tt>except_on: :custom_validation_context</tt> or
76+
# <tt>except_on: [:create, :custom_validation_context]</tt>)
7277
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
7378
# * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
7479
# * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
@@ -84,7 +89,7 @@ def validates_each(*attr_names, &block)
8489
validates_with BlockValidator, _merge_attributes(attr_names), &block
8590
end
8691

87-
VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
92+
VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend, :except_on].freeze # :nodoc:
8893

8994
# Adds a validation method or block to the class. This is useful when
9095
# overriding the +validate+ instance method becomes too unwieldy and
@@ -135,7 +140,12 @@ def validates_each(*attr_names, &block)
135140
# or an array of symbols. (e.g. <tt>on: :create</tt> or
136141
# <tt>on: :custom_validation_context</tt> or
137142
# <tt>on: [:create, :custom_validation_context]</tt>)
138-
# * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
143+
# * <tt>:except_on</tt> - Specifies the contexts where this validation is not active.
144+
# Runs in all validation contexts by default +nil+. You can pass a symbol
145+
# or an array of symbols. (e.g. <tt>except: :create</tt> or
146+
# <tt>except_on: :custom_validation_context</tt> or
147+
# <tt>except_on: [:create, :custom_validation_context]</tt>)
148+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
139149
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
140150
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
141151
# proc or string should return or evaluate to a +true+ or +false+ value.
@@ -162,6 +172,15 @@ def validate(*args, &block)
162172
options = options.merge(if: [predicate_for_validation_context(options[:on]), *options[:if]])
163173
end
164174

175+
if options.key?(:except_on)
176+
options = options.dup
177+
options[:except_on] = Array(options[:except_on])
178+
options[:unless] = [
179+
->(o) { (options[:except_on] & Array(o.validation_context)).any? },
180+
*options[:unless]
181+
]
182+
end
183+
165184
set_callback(:validate, *args, options, &block)
166185
end
167186

activemodel/lib/active_model/validations/validates.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ module ClassMethods
7878
# or an array of symbols. (e.g. <tt>on: :create</tt> or
7979
# <tt>on: :custom_validation_context</tt> or
8080
# <tt>on: [:create, :custom_validation_context]</tt>)
81-
# * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
81+
# * <tt>:except_on</tt> - Specifies the contexts where this validation is not active.
82+
# Runs in all validation contexts by default +nil+. You can pass a symbol
83+
# or an array of symbols. (e.g. <tt>except: :create</tt> or
84+
# <tt>except_on: :custom_validation_context</tt> or
85+
# <tt>except_on: [:create, :custom_validation_context]</tt>)
86+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
8287
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
8388
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
8489
# proc or string should return or evaluate to a +true+ or +false+ value.
@@ -155,7 +160,7 @@ def validates!(*attributes)
155160
# When creating custom validators, it might be useful to be able to specify
156161
# additional default keys. This can be done by overwriting this method.
157162
def _validates_default_keys
158-
[:if, :unless, :on, :allow_blank, :allow_nil, :strict]
163+
[:if, :unless, :on, :allow_blank, :allow_nil, :strict, :except_on]
159164
end
160165

161166
def _parse_validates_options(options)

activemodel/test/cases/validations_test.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def test_invalid_options_to_validate
170170
# A common mistake -- we meant to call 'validates'
171171
Topic.validate :title, presence: true
172172
end
173-
message = "Unknown key: :presence. Valid keys are: :on, :if, :unless, :prepend. Perhaps you meant to call `validates` instead of `validate`?"
173+
message = "Unknown key: :presence. Valid keys are: :on, :if, :unless, :prepend, :except_on. Perhaps you meant to call `validates` instead of `validate`?"
174174
assert_equal message, error.message
175175
end
176176

@@ -462,4 +462,29 @@ def test_frozen_models_can_be_validated
462462
assert_predicate person, :frozen?
463463
assert_not person.valid?
464464
end
465+
466+
def test_validate_with_except_on
467+
Topic.validates :title, presence: true, except_on: :custom_context
468+
469+
topic = Topic.new
470+
topic.validate
471+
472+
assert_equal ["can't be blank"], topic.errors[:title]
473+
474+
assert topic.validate(:custom_context)
475+
end
476+
477+
def test_validations_some_with_except
478+
Topic.validates :title, presence: { except_on: :custom_context }, length: { maximum: 10 }
479+
480+
assert_raise(ActiveModel::ValidationError) do
481+
Topic.new.validate!
482+
end
483+
484+
assert_raise(ActiveModel::ValidationError) do
485+
Topic.new(title: "A" * 11).validate!(:custom_context)
486+
end
487+
488+
assert Topic.new.validate!(:custom_context)
489+
end
465490
end

0 commit comments

Comments
 (0)