shoulda-matchers 5.3.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +26 -9
  4. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +7 -9
  5. data/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +13 -15
  6. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +10 -1
  7. data/lib/shoulda/matchers/active_model/comparison_matcher.rb +157 -0
  8. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +7 -0
  9. data/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +1 -1
  10. data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +16 -6
  11. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +0 -6
  12. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +532 -0
  13. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +3 -3
  14. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +4 -3
  15. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +64 -9
  16. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +40 -96
  17. data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +6 -7
  18. data/lib/shoulda/matchers/active_model/validation_matcher.rb +6 -0
  19. data/lib/shoulda/matchers/active_model/validator.rb +4 -0
  20. data/lib/shoulda/matchers/active_model.rb +2 -1
  21. data/lib/shoulda/matchers/active_record/association_matcher.rb +31 -11
  22. data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +23 -19
  23. data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +27 -23
  24. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +0 -8
  25. data/lib/shoulda/matchers/active_record/encrypt_matcher.rb +174 -0
  26. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
  27. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +24 -13
  28. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
  29. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +1 -1
  30. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  31. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +82 -70
  32. data/lib/shoulda/matchers/active_record.rb +2 -0
  33. data/lib/shoulda/matchers/doublespeak/double_collection.rb +2 -6
  34. data/lib/shoulda/matchers/doublespeak/world.rb +2 -6
  35. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +13 -15
  36. data/lib/shoulda/matchers/rails_shim.rb +8 -6
  37. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  38. data/lib/shoulda/matchers/util.rb +17 -19
  39. data/lib/shoulda/matchers/version.rb +1 -1
  40. data/lib/shoulda/matchers.rb +2 -2
  41. data/shoulda-matchers.gemspec +1 -1
  42. metadata +11 -8
  43. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -136
@@ -0,0 +1,532 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveModel
4
+ # The `validate_comparison_of` matcher tests usage of the
5
+ # `validates_comparison_of` validation.
6
+ #
7
+ # class Person
8
+ # include ActiveModel::Model
9
+ # attr_accessor :gpa
10
+ #
11
+ # validates_comparison_of :gpa, greater_than: 10
12
+ # end
13
+ #
14
+ # # RSpec
15
+ # RSpec.describe Person, type: :model do
16
+ # it { should validate_comparison_of(:gpa).greater_than(10) }
17
+ # end
18
+ #
19
+ # # Minitest (Shoulda)
20
+ # class PersonTest < ActiveSupport::TestCase
21
+ # should validate_comparison_of(:gpa).greater_than(10)
22
+ # end
23
+ #
24
+ # #### Qualifiers
25
+ #
26
+ # ##### on
27
+ #
28
+ # Use `on` if your validation applies only under a certain context.
29
+ #
30
+ # class Person
31
+ # include ActiveModel::Model
32
+ # attribute :number_of_dependents, :integer
33
+ # attr_accessor :number_of_dependents
34
+ #
35
+ # validates_comparison_of :number_of_dependents, on: :create, greater_than: 0
36
+ # end
37
+ #
38
+ # # RSpec
39
+ # RSpec.describe Person, type: :model do
40
+ # it do
41
+ # should validate_comparison_of(:number_of_dependents).
42
+ # greater_than(0).
43
+ # on(:create)
44
+ # end
45
+ # end
46
+ #
47
+ # # Minitest (Shoulda)
48
+ # class PersonTest < ActiveSupport::TestCase
49
+ # should validate_comparison_of(:number_of_dependents).greater_than(0).on(:create)
50
+ # end
51
+ #
52
+ # ##### is_less_than
53
+ #
54
+ # Use `is_less_than` to test usage of the the `:less_than` option. This
55
+ # asserts that the attribute can take a value which is less than the
56
+ # given value and cannot take a value which is greater than or equal to
57
+ # it. It can also accept methods or procs that returns a given value.
58
+ #
59
+ # class Person
60
+ # include ActiveModel::Model
61
+ # attribute :number_of_cars, :integer
62
+ # attr_accessor :number_of_cars
63
+ #
64
+ # validates_comparison_of :number_of_cars, less_than: :current_number_of_cars
65
+ #
66
+ # def current_number_of_cars
67
+ # 10
68
+ # end
69
+ # end
70
+ #
71
+ # # RSpec
72
+ # RSpec.describe Person, type: :model do
73
+ # it do
74
+ # should validate_comparison_of(:number_of_cars).
75
+ # is_less_than(:current_number_of_cars)
76
+ # end
77
+ # end
78
+ #
79
+ # # Minitest (Shoulda)
80
+ # class PersonTest < ActiveSupport::TestCase
81
+ # should validate_comparison_of(:number_of_cars).
82
+ # is_less_than(:current_number_of_cars)
83
+ # end
84
+ #
85
+ # ##### is_less_than_or_equal_to
86
+ #
87
+ # Use `is_less_than_or_equal_to` to test usage of the
88
+ # `:less_than_or_equal_to` option. This asserts that the attribute can
89
+ # take a value which is less than or equal to the given value and cannot
90
+ # take a value which is greater than it. It can also accept methods or
91
+ # procs that returns a given value.
92
+ #
93
+ # class Person
94
+ # include ActiveModel::Model
95
+ # attr_accessor :birth_date
96
+ #
97
+ # validates_comparison_of :birth_date, less_than_or_equal_to: Date.new(1987, 12, 31)
98
+ # end
99
+ #
100
+ # # RSpec
101
+ # RSpec.describe Person, type: :model do
102
+ # it do
103
+ # should validate_comparison_of(:birth_date).
104
+ # is_less_than_or_equal_to(Date.new(1987, 12, 31))
105
+ # end
106
+ # end
107
+ #
108
+ # # Minitest (Shoulda)
109
+ # class PersonTest < ActiveSupport::TestCase
110
+ # should validate_comparison_of(:birth_date).
111
+ # is_less_than_or_equal_to(Date.new(1987, 12, 31))
112
+ # end
113
+ #
114
+ # ##### is_greater_than_or_equal_to
115
+ #
116
+ # Use `is_greater_than_or_equal_to` to test usage of the
117
+ # `:greater_than_or_equal_to` option. This asserts that the attribute can
118
+ # take a value which is greater than or equal to the given value and
119
+ # cannot take a value which is less than it.
120
+ #
121
+ # class Person
122
+ # include ActiveModel::Model
123
+ # attribute :birth_date, :date
124
+ # attr_accessor :birth_date
125
+ #
126
+ # validates_comparison_of :birth_date,
127
+ # greater_than_or_equal_to: -> { 18.years.ago.to_date }
128
+ # end
129
+ #
130
+ # # RSpec
131
+ # RSpec.describe Person, type: :model do
132
+ # it do
133
+ # should validate_comparison_of(:birth_date).
134
+ # is_greater_than_or_equal_to(-> { 18.years.ago.to_date })
135
+ # end
136
+ # end
137
+ #
138
+ # # Minitest (Shoulda)
139
+ # class PersonTest < ActiveSupport::TestCase
140
+ # should validate_comparison_of(:birth_date).
141
+ # is_greater_than_or_equal_to(-> { 18.years.ago.to_date })
142
+ # end
143
+ #
144
+ # ##### is_greater_than
145
+ #
146
+ # Use `is_greater_than` to test usage of the `:greater_than` option.
147
+ # This asserts that the attribute can take a value which is greater than
148
+ # the given value and cannot take a value less than or equal to it.
149
+ # It can also accept methods or procs that returns a given value.
150
+ #
151
+ # class Person
152
+ # include ActiveModel::Model
153
+ # attribute :legal_age, :integer
154
+ # attr_accessor :legal_age
155
+ #
156
+ # validates_comparison_of :legal_age, greater_than: 21
157
+ # end
158
+ #
159
+ # # RSpec
160
+ # RSpec.describe Person, type: :model do
161
+ # it do
162
+ # should validate_comparison_of(:legal_age).
163
+ # is_greater_than(21)
164
+ # end
165
+ # end
166
+ #
167
+ # # Minitest (Shoulda)
168
+ # class PersonTest < ActiveSupport::TestCase
169
+ # should validate_comparison_of(:legal_age).
170
+ # is_greater_than(21)
171
+ # end
172
+ #
173
+ # ##### is_equal_to
174
+ #
175
+ # Use `is_equal_to` to test usage of the `:equal_to` option. This asserts
176
+ # that the attribute can take a value which is equal to the given value
177
+ # and cannot take a value which is not equal. It can also accept methods or
178
+ # procs that returns a given value.
179
+ #
180
+ # class Person
181
+ # include ActiveModel::Model
182
+ # attribute :favorite_color, :string
183
+ # attr_accessor :favorite_color
184
+ #
185
+ # validates_comparison_of :favorite_color, equal_to: "blue"
186
+ # end
187
+ #
188
+ # # RSpec
189
+ # RSpec.describe Person, type: :model do
190
+ # it { should validate_comparison_of(:favorite_color).is_equal_to("blue") }
191
+ # end
192
+ #
193
+ # # Minitest (Shoulda)
194
+ # class PersonTest < ActiveSupport::TestCase
195
+ # should validate_comparison_of(:favorite_color).is_equal_to("blue")
196
+ # end
197
+ #
198
+ #
199
+ # ##### is_other_than
200
+ #
201
+ # Use `is_other_than` to test usage of the `:other_than` option.
202
+ # This asserts that the attribute can take a number which is not equal to
203
+ # the given value.
204
+ #
205
+ # class Person
206
+ # include ActiveModel::Model
207
+ # attr_accessor :legal_age
208
+ #
209
+ # validates_comparison_of :legal_age, other_than: 21
210
+ # end
211
+ #
212
+ # # RSpec
213
+ # RSpec.describe Person, type: :model do
214
+ # it do
215
+ # should validate_comparison_of(:legal_age).
216
+ # is_other_than(21)
217
+ # end
218
+ # end
219
+ #
220
+ # # Minitest (Shoulda)
221
+ # class PersonTest < ActiveSupport::TestCase
222
+ # should validate_comparison_of(:legal_age).
223
+ # is_other_than(21)
224
+ # end
225
+ #
226
+ # ##### with_message
227
+ #
228
+ # Use `with_message` if you are using a custom validation message.
229
+ #
230
+ # class Person
231
+ # include ActiveModel::Model
232
+ # attr_accessor :number_of_dependents
233
+ #
234
+ # validates_comparison_of :number_of_dependents, greater_than: 0
235
+ # message: 'Number of dependents must be a number'
236
+ # end
237
+ #
238
+ # # RSpec
239
+ # RSpec.describe Person, type: :model do
240
+ # it do
241
+ # should validate_comparison_of(:number_of_dependents).
242
+ # is_greater_than(0).
243
+ # with_message('Number of dependents must be a number')
244
+ # end
245
+ # end
246
+ #
247
+ # # Minitest (Shoulda)
248
+ # class PersonTest < ActiveSupport::TestCase
249
+ # should validate_comparison_of(:number_of_dependents).
250
+ # is_greater_than(0).
251
+ # with_message('Number of dependents must be a number')
252
+ # end
253
+ #
254
+ # ##### allow_nil
255
+ #
256
+ # Use `allow_nil` to assert that the attribute allows nil.
257
+ #
258
+ # class Post
259
+ # include ActiveModel::Model
260
+ # attr_accessor :age
261
+ #
262
+ # validates_comparison_of :age, greater_than: 0, allow_nil: true
263
+ # end
264
+ #
265
+ # # RSpec
266
+ # RSpec.describe Post, type: :model do
267
+ # it { should validate_comparison_of(:age).is_greater_than(0).allow_nil }
268
+ # end
269
+ #
270
+ # # Minitest (Shoulda)
271
+ # class PostTest < ActiveSupport::TestCase
272
+ # should validate_comparison_of(:age).is_greater_than(0).allow_nil
273
+ # end
274
+ #
275
+ # @return [ValidateComparisonOfMatcher]
276
+ #
277
+ def validate_comparison_of(attr)
278
+ ValidateComparisonOfMatcher.new(attr)
279
+ end
280
+
281
+ # @private
282
+ class ValidateComparisonOfMatcher < ValidationMatcher
283
+ NUMERIC_NAME = 'number'.freeze
284
+ DEFAULT_DIFF_TO_COMPARE = 1
285
+
286
+ attr_reader :diff_to_compare, :number_of_submatchers
287
+
288
+ def initialize(attribute)
289
+ super
290
+ @submatchers = []
291
+ @diff_to_compare = DEFAULT_DIFF_TO_COMPARE
292
+ @expects_to_allow_nil = false
293
+ @comparison_submatcher = false
294
+ end
295
+
296
+ def allow_nil
297
+ @expects_to_allow_nil = true
298
+ prepare_submatcher(allow_value_matcher(nil))
299
+ self
300
+ end
301
+
302
+ def expects_to_allow_nil?
303
+ @expects_to_allow_nil
304
+ end
305
+
306
+ def is_greater_than(value)
307
+ prepare_submatcher(comparison_matcher_for(value, :>).for(attribute))
308
+ self
309
+ end
310
+
311
+ def is_greater_than_or_equal_to(value)
312
+ prepare_submatcher(comparison_matcher_for(value, :>=).for(attribute))
313
+ self
314
+ end
315
+
316
+ def is_equal_to(value)
317
+ prepare_submatcher(comparison_matcher_for(value, :==).for(attribute))
318
+ self
319
+ end
320
+
321
+ def is_less_than(value)
322
+ prepare_submatcher(comparison_matcher_for(value, :<).for(attribute))
323
+ self
324
+ end
325
+
326
+ def is_less_than_or_equal_to(value)
327
+ prepare_submatcher(comparison_matcher_for(value, :<=).for(attribute))
328
+ self
329
+ end
330
+
331
+ def is_other_than(value)
332
+ prepare_submatcher(comparison_matcher_for(value, :!=).for(attribute))
333
+ self
334
+ end
335
+
336
+ def matches?(subject)
337
+ @subject = subject
338
+ @number_of_submatchers = @submatchers.size
339
+ unless @comparison_matcher
340
+ raise(ArgumentError, "matcher isn't qualified with any comparison matcher")
341
+ end
342
+
343
+ qualify_submatchers
344
+ first_submatcher_that_fails_to_match.nil?
345
+ end
346
+
347
+ def does_not_match?(subject)
348
+ @subject = subject
349
+ @number_of_submatchers = @submatchers.size
350
+
351
+ qualify_submatchers
352
+ first_submatcher_that_fails_to_not_match.nil?
353
+ end
354
+
355
+ def simple_description
356
+ String.new.tap do |description|
357
+ description << "validate that :#{attribute} looks like "
358
+ description << Shoulda::Matchers::Util.a_or_an(allowed_type_name)
359
+
360
+ if comparison_descriptions.present?
361
+ description << " #{comparison_descriptions}"
362
+ end
363
+ end
364
+ end
365
+
366
+ def failure_message
367
+ overall_failure_message.dup.tap do |message|
368
+ message << "\n"
369
+ message << failure_message_for_first_submatcher_that_fails_to_match
370
+ end
371
+ end
372
+
373
+ def failure_message_when_negated
374
+ overall_failure_message_when_negated.dup.tap do |message|
375
+ message << "\n"
376
+ message <<
377
+ failure_message_for_first_submatcher_that_fails_to_not_match
378
+ end
379
+ end
380
+
381
+ def given_numeric_column?
382
+ attribute_is_active_record_column? &&
383
+ [:integer, :float, :decimal].include?(column_type)
384
+ end
385
+
386
+ private
387
+
388
+ def attribute_is_active_record_column?
389
+ columns_hash.key?(attribute.to_s)
390
+ end
391
+
392
+ def column_type
393
+ columns_hash[attribute.to_s].type
394
+ end
395
+
396
+ def columns_hash
397
+ if subject.class.respond_to?(:columns_hash)
398
+ subject.class.columns_hash
399
+ else
400
+ {}
401
+ end
402
+ end
403
+
404
+ def prepare_submatcher(submatcher)
405
+ add_submatcher(submatcher)
406
+ submatcher
407
+ end
408
+
409
+ def comparison_matcher_for(value, operator)
410
+ @comparison_matcher = true
411
+ ComparisonMatcher.
412
+ new(self, value, operator).
413
+ for(attribute)
414
+ end
415
+
416
+ def add_submatcher(submatcher)
417
+ @submatchers << submatcher
418
+ end
419
+
420
+ def qualify_submatchers
421
+ @submatchers.each do |submatcher|
422
+ if @expects_strict
423
+ submatcher.strict
424
+ end
425
+
426
+ if @expected_message.present?
427
+ submatcher.with_message(@expected_message)
428
+ end
429
+
430
+ if @context
431
+ submatcher.on(@context)
432
+ end
433
+
434
+ submatcher.ignoring_interference_by_writer(
435
+ ignore_interference_by_writer,
436
+ )
437
+ end
438
+ end
439
+
440
+ def number_of_submatchers_for_failure_message
441
+ if has_been_qualified?
442
+ number_of_submatchers - 1
443
+ else
444
+ number_of_submatchers
445
+ end
446
+ end
447
+
448
+ def has_been_qualified?
449
+ @submatchers.any? { |submatcher| submatcher_qualified?(submatcher) }
450
+ end
451
+
452
+ def submatcher_qualified?(submatcher)
453
+ submatcher.instance_of?(ComparisonMatcher)
454
+ end
455
+
456
+ def first_submatcher_that_fails_to_match
457
+ @_first_submatcher_that_fails_to_match ||=
458
+ @submatchers.detect do |submatcher|
459
+ !submatcher.matches?(subject)
460
+ end
461
+ end
462
+
463
+ def first_submatcher_that_fails_to_not_match
464
+ @_first_submatcher_that_fails_to_not_match ||=
465
+ @submatchers.detect do |submatcher|
466
+ submatcher.matches?(subject)
467
+ end
468
+ end
469
+
470
+ def failure_message_for_first_submatcher_that_fails_to_match
471
+ build_submatcher_failure_message_for(
472
+ first_submatcher_that_fails_to_match,
473
+ :failure_message,
474
+ )
475
+ end
476
+
477
+ def failure_message_for_first_submatcher_that_fails_to_not_match
478
+ build_submatcher_failure_message_for(
479
+ first_submatcher_that_fails_to_not_match,
480
+ :failure_message_when_negated,
481
+ )
482
+ end
483
+
484
+ def build_submatcher_failure_message_for(
485
+ submatcher,
486
+ failure_message_method
487
+ )
488
+ failure_message = submatcher.public_send(failure_message_method)
489
+ submatcher_description = submatcher.simple_description.
490
+ sub(/\bvalidate that\b/, 'validates').
491
+ sub(/\bdisallow\b/, 'disallows').
492
+ sub(/\ballow\b/, 'allows')
493
+ submatcher_message =
494
+ if number_of_submatchers_for_failure_message > 1
495
+ "In checking that #{model.name} #{submatcher_description}, " +
496
+ failure_message[0].downcase +
497
+ failure_message[1..]
498
+ else
499
+ failure_message
500
+ end
501
+
502
+ Shoulda::Matchers.word_wrap(submatcher_message, indent: 2)
503
+ end
504
+
505
+ def comparison_descriptions
506
+ description_array = submatcher_comparison_descriptions
507
+ if description_array.empty?
508
+ ''
509
+ else
510
+ submatcher_comparison_descriptions.join(' and ')
511
+ end
512
+ end
513
+
514
+ def submatcher_comparison_descriptions
515
+ @submatchers.inject([]) do |arr, submatcher|
516
+ arr << if submatcher.respond_to? :comparison_description
517
+ submatcher.comparison_description
518
+ end
519
+ end
520
+ end
521
+
522
+ def allowed_type_name
523
+ 'value'
524
+ end
525
+
526
+ def non_numeric_value
527
+ 'abcd'
528
+ end
529
+ end
530
+ end
531
+ end
532
+ end
@@ -3,10 +3,10 @@ module Shoulda
3
3
  module ActiveModel
4
4
  # The `validate_exclusion_of` matcher tests usage of the
5
5
  # `validates_exclusion_of` validation, asserting that an attribute cannot
6
- # take a blacklist of values, and inversely, can take values outside of
6
+ # take a blocklist of values, and inversely, can take values outside of
7
7
  # this list.
8
8
  #
9
- # If your blacklist is an array of values, use `in_array`:
9
+ # If your blocklist an array of values, use `in_array`:
10
10
  #
11
11
  # class Game
12
12
  # include ActiveModel::Model
@@ -29,7 +29,7 @@ module Shoulda
29
29
  # in_array(['Mac', 'Linux'])
30
30
  # end
31
31
  #
32
- # If your blacklist is a range of values, use `in_range`:
32
+ # If your blocklist is a range of values, use `in_range`:
33
33
  #
34
34
  # class Game
35
35
  # include ActiveModel::Model
@@ -3,14 +3,15 @@ require 'date'
3
3
 
4
4
  module Shoulda
5
5
  module Matchers
6
+ # @private
6
7
  class ExampleClass; end
7
8
 
8
9
  module ActiveModel
9
10
  # The `validate_inclusion_of` matcher tests usage of the
10
11
  # `validates_inclusion_of` validation, asserting that an attribute can
11
- # take a whitelist of values and cannot take values outside of this list.
12
+ # take a allowlist of values and cannot take values outside of this list.
12
13
  #
13
- # If your whitelist is an array of values, use `in_array`:
14
+ # If your allowlist is an array of values, use `in_array`:
14
15
  #
15
16
  # class Issue
16
17
  # include ActiveModel::Model
@@ -34,7 +35,7 @@ module Shoulda
34
35
  # in_array(['open', 'resolved', 'unresolved'])
35
36
  # end
36
37
  #
37
- # If your whitelist is a range of values, use `in_range`:
38
+ # If your allowlist is a range of values, use `in_range`:
38
39
  #
39
40
  # class Issue
40
41
  # include ActiveModel::Model
@@ -3,7 +3,7 @@ module Shoulda
3
3
  module ActiveModel
4
4
  # The `validate_length_of` matcher tests usage of the
5
5
  # `validates_length_of` matcher. Note that this matcher is intended to be
6
- # used against string columns and not integer columns.
6
+ # used against string columns and associations and not integer columns.
7
7
  #
8
8
  # #### Qualifiers
9
9
  #
@@ -36,7 +36,8 @@ module Shoulda
36
36
  #
37
37
  # Use `is_at_least` to test usage of the `:minimum` option. This asserts
38
38
  # that the attribute can take a string which is equal to or longer than
39
- # the given length and cannot take a string which is shorter.
39
+ # the given length and cannot take a string which is shorter. This qualifier
40
+ # also works for associations.
40
41
  #
41
42
  # class User
42
43
  # include ActiveModel::Model
@@ -61,7 +62,8 @@ module Shoulda
61
62
  #
62
63
  # Use `is_at_most` to test usage of the `:maximum` option. This asserts
63
64
  # that the attribute can take a string which is equal to or shorter than
64
- # the given length and cannot take a string which is longer.
65
+ # the given length and cannot take a string which is longer. This qualifier
66
+ # also works for associations.
65
67
  #
66
68
  # class User
67
69
  # include ActiveModel::Model
@@ -84,7 +86,8 @@ module Shoulda
84
86
  #
85
87
  # Use `is_equal_to` to test usage of the `:is` option. This asserts that
86
88
  # the attribute can take a string which is exactly equal to the given
87
- # length and cannot take a string which is shorter or longer.
89
+ # length and cannot take a string which is shorter or longer. This qualifier
90
+ # also works for associations.
88
91
  #
89
92
  # class User
90
93
  # include ActiveModel::Model
@@ -106,7 +109,7 @@ module Shoulda
106
109
  # ##### is_at_least + is_at_most
107
110
  #
108
111
  # Use `is_at_least` and `is_at_most` together to test usage of the `:in`
109
- # option.
112
+ # option. This qualifies also works for associations.
110
113
  #
111
114
  # class User
112
115
  # include ActiveModel::Model
@@ -260,6 +263,29 @@ module Shoulda
260
263
  # should validate_length_of(:bio).is_at_least(15).allow_blank
261
264
  # end
262
265
  #
266
+ # ##### as_array
267
+ #
268
+ # Use `as_array` if you have an ActiveModel model and the attribute being validated
269
+ # is designed to store an array. (This is not necessary if you have an ActiveRecord
270
+ # model with an array column, as the matcher will detect this automatically.)
271
+ #
272
+ # class User
273
+ # include ActiveModel::Model
274
+ # attribute :arr, array: true
275
+ #
276
+ # validates_length_of :arr, minimum: 15
277
+ # end
278
+ #
279
+ # # RSpec
280
+ # describe User do
281
+ # it { should validate_length_of(:arr).as_array.is_at_least(15) }
282
+ # end
283
+ #
284
+ # # Minitest (Shoulda)
285
+ # class UserTest < ActiveSupport::TestCase
286
+ # should validate_length_of(:arr).as_array.is_at_least(15)
287
+ # end
288
+ #
263
289
  def validate_length_of(attr)
264
290
  ValidateLengthOfMatcher.new(attr)
265
291
  end
@@ -275,6 +301,11 @@ module Shoulda
275
301
  @long_message = nil
276
302
  end
277
303
 
304
+ def as_array
305
+ @options[:array] = true
306
+ self
307
+ end
308
+
278
309
  def is_at_least(length)
279
310
  @options[:minimum] = length
280
311
  @short_message ||= :too_short
@@ -451,15 +482,39 @@ module Shoulda
451
482
  end
452
483
 
453
484
  def allows_length_of?(length, message)
454
- allows_value_of(string_of_length(length), message)
485
+ allows_value_of(value_of_length(length), message)
455
486
  end
456
487
 
457
488
  def disallows_length_of?(length, message)
458
- disallows_value_of(string_of_length(length), message)
489
+ disallows_value_of(value_of_length(length), message)
490
+ end
491
+
492
+ def value_of_length(length)
493
+ if array_column?
494
+ ['x'] * length
495
+ elsif collection_association?
496
+ Array.new(length) { association_reflection.klass.new }
497
+ else
498
+ 'x' * length
499
+ end
500
+ end
501
+
502
+ def array_column?
503
+ @options[:array] || super
504
+ end
505
+
506
+ def collection_association?
507
+ association? && [:has_many, :has_and_belongs_to_many].include?(
508
+ association_reflection.macro,
509
+ )
510
+ end
511
+
512
+ def association?
513
+ association_reflection.present?
459
514
  end
460
515
 
461
- def string_of_length(length)
462
- 'x' * length
516
+ def association_reflection
517
+ model.try(:reflect_on_association, @attribute)
463
518
  end
464
519
 
465
520
  def translated_short_message