shoulda-matchers 5.3.0 → 6.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +39 -15
  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 +46 -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 +24 -11
  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 +543 -15
  22. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +9 -1
  23. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +1 -0
  24. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +4 -0
  25. data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +23 -19
  26. data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +27 -23
  27. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +0 -8
  28. data/lib/shoulda/matchers/active_record/encrypt_matcher.rb +174 -0
  29. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
  30. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +24 -13
  31. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
  32. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +1 -1
  33. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  34. data/lib/shoulda/matchers/active_record/uniqueness/model.rb +1 -1
  35. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +82 -70
  36. data/lib/shoulda/matchers/active_record.rb +2 -0
  37. data/lib/shoulda/matchers/doublespeak/double_collection.rb +2 -6
  38. data/lib/shoulda/matchers/doublespeak/world.rb +2 -6
  39. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +13 -15
  40. data/lib/shoulda/matchers/integrations/libraries/action_controller.rb +7 -5
  41. data/lib/shoulda/matchers/integrations/libraries/routing.rb +5 -3
  42. data/lib/shoulda/matchers/rails_shim.rb +8 -6
  43. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  44. data/lib/shoulda/matchers/util.rb +18 -20
  45. data/lib/shoulda/matchers/version.rb +1 -1
  46. data/lib/shoulda/matchers.rb +2 -2
  47. data/shoulda-matchers.gemspec +1 -1
  48. metadata +11 -8
  49. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -136
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d083d22f49f99bf60d6c6bdfed7ea08ea1f838a90b4aed439d84108ebc814af9
4
- data.tar.gz: 90cc8440d8c63da1c338306b3d9f129cc9af8dc1a80277d9e7f5b233f2bbe115
3
+ metadata.gz: d549ac8f3629ad37bc56d0d09daf37416ec23d78374a8ed6a630ea23e5c9e487
4
+ data.tar.gz: f09bc94b6be181564cdde56729c2055192a4725adee4ba8b6b7f8e84429fe9a0
5
5
  SHA512:
6
- metadata.gz: '09b1dcf7e19179b50992d34cf73a86e72727cf27a1db911f2adb98fe291dc4d3d711da962d3efdbec60d2b1f280dcecd441e725a7e6a701a152ff60fdd8e340c'
7
- data.tar.gz: 8ba54626246e133cfd86b84aba3d3b05487166d6359b2ba97256af242a057b5ff6cd40d5eae35bd35f619c9a9b0f39cc2c8b6508e299b44e2605e3cdf4f64078
6
+ metadata.gz: 1f3ba08a6d15ad56cb583c0a8289e180854f5d89020b3182b2d6140289800d71bca467f187c9a4718ba26078cc2562f6377d77b00a0e84f2e683079d62ef60d3
7
+ data.tar.gz: 45d0141c79d9642439c9c03ffcd8e41d1aee993eafaea11fc72812456ead1dcf17d1698e9eb7f311c40945c54c77fc4e573a820ffa115fc344e9b0049ed69479
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2006-2022 Tammer Saleh and thoughtbot, inc.
1
+ Copyright (c) Tammer Saleh and thoughtbot, inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [version-badge]: https://img.shields.io/gem/v/shoulda-matchers.svg
4
4
  [rubygems]: https://rubygems.org/gems/shoulda-matchers
5
- [github-actions-badge]: https://img.shields.io/github/workflow/status/thoughtbot/shoulda-matchers/Test
5
+ [github-actions-badge]: https://img.shields.io/github/actions/workflow/status/thoughtbot/shoulda-matchers/ci.yml?branch=main
6
6
  [github-actions]: https://github.com/thoughtbot/shoulda-matchers/actions
7
7
  [downloads-total]: https://img.shields.io/gem/dt/shoulda-matchers.svg
8
8
  [downloads-badge]: https://img.shields.io/gem/dtv/shoulda-matchers.svg
@@ -55,7 +55,7 @@ Start by including `shoulda-matchers` in your Gemfile:
55
55
 
56
56
  ```ruby
57
57
  group :test do
58
- gem 'shoulda-matchers', '~> 5.0'
58
+ gem 'shoulda-matchers', '~> 6.0'
59
59
  end
60
60
  ```
61
61
 
@@ -117,7 +117,7 @@ Otherwise, add `shoulda-matchers` to your Gemfile:
117
117
 
118
118
  ```ruby
119
119
  group :test do
120
- gem 'shoulda-matchers', '~> 5.0'
120
+ gem 'shoulda-matchers', '~> 6.0'
121
121
  end
122
122
  ```
123
123
 
@@ -374,6 +374,8 @@ about any of them, make sure to [consult the documentation][rubydocs]!
374
374
  tests usage of `validates_numericality_of`.
375
375
  * **[validate_presence_of](lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb)**
376
376
  tests usage of `validates_presence_of`.
377
+ * **[validate_comparison_of](lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb)**
378
+ tests usage of `validates_comparison_of`.
377
379
 
378
380
  ### ActiveRecord matchers
379
381
 
@@ -383,8 +385,10 @@ about any of them, make sure to [consult the documentation][rubydocs]!
383
385
  tests your `belongs_to` associations.
384
386
  * **[define_enum_for](lib/shoulda/matchers/active_record/define_enum_for_matcher.rb)**
385
387
  tests usage of the `enum` macro.
386
- * **[have_and_belong_to_many](lib/shoulda/matchers/active_record/association_matcher.rb#L827)**
388
+ * **[have_and_belong_to_many](lib/shoulda/matchers/active_record/association_matcher.rb)**
387
389
  tests your `has_and_belongs_to_many` associations.
390
+ * **[have_delegated_type](lib/shoulda/matchers/active_record/association_matcher.rb#L687)**
391
+ tests usage of the `delegated_type` macro.
388
392
  * **[have_db_column](lib/shoulda/matchers/active_record/have_db_column_matcher.rb)**
389
393
  tests that the table that backs your model has a specific column.
390
394
  * **[have_db_index](lib/shoulda/matchers/active_record/have_db_index_matcher.rb)**
@@ -407,6 +411,10 @@ about any of them, make sure to [consult the documentation][rubydocs]!
407
411
  usage of the `serialize` macro.
408
412
  * **[validate_uniqueness_of](lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb)**
409
413
  tests usage of `validates_uniqueness_of`.
414
+ * **[normalize](lib/shoulda/matchers/active_record/normalize_matcher.rb)** tests
415
+ usage of the `normalize` macro
416
+ * **[encrypt](lib/shoulda/matchers/active_record/encrypt_matcher.rb)**
417
+ tests usage of the `encrypts` macro.
410
418
 
411
419
  ### ActionController matchers
412
420
 
@@ -468,8 +476,8 @@ machine, understanding the codebase, and creating a good pull request.
468
476
 
469
477
  ## Compatibility
470
478
 
471
- Shoulda Matchers is tested and supported against Ruby 2.6+, Rails
472
- 5.2+, RSpec 3.x, and Minitest 5.x.
479
+ Shoulda Matchers is tested and supported against Ruby 3.0+, Rails
480
+ 6.1+, RSpec 3.x, and Minitest 5.x.
473
481
 
474
482
  - For Ruby < 2.4 and Rails < 4.1 compatibility, please use [v3.1.3][v3.1.3].
475
483
  - For Ruby < 3.0 and Rails < 6.1 compatibility, please use [v4.5.1][v4.5.1].
@@ -484,30 +492,46 @@ Shoulda Matchers follows Semantic Versioning 2.0 as defined at
484
492
 
485
493
  ## Team
486
494
 
487
- Shoulda Matchers is maintained by [Elliot Winkler][mcmire] and [Gui
488
- Albuk][guialbuk].
495
+ Shoulda Matchers is currently maintained by [Pedro Paiva][VSPPedro] and [Matheus
496
+ Sales][matsales28]. Previous maintainers include [Elliot Winkler][mcmire],
497
+ [Gui Albuk][guialbuk], [Jason Draper][drapergeek], [Melissa Xie][mxie],
498
+ [Gabe Berke-Williams][gabebw], [Ryan McGeary][rmm5t], [Joe Ferris][jferris], and
499
+ [Tammer Saleh][tammersaleh].
489
500
 
501
+ [VSPPedro]: https://github.com/VSPPedro
502
+ [matsales28]: https://github.com/matsales28
490
503
  [mcmire]: https://github.com/mcmire
491
504
  [guialbuk]: https://github.com/guialbuk
505
+ [drapergeek]: https://github.com/drapergeek
506
+ [mxie]: https://github.com/mxie
507
+ [gabebw]: https://github.com/gabebw
508
+ [rmm5t]: https://github.com/rmm5t
509
+ [jferris]: https://github.com/jferris
510
+ [tammersaleh]: https://github.com/tammersaleh
492
511
 
493
512
  ## Copyright/License
494
513
 
495
- Shoulda Matchers is copyright © 2006-2022 Tammer Saleh and [thoughtbot,
514
+ Shoulda Matchers is copyright © Tammer Saleh and [thoughtbot,
496
515
  inc][thoughtbot-website]. It is free and opensource software and may be
497
516
  redistributed under the terms specified in the [LICENSE](LICENSE) file.
498
517
 
499
518
  [thoughtbot-website]: https://thoughtbot.com
500
519
 
520
+ <!-- START /templates/footer.md -->
501
521
  ## About thoughtbot
502
522
 
503
- ![thoughtbot][thoughtbot-logo]
504
-
505
- [thoughtbot-logo]: https://presskit.thoughtbot.com/images/thoughtbot-logo-for-readmes.svg
523
+ ![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg)
506
524
 
525
+ This repo is maintained and funded by thoughtbot, inc.
507
526
  The names and logos for thoughtbot are trademarks of thoughtbot, inc.
508
527
 
509
- We are passionate about open source software. See [our other
510
- projects][community]. We are [available for hire][hire].
528
+ We love open source software!
529
+ See [our other projects][community].
530
+
531
+ We are [available for hire][hire].
511
532
 
512
533
  [community]: https://thoughtbot.com/community?utm_source=github
513
- [hire]: https://thoughtbot.com?utm_source=github
534
+ [hire]: https://thoughtbot.com/hire-us?utm_source=github
535
+
536
+
537
+ <!-- END /templates/footer.md -->
@@ -11,7 +11,7 @@ module Shoulda
11
11
  module Matchers
12
12
  module ActionController
13
13
  # The `permit` matcher tests that an action in your controller receives a
14
- # whitelist of parameters using Rails' Strong Parameters feature
14
+ # allowlist of parameters using Rails' Strong Parameters feature
15
15
  # (specifically that `permit` was called with the correct arguments).
16
16
  #
17
17
  # Here's an example:
@@ -276,16 +276,14 @@ module Shoulda
276
276
  :context, :subparameter_name, :parameters_double_registry
277
277
 
278
278
  def expectation
279
- message = 'restrict parameters '
279
+ String.new('restrict parameters ').tap do |message|
280
+ if subparameter_name
281
+ message << "on #{subparameter_name.inspect} "
282
+ end
280
283
 
281
- if subparameter_name
282
- message << "on #{subparameter_name.inspect} "
284
+ message << 'to '\
285
+ "#{format_parameter_names(expected_permitted_parameter_names)}"
283
286
  end
284
-
285
- message << 'to '\
286
- "#{format_parameter_names(expected_permitted_parameter_names)}"
287
-
288
- message
289
287
  end
290
288
 
291
289
  def reality
@@ -83,25 +83,23 @@ module Shoulda
83
83
  end
84
84
 
85
85
  def expectation_description
86
- string = 'set'
87
-
88
- string <<
89
- if key_set?
90
- " #{store.name}[#{key.inspect}]"
91
- else
92
- " any key in #{store.name}"
93
- end
94
-
95
- if expected_value_set?
86
+ String.new('set').tap do |string|
96
87
  string <<
97
- if expected_value.is_a?(Regexp)
98
- " to a value matching #{expected_value.inspect}"
88
+ if key_set?
89
+ " #{store.name}[#{key.inspect}]"
99
90
  else
100
- " to #{expected_value.inspect}"
91
+ " any key in #{store.name}"
101
92
  end
102
- end
103
93
 
104
- string
94
+ if expected_value_set?
95
+ string <<
96
+ if expected_value.is_a?(Regexp)
97
+ " to a value matching #{expected_value.inspect}"
98
+ else
99
+ " to #{expected_value.inspect}"
100
+ end
101
+ end
102
+ end
105
103
  end
106
104
  end
107
105
  end
@@ -169,6 +169,36 @@ module Shoulda
169
169
  # on(:create)
170
170
  # end
171
171
  #
172
+ # ##### against
173
+ #
174
+ # Use `against` if the validation is on an attribute
175
+ # other than the attribute being validated:
176
+ #
177
+ # class UserProfile
178
+ # include ActiveModel::Model
179
+ # attr_accessor :website_url
180
+ #
181
+ # alias_attribute :url, :website_url
182
+ #
183
+ # validates_format_of :url, with: URI.regexp
184
+ # end
185
+ #
186
+ # # RSpec
187
+ # RSpec.describe UserProfile, type: :model do
188
+ # it do
189
+ # should allow_value('https://foo.com').
190
+ # for(:website_url).
191
+ # against(:url)
192
+ # end
193
+ # end
194
+ #
195
+ # # Minitest (Shoulda)
196
+ # class UserProfileTest < ActiveSupport::TestCase
197
+ # should allow_value('https://foo.com').
198
+ # for(:website_url).
199
+ # against(:url)
200
+ # end
201
+ #
172
202
  # ##### with_message
173
203
  #
174
204
  # Use `with_message` if you are using a custom validation message.
@@ -349,6 +379,12 @@ module Shoulda
349
379
  self
350
380
  end
351
381
 
382
+ def against(attribute)
383
+ @attribute_to_check_message_against = attribute if attribute.present?
384
+
385
+ self
386
+ end
387
+
352
388
  def with_message(message, given_options = {})
353
389
  if message.present?
354
390
  @expects_custom_validation_message = true
@@ -402,6 +438,10 @@ module Shoulda
402
438
  @result.nil?
403
439
  end
404
440
 
441
+ def has_any_errors?
442
+ validator.record.errors.any?
443
+ end
444
+
405
445
  def failure_message
406
446
  attribute_setter = result.attribute_setter
407
447
 
@@ -480,6 +520,11 @@ module Shoulda
480
520
  message << " it produced these validation errors instead:\n\n"
481
521
  message << validator.all_formatted_validation_error_messages
482
522
  end
523
+ elsif validator.has_any_errors?
524
+ message << ", placing a validation error on :#{attribute_setter.attribute_name}"
525
+ message << '. The Example was invalid,'
526
+ message << " but it had errors involving other attributes:\n\n"
527
+ message << validator.all_formatted_validation_error_messages
483
528
  else
484
529
  message << ', but it was valid instead.'
485
530
  end
@@ -541,7 +586,7 @@ module Shoulda
541
586
  end
542
587
 
543
588
  def default_failure_message_preface
544
- ''.tap do |preface|
589
+ String.new.tap do |preface|
545
590
  if descriptions_for_preset_values.any?
546
591
  preface << 'After setting '
547
592
  preface << descriptions_for_preset_values.to_sentence
@@ -0,0 +1,157 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module Shoulda
4
+ module Matchers
5
+ module ActiveModel
6
+ # @private
7
+ class ComparisonMatcher < ValidationMatcher
8
+ ERROR_MESSAGES = {
9
+ :> => {
10
+ label: :greater_than,
11
+ assertions: [false, false, true],
12
+ },
13
+ :>= => {
14
+ label: :greater_than_or_equal_to,
15
+ assertions: [false, true, true],
16
+ },
17
+ :< => {
18
+ label: :less_than,
19
+ assertions: [true, false, false],
20
+ },
21
+ :<= => {
22
+ label: :less_than_or_equal_to,
23
+ assertions: [true, true, false],
24
+ },
25
+ :== => {
26
+ label: :equal_to,
27
+ assertions: [false, true, false],
28
+ },
29
+ :!= => {
30
+ label: :other_than,
31
+ assertions: [true, false, true],
32
+ },
33
+ }.freeze
34
+
35
+ delegate :failure_message, :failure_message_when_negated, to: :comparison_submatchers
36
+
37
+ def initialize(matcher, value, operator)
38
+ super(nil)
39
+ unless matcher.respond_to? :diff_to_compare
40
+ raise ArgumentError, 'matcher is invalid'
41
+ end
42
+
43
+ @matcher = matcher
44
+ @value = value
45
+ @operator = operator
46
+ @message = ERROR_MESSAGES[operator][:label]
47
+ end
48
+
49
+ def simple_description
50
+ description = ''
51
+
52
+ if expects_strict?
53
+ description = ' strictly'
54
+ end
55
+
56
+ description +
57
+ "disallow :#{attribute} from being a number that is not " +
58
+ "#{comparison_expectation} #{@value}"
59
+ end
60
+
61
+ def for(attribute)
62
+ @attribute = attribute
63
+ self
64
+ end
65
+
66
+ def with_message(message)
67
+ @expects_custom_validation_message = true
68
+ @message = message
69
+ self
70
+ end
71
+
72
+ def expects_custom_validation_message?
73
+ @expects_custom_validation_message
74
+ end
75
+
76
+ def matches?(subject)
77
+ @subject = subject
78
+ comparison_submatchers.matches?(subject)
79
+ end
80
+
81
+ def comparison_description
82
+ "#{comparison_expectation} #{@value}"
83
+ end
84
+
85
+ def comparison_submatchers
86
+ @_comparison_submatchers ||=
87
+ NumericalityMatchers::Submatchers.new(build_comparison_submatchers)
88
+ end
89
+
90
+ private
91
+
92
+ def build_comparison_submatchers
93
+ comparison_combos.map do |diff, submatcher_method_name|
94
+ matcher = __send__(submatcher_method_name, diff, nil)
95
+ matcher.with_message(@message, values: { count: option_value })
96
+ matcher
97
+ end
98
+ end
99
+
100
+ def comparison_combos
101
+ diffs_to_compare.zip(submatcher_method_names)
102
+ end
103
+
104
+ def submatcher_method_names
105
+ assertions.map do |value|
106
+ if value
107
+ :allow_value_matcher
108
+ else
109
+ :disallow_value_matcher
110
+ end
111
+ end
112
+ end
113
+
114
+ def assertions
115
+ ERROR_MESSAGES[@operator][:assertions]
116
+ end
117
+
118
+ def option_value
119
+ if defined?(@_option_value)
120
+ @_option_value
121
+ else
122
+ @_option_value =
123
+ case @value
124
+ when Proc then @value.call(@subject)
125
+ when Symbol then @subject.send(@value)
126
+ else @value
127
+ end
128
+ end
129
+ end
130
+
131
+ def diffs_to_compare
132
+ diff_to_compare = @matcher.diff_to_compare
133
+ values = case option_value
134
+ when String then diffs_when_string(diff_to_compare)
135
+ else [-1, 0, 1].map { |sign| option_value + (diff_to_compare * sign) }
136
+ end
137
+
138
+ if @matcher.given_numeric_column?
139
+ values
140
+ else
141
+ values.map(&:to_s)
142
+ end
143
+ end
144
+
145
+ def diffs_when_string(diff_to_compare)
146
+ [-1, 0, 1].map do |sign|
147
+ option_value[0..-2] + (option_value[-1].ord + diff_to_compare * sign).chr
148
+ end
149
+ end
150
+
151
+ def comparison_expectation
152
+ ERROR_MESSAGES[@operator][:label].to_s.tr('_', ' ')
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -47,6 +47,8 @@ module Shoulda
47
47
  did_not_authenticate_correct_password: 'expected %{subject} to'\
48
48
  ' authenticate the correct %{attribute}',
49
49
  method_not_found: 'expected %{subject} to respond to %{methods}',
50
+ should_not_have_secure_password: 'expected %{subject} to'\
51
+ ' not %{description}!',
50
52
  }.freeze
51
53
 
52
54
  def initialize(attribute)
@@ -69,6 +71,11 @@ module Shoulda
69
71
  failure.nil?
70
72
  end
71
73
 
74
+ def failure_message_when_negated
75
+ MESSAGES[:should_not_have_secure_password] %
76
+ { subject: @subject.class, description: description }
77
+ end
78
+
72
79
  protected
73
80
 
74
81
  attr_reader :subject
@@ -59,7 +59,7 @@ module Shoulda
59
59
  end
60
60
 
61
61
  def build_comparison_submatcher(value, operator)
62
- NumericalityMatchers::ComparisonMatcher.new(@numericality_matcher, value, operator).
62
+ ComparisonMatcher.new(@numericality_matcher, value, operator).
63
63
  for(@attribute).
64
64
  with_message(@message).
65
65
  on(@context)
@@ -14,21 +14,17 @@ module Shoulda
14
14
  end
15
15
 
16
16
  def failure_message
17
- last_failing_submatcher.failure_message
17
+ failing_submatcher.failure_message
18
18
  end
19
19
 
20
20
  def failure_message_when_negated
21
- last_failing_submatcher.failure_message_when_negated
21
+ non_failing_submatcher.failure_message_when_negated
22
22
  end
23
23
 
24
24
  def add(submatcher)
25
25
  @submatchers << submatcher
26
26
  end
27
27
 
28
- def last_failing_submatcher
29
- failing_submatchers.last
30
- end
31
-
32
28
  private
33
29
 
34
30
  def failing_submatchers
@@ -36,6 +32,20 @@ module Shoulda
36
32
  submatcher.matches?(@subject)
37
33
  end
38
34
  end
35
+
36
+ def non_failing_submatchers
37
+ @_non_failing_submatchers ||= @submatchers.reject do |submatcher|
38
+ submatcher.does_not_match?(@subject)
39
+ end
40
+ end
41
+
42
+ def failing_submatcher
43
+ failing_submatchers.last
44
+ end
45
+
46
+ def non_failing_submatcher
47
+ non_failing_submatchers.last
48
+ end
39
49
  end
40
50
  end
41
51
  end
@@ -143,12 +143,6 @@ module Shoulda
143
143
  @subject.class.reflect_on_association(@attribute)
144
144
  end
145
145
 
146
- def array_column?
147
- @subject.class.respond_to?(:columns_hash) &&
148
- @subject.class.columns_hash[@attribute.to_s].respond_to?(:array) &&
149
- @subject.class.columns_hash[@attribute.to_s].array
150
- end
151
-
152
146
  def enum_column?
153
147
  @subject.class.respond_to?(:defined_enums) &&
154
148
  @subject.class.defined_enums.key?(@attribute.to_s)