Problem with conditional validations
sergiopatricio opened this issue · 12 comments
I have the following model:
class Document < ApplicationRecord
has_one_attached :asset
validates :asset, attached: true
validates :asset, size: { less_than: 20.megabytes, message: 'must be smaller than 20MB' }, if: :condition1?
validates :asset, size: { less_than: 10.megabytes, message: 'must be smaller than 10MB' }, if: :condition2?
end
When I run this test:
context 'when condition2' do
subject(:document) { Document.new(...set condition2...) }
it { is_expected.to validate_size_of(:asset).less_than(10.megabytes) }
end
the test fails.
I noticed that if I remove message
from the validator options the test passes.
This problem started happening when updating to >= 1.1.0
.
I did some debugging and the problem seems to be in https://github.com/igorkasyanchuk/active_storage_validations/blob/master/lib/active_storage_validations/matchers/concerns/validatable.rb, with this code:
@subject.class.validators_on(@attribute_name).find do |validator|
validator.class == validator_class
end
This will use the first SizeValidator
that it finds, but it may not be the correct one.
@subject.class.validators_on(@attribute_name)
=> [#<ActiveStorageValidations::AttachedValidator:0x000000010efb5388 @attributes=[:asset], @options={}>,
#<ActiveStorageValidations::SizeValidator:0x000000010ef7f4b8 @attributes=[:asset], @options={:if=>:condition1?, :less_than=>20971520, :message=>"must be smaller than 20MB"}>,
#<ActiveStorageValidations::SizeValidator:0x000000010ef7eba8 @attributes=[:asset], @options={:if=>:condition2?, :less_than=>10485760, :message=>"must be smaller than 10MB"}>]
It finds the SizeValidator
for 20MB
, but I'm testing the validator for "10MB".
Hi @sergiopatricio,
Thanks for raising the issue and the debugging, we are apparently missing an extra step to identify the validation that raises the error.
A good fix would be to add another condition in the Validatable
concern method that you mentioned, would you mind creating a PR with the fix + its associated tests? I would encourage the use of a shared_example for the Validatable
concern that we could use on all validators during the test phase.
Or I can dedicate some time to solve it in the coming days, as you prefer
Thanks @Mth0158, I can try although I need to investigate a bit more how I can create a test for this.
Regarding the solution, I do not see how I could select the exact validator, I suppose it may not be possible and that's why we also check all errors in validator_class::ERROR_TYPES
.
So maybe we can do something similar with the custom messages:
diff --git a/lib/active_storage_validations/matchers/concerns/validatable.rb b/lib/active_storage_validations/matchers/concerns/validatable.rb
index b3b0fa2..ac38c6d 100644
--- a/lib/active_storage_validations/matchers/concerns/validatable.rb
+++ b/lib/active_storage_validations/matchers/concerns/validatable.rb
@@ -26,7 +26,7 @@ module ActiveStorageValidations
def available_errors
[
*validator_class::ERROR_TYPES,
- *error_from_custom_message
+ *errors_from_custom_messages
].compact
end
@@ -34,14 +34,14 @@ module ActiveStorageValidations
self.class.name.gsub(/::Matchers|Matcher/, '').constantize
end
- def attribute_validator
- @subject.class.validators_on(@attribute_name).find do |validator|
+ def attribute_validators
+ @subject.class.validators_on(@attribute_name).select do |validator|
validator.class == validator_class
end
end
- def error_from_custom_message
- attribute_validator.options[:message]
+ def errors_from_custom_messages
+ attribute_validators.map { |validation| validation.options[:message] }
end
end
end
But not sure if I'm breaking anything related to attribute_validator
.
At first glance I think you solved the problem.
The issue was that attribute_validator
was only returning the first size/dimension/etc (depending on context) validator it finds for the matcher attribute in the ActiveModel::Errors
object. Therefore in your case it does not work properly because you have 2 size errors for the same attribute.
Your code will now get all the size/dimension/etc (depending on context) validators in error for the matcher attribute, and then compare it with all possible values (base case (ie gem custom message), all the custom messages added on the different size/dimension/etc (depending on context) validators).
However attribute_validator
is also used in the Contextable
concern to test the Rails :on validation option, so you would have to update this part too, it's a bit more work :s
@sergiopatricio forgot to tag you to get you notified :)
@Mth0158 my doubt is if the attribute_validator
I renamed is used in https://github.com/igorkasyanchuk/active_storage_validations/blob/master/lib/active_storage_validations/matchers/concerns/contextable.rb which would brake it. (And also means that Contextable
may suffer from the same bug I reported for Validatable
)
Yes @sergiopatricio you are absolutely right, its a test case that is not handled properly for by these 2 concerns 😕
How do you want to operate to solve this issue?
I do not see how we can select the exact validator.
I could fix my case with something like this:
diff --git a/lib/active_storage_validations/matchers/concerns/validatable.rb b/lib/active_storage_validations/matchers/concerns/validatable.rb
index b3b0fa2..842c600 100644
--- a/lib/active_storage_validations/matchers/concerns/validatable.rb
+++ b/lib/active_storage_validations/matchers/concerns/validatable.rb
@@ -26,7 +26,7 @@ module ActiveStorageValidations
def available_errors
[
*validator_class::ERROR_TYPES,
- *error_from_custom_message
+ *errors_from_custom_messages
].compact
end
@@ -34,14 +34,18 @@ module ActiveStorageValidations
self.class.name.gsub(/::Matchers|Matcher/, '').constantize
end
+ # TODO: this may return the wrong validator when we have multiple validations based on conditions (if, on, etc.)
def attribute_validator
@subject.class.validators_on(@attribute_name).find do |validator|
validator.class == validator_class
end
end
- def error_from_custom_message
- attribute_validator.options[:message]
+ def errors_from_custom_messages
+ validators = @subject.class.validators_on(@attribute_name).select do |validator|
+ validator.class == validator_class
+ end
+ validators.map { |validation| validation.options[:message] }
end
end
end
But Contextable
would still have the bug.
I need to dig a bit more into the gem to see if I can get some ideas.
@sergiopatricio If think a little of change in the Contextable
concern could do it. If we change the is_context_valid?
method internals to compare to attribute_validators.map{ |attribute_validator| attribute_validator.options[:on]}.uniq
with the idea that @context
should be strictly included in it.
In the previous code, uniq
should be replaced by something that checks uniqueness regardless of type to handle :custom and 'custom' being interpreted as 2 different contexts.
@sergiopatricio any news on this issue? Tell me if you would like some help on it, I'll gladly help!
@Mth0158 sorry, I will not have time to look at this for the next few days.
Hi @sergiopatricio,
The fix has been merged, it'll be available in the next release ;)
@Mth0158 thank you!