-
-
Notifications
You must be signed in to change notification settings - Fork 909
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implement comparison of matcher #1552
feat: Implement comparison of matcher #1552
Conversation
The comparison matcher will be used now on both ValidateNumericalityOfMatcher and on the new ValidateComparisonOfMatcher, that's why we removed it from the NumericalityMatcher module.
We needed to make sure the ComparisonMatcher will be working for both ValidateComparisonOfMatcher and ValidateNumericalityOfMatcher, some changes were required to make it work properly. We also changed the order of the submatchers, where the `DissalowMatcher` of the `ValidateNumericalityOfMatcher` will always be evaluated first, because of that change it was required some minor changes in the specs.
This commit intends to instead of getting the first submatcher failure_message_when_negated message, to get from the DisallowMatcher first because the messages are more easy to read and to debug.
This commit implements the base of the validate_comparion_of_matcher. Creating the `ValidateComparisonOfMatcher` class and specs for the submatchers `is_greater_than`, `is_greater_than_or_equal_to` and `is_less_than`. In the next commits we'll implement the specs for the other submatchers and for combinations of submatchers. We are also missing the specs and implementation for comparing with other values than just pure integers. We need to implement an way to compare with Procs and also symbols.
In this commit we added the support for making validations using the `validate_comparison_of` matcher for string values. Ruby compares strings using their ASCII/Unicode value, so for example: ```ruby 'aaa' > 'bbb' # false 'bbb' > 'aaa' # true ``` In this example `'aaa'` gets converted to an array with the unicode values for the letters ```ruby 'aaa' #=> [97, 97, 97] ``` Ruby compares two strings character by character based on their Unicode code point values. When comparing two strings, it compares the corresponding characters at each position until it finds a difference. If the characters at the current position are the same, it moves on to the next position and continues the comparison. If the characters at the current position are different, it compares their Unicode code point values to determine which character is greater. The string with the greater character at the first differing position is considered greater than the other string.
In this commit we added the support for making validations using the `validate_comparison_of` matcher for date values.
This commit inteds to implement the `allow_nil`, `strict` and `with_message` qualifiers on top of ComparisonMatcher. These qualifiers were copy/pasted from the NumericalityMatcher on previous commits and some of them required some minor adjustments to work properly.
09d8beb
to
266ed7b
Compare
def not_failing_submatcher | ||
not_failing_submatchers.detect { |submatcher| | ||
submatcher.instance_of?(DisallowValueMatcher) | ||
} || not_failing_submatchers.first | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was something that bothered me for a while when creating the first specs for the validate_comparison_of
matcher, sometimes the failure message was very hard to understand because it was using AllowValueMatcher
failure messages, the DisallowValueMatcher
ones are easy to understand when dealing with matcher that didn't fail but was supposed to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... I'm wondering if perhaps we should leave it to the developer to add matchers in the order that would lead to a better user experience instead of trying to "fix" it for them. I can see this behavior being somewhat confusing as well. Is there another approach we could take here or what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is tricky because the developer can't define the order of the submatchers. After all, in this specific case, when the user defines one comparison matcher, we define three submatchers for that matcher, and that order is defined by the shoulda-matchers
code.
Generates
validates_comparison_of :participant_count, greater_than_or_equal_to: 2
# Generates in shoulda-matchers
# ComparisonMatcher with the following submatchers
# DisallowValueMatcher with value 1
# AllowValueMatcher with value 2
# AllowValue matcher with value 2
I totally get what you're saying about this behavior being confusing. Honestly, I'm fresh out of ideas here. I'm also fine reverting that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reverted that change on 5906112
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah sorry for not replying, I'm slammed with work currently 😵💫 Thank you! If this indeed continues to be confusing, I'm all for revisiting this. It'll also be easier to see the consequences of this change separately.
@@ -377,6 +377,8 @@ def initialize(attribute) | |||
@allowed_type_name = 'number' | |||
@context = nil | |||
@expected_message = nil | |||
|
|||
add_disallow_non_numeric_value_matcher |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed that behavior to the initializer because by doing this, we can ensure that the first matcher to be checked is always the one to disallow non-numerical values. With that, we can display more intuitive failure messages.
spec/unit/shoulda/matchers/active_model/validate_comparison_of_matcher_spec.rb
Outdated
Show resolved
Hide resolved
12c311d
to
23c76b3
Compare
23c76b3
to
9428461
Compare
@mcmire @vsppedro Just wanted to check in on this PR - it's been sitting there for a little while now. I understand that you folks may be busy with other things, so there's no rush on my end. However, if you could take a look and let me know your thoughts when you get the chance, I'd really appreciate it. I'd love to hear if there's anything I can improve. Thanks for all your hard work on this project! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Sorry for taking so long.
Nicely done! Would love to see this get merged in. |
I think both options are good, but it's always helpful to hear what @mcmire thinks because he really knows this project inside out and has way more experience. |
I'll try to prioritize this tomorrow! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @matsales28, I'm so sorry it took me a while to review this. Overall this looks like great work — I just had some comments.
lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb
Outdated
Show resolved
Hide resolved
def not_failing_submatcher | ||
not_failing_submatchers.detect { |submatcher| | ||
submatcher.instance_of?(DisallowValueMatcher) | ||
} || not_failing_submatchers.first | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... I'm wondering if perhaps we should leave it to the developer to add matchers in the order that would lead to a better user experience instead of trying to "fix" it for them. I can see this behavior being somewhat confusing as well. Is there another approach we could take here or what do you think?
16df8f1
to
0737f1c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me. Again sorry it took me a while to get to this! Thank you for your hard work on this.
I tried this out with a database
It looks like 1 day is being added, but for a |
Hi @michaelgpearce, thanks for reporting that! I just got back from PTO, I'll take a look at that as soon as possible! |
Hey @michaelgpearce, I've thoroughly investigated the issue and discovered that the problem occurs not only with past dates but all dates when using a time column in Rails. The behaviour you're observing is caused by the way Rails' typecast of dates to time handles comparisons. Rails convert any date assigned to a time column to the (default timezone)[https://guides.rubyonrails.org/configuring.html#config-active-record-default-timezone] and sets the date as 2000-01-01. This means that any validation comparison expecting a time value other than 2000-01-01 will fail because Rails compares the entire Date object, not only the hours, minutes and seconds. For example: class Example
validates :start_time, comparison: { other_than: Time.zone.local(2021, 1, 1) }
end
example = Example.new(start_time: Time.zone.local(2000, 1, 1)) # We are not specifying the time, so the time should be the same as declared in the comparison
example.start_time #=> "2000-01-01 00:00:00.000000000 +0000"
example.valid? #=> true
# Rails will make the following comparison
example.start_time != Time.zone.local(2000, 1, 1) #=> true This example demonstrates that Rails performs the comparison using the Time object, which, in this case, will be different due to the year and not the time. It's important to note that this behaviour is not limited to past dates but affects all dates when using a time column. Another issue arises when using For example: class Example
validates :start_time, comparison: { less_than: Time.zone.local(2020, 1, 1).end_of_day }
end
# We cannot ensure that this validation will work using `shoulda-matchers` because we cannot test that a value greater than the end of the day will be invalid.
# Adding one second to the comparison value causes it to fall into the next day (2020-01-02). However, when we assign that value to `start_time`, Rails typecasts it to the beginning of the day (2000-01-01). Due to these behaviours, validating time boundaries precisely when using a time column and Please let me know if you have further questions or need additional assistance! @mcmire do you think that makes sense? Some references: |
@matsales28 @vsppedro Hi! Awesome work on this PR. 👏 Do you know when this will be released? Not sure if you're waiting for some other things to be wrapped up before releasing a 5.4.0. |
Hey @jeffdill2, we're planning to cut a release soon. We'd like to include support for Ruby 3.3 on that release, so I'd say until the end of the year, we'll launch a new release. In the meantime, I've created a branch that includes this feature, you can use it if you want to. gem 'shoulda-matchers', github: 'thoughtbot/shoulda-matchers', branch: 'with-comparison-matcher' |
Awesome, thanks @matsales28! 😄 |
Closes: #1514
Why:
In Rails, 7 was added a new option to perform validations: the ComparisonValidator with it you make a comparison between any object.
Before Rails 7
After Rails 7
Those examples were extracted from this blogpost.
How
On shoulda, we used to have a class called
lib/shoulda/matchers/active_model/comparison_matcher.rb
. That class was used to implement logic to make comparisons for the numericality matcher. In this PR we are making that class generic more generic to be used on bothvalidate_numericality_of_matcher.rb
andvalidate_comparison_of_matcher.rb
files.Compatibility changes
The first two commits of this PR are making adjustments to that and other classes to make it easy to use on the new
ValidateComparisoOfMatcher
class.Compatibility changes to Submatchers and ComparisonMatcher
We needed to make sure the ComparisonMatcher would be working for both
ValidateComparisonOfMatcher and ValidateNumericalityOfMatcher, some
changes were required to make it work properly.
We also changed the order of the submatchers, where the
DissalowMatcher
of the
ValidateNumericalityOfMatcher
will always be evaluated first,because of that change, it was required some minor changes in the specs.
fix: Get failure_message_when_negated from DisalllowMatcher
This commit intends to, instead of getting the first submatcher
failure_message_when_negated message to get from the DisallowMatcher
first, because the messages are easier to read and debug.
Implementation of the new Matcher and specs
feat: Implement ValidateComparisonOfMatcher class
This commit implements the base of the validate_comparion_of_matcher.
Creating the
ValidateComparisonOfMatcher
class and specs for thesubmatchers
is_greater_than
,is_greater_than_or_equal_to
andis_less_than
. In the next commits we'll implement the specs for theother submatchers and for combinations of submatchers.
We are also missing the specs and implementation for comparing with
other values than just pure integers. We need to implement an way to
compare with Procs and also symbols.
feat: Implement support for comparing between string values
In this commit we added the support for making validations using the
validate_comparison_of
matcher for string values. Ruby comparesstrings using their ASCII/Unicode value, so for example:
In this example
'aaa'
gets converted to an array with the unicodevalues for the letters
Ruby compares two strings character by character based on their Unicode
code point values. When comparing two strings, it compares the
corresponding characters at each position until it finds a difference.
If the characters at the current position are the same, it moves on to
the next position and continues the comparison. If the characters at
the current position are different, it compares their Unicode code
point values to determine which character is greater. The string with
the greater character at the first differing position is considered
greater than the other string.
feat: Implement support for comparing between date values
In this commit we added the support for making validations using the
validate_comparison_of
matcher for date values.feat: Implement allow_nil, strict and with_message qualifiers
This commit inteds to implement the
allow_nil
,strict
andwith_message
qualifiers on top of ComparisonMatcher.These qualifiers were copy/pasted from the NumericalityMatcher on
previous commits and some of them required some minor adjustments to
work properly.
Disclaimer
I'm sorry for the super long PR but I wanted to try to cover all the cases with specs.