Skip to content

Commit

Permalink
feat: Allow length validation on associations (#1569)
Browse files Browse the repository at this point in the history
This commit allows the length matcher to be used on associations. It
does this by checking if the attribute is an association, and if so, it
uses the associations as the attribute to validate.

This commit also test for the length matcher on associations (has_many and has_many through).

I want to give credit to @prashantjois for the initial work on this
feature. I took his work and expanded on it.

Closes #1007
  • Loading branch information
matsales28 authored Aug 18, 2023
1 parent 783a905 commit 68f76ce
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 6 deletions.
35 changes: 29 additions & 6 deletions lib/shoulda/matchers/active_model/validate_length_of_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Matchers
module ActiveModel
# The `validate_length_of` matcher tests usage of the
# `validates_length_of` matcher. Note that this matcher is intended to be
# used against string columns and not integer columns.
# used against string columns and associations and not integer columns.
#
# #### Qualifiers
#
Expand Down Expand Up @@ -36,7 +36,8 @@ module ActiveModel
#
# Use `is_at_least` to test usage of the `:minimum` option. This asserts
# that the attribute can take a string which is equal to or longer than
# the given length and cannot take a string which is shorter.
# the given length and cannot take a string which is shorter. This qualifier
# also works for associations.
#
# class User
# include ActiveModel::Model
Expand All @@ -61,7 +62,8 @@ module ActiveModel
#
# Use `is_at_most` to test usage of the `:maximum` option. This asserts
# that the attribute can take a string which is equal to or shorter than
# the given length and cannot take a string which is longer.
# the given length and cannot take a string which is longer. This qualifier
# also works for associations.
#
# class User
# include ActiveModel::Model
Expand All @@ -84,7 +86,8 @@ module ActiveModel
#
# Use `is_equal_to` to test usage of the `:is` option. This asserts that
# the attribute can take a string which is exactly equal to the given
# length and cannot take a string which is shorter or longer.
# length and cannot take a string which is shorter or longer. This qualifier
# also works for associations.
#
# class User
# include ActiveModel::Model
Expand All @@ -106,7 +109,7 @@ module ActiveModel
# ##### is_at_least + is_at_most
#
# Use `is_at_least` and `is_at_most` together to test usage of the `:in`
# option.
# option. This qualifies also works for associations.
#
# class User
# include ActiveModel::Model
Expand Down Expand Up @@ -487,13 +490,33 @@ def disallows_length_of?(length, message)
end

def value_of_length(length)
(array_column? ? ['x'] : 'x') * length
if array_column?
['x'] * length
elsif collection_association?
Array.new(length) { association_reflection.klass.new }
else
'x' * length
end
end

def array_column?
@options[:array] || super
end

def collection_association?
association? && [:has_many, :has_and_belongs_to_many].include?(
association_reflection.macro,
)
end

def association?
association_reflection.present?
end

def association_reflection
model.try(:reflect_on_association, @attribute)
end

def translated_short_message
@_translated_short_message ||=
if @short_message.is_a?(Symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,190 @@ def configure_validation_matcher(matcher)
end
end

context 'when validating has many associations' do
context 'an association with a non-zero minimum length validation' do
it 'accepts ensuring the correct minimum length' do
expect(validating_association_length(minimum: 4)).
to validate_length_of(:children).is_at_least(4)
end

it 'rejects ensuring a lower minimum length with any message' do
expect(validating_association_length(minimum: 4)).
not_to validate_length_of(:children).is_at_least(3).with_short_message(/.*/)
end

it 'rejects ensuring a higher minimum length with any message' do
expect(validating_association_length(minimum: 4)).
not_to validate_length_of(:children).is_at_least(5).with_short_message(/.*/)
end

it 'does not override the default message with a blank' do
expect(validating_association_length(minimum: 4)).
to validate_length_of(:children).is_at_least(4).with_short_message(nil)
end

it 'fails when used in the negative' do
assertion = lambda do
expect(validating_association_length(minimum: 4)).
not_to validate_length_of(:children).is_at_least(4)
end

message = <<-MESSAGE
Expected Parent not to validate that the length of :children is at least
4, but this could not be proved.
After setting :children to ‹[#<Child id: nil>, #<Child id: nil>,
#<Child id: nil>, #<Child id: nil>]›, the matcher expected the Parent
to be invalid, but it was valid instead.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end

context 'an attribute with a minimum length validation of 0' do
it 'accepts ensuring the correct minimum length' do
expect(validating_association_length(minimum: 0)).
to validate_length_of(:children).is_at_least(0)
end
end

context 'an attribute with a maximum length' do
it 'accepts ensuring the correct maximum length' do
expect(validating_association_length(maximum: 4)).
to validate_length_of(:children).is_at_most(4)
end

it 'rejects ensuring a lower maximum length with any message' do
expect(validating_association_length(maximum: 4)).
not_to validate_length_of(:children).is_at_most(3).with_long_message(/.*/)
end

it 'rejects ensuring a higher maximum length with any message' do
expect(validating_association_length(maximum: 4)).
not_to validate_length_of(:children).is_at_most(5).with_long_message(/.*/)
end

it 'does not override the default message with a blank' do
expect(validating_association_length(maximum: 4)).
to validate_length_of(:children).is_at_most(4).with_long_message(nil)
end
end

context 'an attribute with a required exact length' do
it 'accepts ensuring the correct length' do
expect(validating_association_length(is: 4)).
to validate_length_of(:children).is_equal_to(4)
end

it 'rejects ensuring a lower maximum length with any message' do
expect(validating_association_length(is: 4)).
not_to validate_length_of(:children).is_equal_to(3).with_message(/.*/)
end

it 'rejects ensuring a higher maximum length with any message' do
expect(validating_association_length(is: 4)).
not_to validate_length_of(:children).is_equal_to(5).with_message(/.*/)
end

it 'does not override the default message with a blank' do
expect(validating_association_length(is: 4)).
to validate_length_of(:children).is_equal_to(4).with_message(nil)
end
end
end

context 'when validating has many through associations' do
context 'an association with a non-zero minimum length validation' do
it 'accepts ensuring the correct minimum length' do
expect(validating_through_association_length(minimum: 4)).
to validate_length_of(:children).is_at_least(4)
end

it 'rejects ensuring a lower minimum length with any message' do
expect(validating_through_association_length(minimum: 4)).
not_to validate_length_of(:children).is_at_least(3).with_short_message(/.*/)
end

it 'rejects ensuring a higher minimum length with any message' do
expect(validating_through_association_length(minimum: 4)).
not_to validate_length_of(:children).is_at_least(5).with_short_message(/.*/)
end

it 'does not override the default message with a blank' do
expect(validating_through_association_length(minimum: 4)).
to validate_length_of(:children).is_at_least(4).with_short_message(nil)
end

it 'fails when used in the negative' do
assertion = lambda do
expect(validating_through_association_length(minimum: 4)).
not_to validate_length_of(:children).is_at_least(4)
end

message = <<-MESSAGE
Expected Parent not to validate that the length of :children is at least
4, but this could not be proved.
After setting :children to ‹[#<Child id: nil>, #<Child id: nil>,
#<Child id: nil>, #<Child id: nil>]›, the matcher expected the Parent
to be invalid, but it was valid instead.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end

context 'an attribute with a minimum length validation of 0' do
it 'accepts ensuring the correct minimum length' do
expect(validating_through_association_length(minimum: 0)).
to validate_length_of(:children).is_at_least(0)
end
end

context 'an attribute with a maximum length' do
it 'accepts ensuring the correct maximum length' do
expect(validating_through_association_length(maximum: 4)).
to validate_length_of(:children).is_at_most(4)
end

it 'rejects ensuring a lower maximum length with any message' do
expect(validating_through_association_length(maximum: 4)).
not_to validate_length_of(:children).is_at_most(3).with_long_message(/.*/)
end

it 'rejects ensuring a higher maximum length with any message' do
expect(validating_through_association_length(maximum: 4)).
not_to validate_length_of(:children).is_at_most(5).with_long_message(/.*/)
end

it 'does not override the default message with a blank' do
expect(validating_through_association_length(maximum: 4)).
to validate_length_of(:children).is_at_most(4).with_long_message(nil)
end
end

context 'an attribute with a required exact length' do
it 'accepts ensuring the correct length' do
expect(validating_through_association_length(is: 4)).
to validate_length_of(:children).is_equal_to(4)
end

it 'rejects ensuring a lower maximum length with any message' do
expect(validating_through_association_length(is: 4)).
not_to validate_length_of(:children).is_equal_to(3).with_message(/.*/)
end

it 'rejects ensuring a higher maximum length with any message' do
expect(validating_through_association_length(is: 4)).
not_to validate_length_of(:children).is_equal_to(5).with_message(/.*/)
end

it 'does not override the default message with a blank' do
expect(validating_through_association_length(is: 4)).
to validate_length_of(:children).is_equal_to(4).with_message(nil)
end
end
end

if database_supports_array_columns?
context 'when the column backing the attribute is an array' do
context 'an attribute with a non-zero minimum length validation' do
Expand Down Expand Up @@ -665,6 +849,27 @@ def define_active_model_validating_length(options)
end.new
end

def validating_association_length(options)
define_model :child
define_model :parent do
has_many :children
validates_length_of :children, options
end.new
end

def validating_through_association_length(options)
define_model :child
define_model :conception, child_id: :integer, parent_id: :integer do
belongs_to :child
end
define_model :parent do
has_many :conceptions
has_many :children, through: :conceptions

validates_length_of :children, options
end.new
end

def validating_array_length(options = {})
define_model_validating_length(options.merge(array: true)).new
end
Expand Down

0 comments on commit 68f76ce

Please sign in to comment.