shoulda-matchers 5.2.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +22 -7
  4. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +1 -1
  5. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +9 -0
  6. data/lib/shoulda/matchers/active_model/comparison_matcher.rb +162 -0
  7. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +3 -5
  8. data/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +71 -0
  9. data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +58 -0
  10. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +2 -1
  11. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +534 -0
  12. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +5 -5
  13. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +9 -6
  14. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +64 -9
  15. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +72 -80
  16. data/lib/shoulda/matchers/active_model/validation_matcher.rb +6 -0
  17. data/lib/shoulda/matchers/active_model/validator.rb +4 -0
  18. data/lib/shoulda/matchers/active_model.rb +4 -1
  19. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +0 -8
  20. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
  21. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +14 -3
  22. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
  23. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  24. data/lib/shoulda/matchers/active_record.rb +1 -0
  25. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +1 -1
  26. data/lib/shoulda/matchers/rails_shim.rb +10 -8
  27. data/lib/shoulda/matchers/util/word_wrap.rb +2 -2
  28. data/lib/shoulda/matchers/util.rb +1 -1
  29. data/lib/shoulda/matchers/version.rb +1 -1
  30. data/lib/shoulda/matchers.rb +2 -2
  31. data/shoulda-matchers.gemspec +1 -1
  32. metadata +12 -8
  33. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -157
@@ -0,0 +1,151 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `normalize` matcher is used to ensure attribute normalizations
5
+ # are transforming attribute values as expected.
6
+ #
7
+ # Take this model for example:
8
+ #
9
+ # class User < ActiveRecord::Base
10
+ # normalizes :email, with: -> email { email.strip.downcase }
11
+ # end
12
+ #
13
+ # You can use `normalize` providing an input and defining the expected
14
+ # normalization output:
15
+ #
16
+ # # RSpec
17
+ # RSpec.describe User, type: :model do
18
+ # it do
19
+ # should normalize(:email).from(" ME@XYZ.COM\n").to("me@xyz.com")
20
+ # end
21
+ # end
22
+ #
23
+ # # Minitest (Shoulda)
24
+ # class User < ActiveSupport::TestCase
25
+ # should normalize(:email).from(" ME@XYZ.COM\n").to("me@xyz.com")
26
+ # end
27
+ #
28
+ # You can use `normalize` to test multiple attributes at once:
29
+ #
30
+ # class User < ActiveRecord::Base
31
+ # normalizes :email, :handle, with: -> value { value.strip.downcase }
32
+ # end
33
+ #
34
+ # # RSpec
35
+ # RSpec.describe User, type: :model do
36
+ # it do
37
+ # should normalize(:email, :handle).from(" Example\n").to("example")
38
+ # end
39
+ # end
40
+ #
41
+ # # Minitest (Shoulda)
42
+ # class User < ActiveSupport::TestCase
43
+ # should normalize(:email, handle).from(" Example\n").to("example")
44
+ # end
45
+ #
46
+ # If the normalization accepts nil values with the `apply_to_nil` option,
47
+ # you just need to use `.from(nil).to("Your expected value here")`.
48
+ #
49
+ # class User < ActiveRecord::Base
50
+ # normalizes :name, with: -> name { name&.titleize || 'Untitled' },
51
+ # apply_to_nil: true
52
+ # end
53
+ #
54
+ # # RSpec
55
+ # RSpec.describe User, type: :model do
56
+ # it { should normalize(:name).from("jane doe").to("Jane Doe") }
57
+ # it { should normalize(:name).from(nil).to("Untitled") }
58
+ # end
59
+ #
60
+ # # Minitest (Shoulda)
61
+ # class User < ActiveSupport::TestCase
62
+ # should normalize(:name).from("jane doe").to("Jane Doe")
63
+ # should normalize(:name).from(nil).to("Untitled")
64
+ # end
65
+ #
66
+ # @return [NormalizeMatcher]
67
+ #
68
+ def normalize(*attributes)
69
+ if attributes.empty?
70
+ raise ArgumentError, 'need at least one attribute'
71
+ else
72
+ NormalizeMatcher.new(*attributes)
73
+ end
74
+ end
75
+
76
+ # @private
77
+ class NormalizeMatcher
78
+ attr_reader :attributes, :from_value, :to_value, :failure_message,
79
+ :failure_message_when_negated
80
+
81
+ def initialize(*attributes)
82
+ @attributes = attributes
83
+ end
84
+
85
+ def description
86
+ %(
87
+ normalize #{attributes.to_sentence(last_word_connector: ' and ')} from
88
+ ‹#{from_value.inspect}› to ‹#{to_value.inspect}›
89
+ ).squish
90
+ end
91
+
92
+ def from(value)
93
+ @from_value = value
94
+
95
+ self
96
+ end
97
+
98
+ def to(value)
99
+ @to_value = value
100
+
101
+ self
102
+ end
103
+
104
+ def matches?(subject)
105
+ attributes.all? { |attribute| attribute_matches?(subject, attribute) }
106
+ end
107
+
108
+ def does_not_match?(subject)
109
+ attributes.all? { |attribute| attribute_does_not_match?(subject, attribute) }
110
+ end
111
+
112
+ private
113
+
114
+ def attribute_matches?(subject, attribute)
115
+ return true if normalize_attribute?(subject, attribute)
116
+
117
+ @failure_message = build_failure_message(
118
+ attribute,
119
+ subject.class.normalize_value_for(attribute, from_value),
120
+ )
121
+ false
122
+ end
123
+
124
+ def attribute_does_not_match?(subject, attribute)
125
+ return true unless normalize_attribute?(subject, attribute)
126
+
127
+ @failure_message_when_negated = build_failure_message_when_negated(attribute)
128
+ false
129
+ end
130
+
131
+ def normalize_attribute?(subject, attribute)
132
+ subject.class.normalize_value_for(attribute, from_value) == to_value
133
+ end
134
+
135
+ def build_failure_message(attribute, attribute_value)
136
+ %(
137
+ Expected to normalize #{attribute.inspect} from ‹#{from_value.inspect}› to
138
+ ‹#{to_value.inspect}› but it was normalized to ‹#{attribute_value.inspect}›
139
+ ).squish
140
+ end
141
+
142
+ def build_failure_message_when_negated(attribute)
143
+ %(
144
+ Expected to not normalize #{attribute.inspect} from ‹#{from_value.inspect}› to
145
+ ‹#{to_value.inspect}› but it was normalized
146
+ ).squish
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -24,6 +24,7 @@ require 'shoulda/matchers/active_record/define_enum_for_matcher'
24
24
  require 'shoulda/matchers/active_record/uniqueness'
25
25
  require 'shoulda/matchers/active_record/validate_uniqueness_of_matcher'
26
26
  require 'shoulda/matchers/active_record/have_attached_matcher'
27
+ require 'shoulda/matchers/active_record/normalize_matcher'
27
28
 
28
29
  module Shoulda
29
30
  module Matchers
@@ -400,7 +400,7 @@ module Shoulda
400
400
  false
401
401
  rescue NoMethodError => e
402
402
  if e.message =~
403
- /undefined method `#{delegate_method}' for nil:NilClass/
403
+ /undefined method `#{delegate_method}' for nil/
404
404
  false
405
405
  else
406
406
  raise e
@@ -1,7 +1,7 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- module RailsShim # rubocop:disable Metrics/ModuleLength
4
+ module RailsShim
5
5
  class << self
6
6
  def action_pack_version
7
7
  Gem::Version.new(::ActionPack::VERSION::STRING)
@@ -9,10 +9,6 @@ module Shoulda
9
9
  Gem::Version.new('0')
10
10
  end
11
11
 
12
- def active_record_gte_6?
13
- Gem::Requirement.new('>= 6').satisfied_by?(active_record_version)
14
- end
15
-
16
12
  def active_record_version
17
13
  Gem::Version.new(::ActiveRecord::VERSION::STRING)
18
14
  rescue NameError
@@ -25,8 +21,8 @@ module Shoulda
25
21
  Gem::Version.new('0')
26
22
  end
27
23
 
28
- def active_model_gte_7?
29
- Gem::Requirement.new('>= 7').satisfied_by?(active_model_version)
24
+ def active_model_st_6_1?
25
+ Gem::Requirement.new('< 6.1').satisfied_by?(active_model_version)
30
26
  end
31
27
 
32
28
  def active_model_lt_7?
@@ -60,9 +56,10 @@ module Shoulda
60
56
  end
61
57
 
62
58
  def serialized_attributes_for(model)
59
+ type_serialized_defined = Object.const_defined?('ActiveRecord::Type::Serialized')
63
60
  attribute_types_for(model).
64
61
  inject({}) do |hash, (attribute_name, attribute_type)|
65
- if attribute_type.is_a?(::ActiveRecord::Type::Serialized)
62
+ if type_serialized_defined && attribute_type.is_a?(::ActiveRecord::Type::Serialized)
66
63
  hash.merge(attribute_name => attribute_type.coder)
67
64
  else
68
65
  hash
@@ -146,6 +143,10 @@ module Shoulda
146
143
  model.respond_to?(:attribute_types)
147
144
  end
148
145
 
146
+ def validates_column_options?
147
+ Gem::Requirement.new('>= 7.1.0').satisfied_by?(active_record_version)
148
+ end
149
+
149
150
  private
150
151
 
151
152
  def simply_generate_validation_message(
@@ -172,6 +173,7 @@ module Shoulda
172
173
  I18n.translate(primary_translation_key, translate_options)
173
174
  end
174
175
 
176
+ # @private
175
177
  class FakeAttributeType
176
178
  def initialize(model, attribute_name)
177
179
  @model = model
@@ -41,7 +41,7 @@ module Shoulda
41
41
 
42
42
  # @private
43
43
  class Text < ::String
44
- LIST_ITEM_REGEXP = /\A((?:[a-z0-9]+(?:\)|\.)|\*) )/.freeze
44
+ LIST_ITEM_REGEXP = /\A((?:[a-z0-9]+(?:\)|\.)|\*) )/
45
45
 
46
46
  def indented?
47
47
  self =~ /\A +/
@@ -92,7 +92,7 @@ module Shoulda
92
92
  if line.list_item?
93
93
  combined_lines << line
94
94
  else
95
- combined_lines.last << (" #{line}").squeeze(' ')
95
+ combined_lines.last << " #{line}".squeeze(' ')
96
96
  end
97
97
 
98
98
  combined_lines
@@ -94,7 +94,7 @@ module Shoulda
94
94
  0
95
95
  when :date
96
96
  Date.new(2100, 1, 1)
97
- when :datetime, :timestamp
97
+ when :datetime, :timestamp, :timestamptz
98
98
  DateTime.new(2100, 1, 1)
99
99
  when :time
100
100
  Time.new(2000, 1, 1)
@@ -1,6 +1,6 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- VERSION = '5.2.0'.freeze
4
+ VERSION = '6.0.0'.freeze
5
5
  end
6
6
  end
@@ -14,8 +14,8 @@ require 'shoulda/matchers/active_model'
14
14
  require 'shoulda/matchers/active_record'
15
15
  require 'shoulda/matchers/routing'
16
16
 
17
- module Shoulda
18
- module Matchers
17
+ module Shoulda # :nodoc:
18
+ module Matchers # :nodoc:
19
19
  class << self
20
20
  # @private
21
21
  attr_accessor :assertion_exception_class
@@ -36,6 +36,6 @@ Gem::Specification.new do |s|
36
36
  'shoulda-matchers.gemspec']
37
37
  s.require_paths = ['lib']
38
38
 
39
- s.required_ruby_version = '>= 2.6.0'
39
+ s.required_ruby_version = '>= 3.0.5'
40
40
  s.add_dependency('activesupport', '>= 5.2.0')
41
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shoulda-matchers
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tammer Saleh
@@ -11,10 +11,10 @@ authors:
11
11
  - Matt Jankowski
12
12
  - Stafford Brunk
13
13
  - Elliot Winkler
14
- autorequire:
14
+ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2022-09-17 00:00:00.000000000 Z
17
+ date: 2023-12-22 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: activesupport
@@ -69,16 +69,18 @@ files:
69
69
  - lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb
70
70
  - lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb
71
71
  - lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb
72
+ - lib/shoulda/matchers/active_model/comparison_matcher.rb
72
73
  - lib/shoulda/matchers/active_model/disallow_value_matcher.rb
73
74
  - lib/shoulda/matchers/active_model/errors.rb
74
75
  - lib/shoulda/matchers/active_model/have_secure_password_matcher.rb
75
76
  - lib/shoulda/matchers/active_model/helpers.rb
76
77
  - lib/shoulda/matchers/active_model/numericality_matchers.rb
77
- - lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb
78
78
  - lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb
79
79
  - lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb
80
80
  - lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb
81
81
  - lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb
82
+ - lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb
83
+ - lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb
82
84
  - lib/shoulda/matchers/active_model/qualifiers.rb
83
85
  - lib/shoulda/matchers/active_model/qualifiers/allow_blank.rb
84
86
  - lib/shoulda/matchers/active_model/qualifiers/allow_nil.rb
@@ -86,6 +88,7 @@ files:
86
88
  - lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb
87
89
  - lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb
88
90
  - lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb
91
+ - lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb
89
92
  - lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb
90
93
  - lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb
91
94
  - lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb
@@ -120,6 +123,7 @@ files:
120
123
  - lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb
121
124
  - lib/shoulda/matchers/active_record/have_rich_text_matcher.rb
122
125
  - lib/shoulda/matchers/active_record/have_secure_token_matcher.rb
126
+ - lib/shoulda/matchers/active_record/normalize_matcher.rb
123
127
  - lib/shoulda/matchers/active_record/serialize_matcher.rb
124
128
  - lib/shoulda/matchers/active_record/uniqueness.rb
125
129
  - lib/shoulda/matchers/active_record/uniqueness/model.rb
@@ -178,7 +182,7 @@ metadata:
178
182
  documentation_uri: https://matchers.shoulda.io/docs
179
183
  homepage_uri: https://matchers.shoulda.io
180
184
  source_code_uri: https://github.com/thoughtbot/shoulda-matchers
181
- post_install_message:
185
+ post_install_message:
182
186
  rdoc_options: []
183
187
  require_paths:
184
188
  - lib
@@ -186,15 +190,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
186
190
  requirements:
187
191
  - - ">="
188
192
  - !ruby/object:Gem::Version
189
- version: 2.6.0
193
+ version: 3.0.5
190
194
  required_rubygems_version: !ruby/object:Gem::Requirement
191
195
  requirements:
192
196
  - - ">="
193
197
  - !ruby/object:Gem::Version
194
198
  version: '0'
195
199
  requirements: []
196
- rubygems_version: 3.2.32
197
- signing_key:
200
+ rubygems_version: 3.4.20
201
+ signing_key:
198
202
  specification_version: 4
199
203
  summary: Simple one-liner tests for common Rails functionality
200
204
  test_files: []
@@ -1,157 +0,0 @@
1
- module Shoulda
2
- module Matchers
3
- module ActiveModel
4
- module NumericalityMatchers
5
- # @private
6
- class ComparisonMatcher < ValidationMatcher
7
- ERROR_MESSAGES = {
8
- :> => {
9
- label: :greater_than,
10
- assertions: [false, false, true],
11
- },
12
- :>= => {
13
- label: :greater_than_or_equal_to,
14
- assertions: [false, true, true],
15
- },
16
- :< => {
17
- label: :less_than,
18
- assertions: [true, false, false],
19
- },
20
- :<= => {
21
- label: :less_than_or_equal_to,
22
- assertions: [true, true, false],
23
- },
24
- :== => {
25
- label: :equal_to,
26
- assertions: [false, true, false],
27
- },
28
- :!= => {
29
- label: :other_than,
30
- assertions: [true, false, true],
31
- },
32
- }.freeze
33
-
34
- def initialize(numericality_matcher, value, operator)
35
- super(nil)
36
- unless numericality_matcher.respond_to? :diff_to_compare
37
- raise ArgumentError, 'numericality_matcher is invalid'
38
- end
39
-
40
- @numericality_matcher = numericality_matcher
41
- @value = value
42
- @operator = operator
43
- @message = ERROR_MESSAGES[operator][:label]
44
- end
45
-
46
- def simple_description
47
- description = ''
48
-
49
- if expects_strict?
50
- description << ' strictly'
51
- end
52
-
53
- description +
54
- "disallow :#{attribute} from being a number that is not " +
55
- "#{comparison_expectation} #{@value}"
56
- end
57
-
58
- def for(attribute)
59
- @attribute = attribute
60
- self
61
- end
62
-
63
- def with_message(message)
64
- @expects_custom_validation_message = true
65
- @message = message
66
- self
67
- end
68
-
69
- def expects_custom_validation_message?
70
- @expects_custom_validation_message
71
- end
72
-
73
- def matches?(subject)
74
- @subject = subject
75
- all_bounds_correct?
76
- end
77
-
78
- def failure_message
79
- last_failing_submatcher.failure_message
80
- end
81
-
82
- def failure_message_when_negated
83
- last_failing_submatcher.failure_message_when_negated
84
- end
85
-
86
- def comparison_description
87
- "#{comparison_expectation} #{@value}"
88
- end
89
-
90
- private
91
-
92
- def all_bounds_correct?
93
- failing_submatchers.empty?
94
- end
95
-
96
- def failing_submatchers
97
- submatchers_and_results.
98
- select { |x| !x[:matched] }.
99
- map { |x| x[:matcher] }
100
- end
101
-
102
- def last_failing_submatcher
103
- failing_submatchers.last
104
- end
105
-
106
- def submatchers
107
- @_submatchers ||=
108
- comparison_combos.map do |diff, submatcher_method_name|
109
- matcher = __send__(submatcher_method_name, diff, nil)
110
- matcher.with_message(@message, values: { count: @value })
111
- matcher
112
- end
113
- end
114
-
115
- def submatchers_and_results
116
- @_submatchers_and_results ||= submatchers.map do |matcher|
117
- { matcher: matcher, matched: matcher.matches?(@subject) }
118
- end
119
- end
120
-
121
- def comparison_combos
122
- diffs_to_compare.zip(submatcher_method_names)
123
- end
124
-
125
- def submatcher_method_names
126
- assertions.map do |value|
127
- if value
128
- :allow_value_matcher
129
- else
130
- :disallow_value_matcher
131
- end
132
- end
133
- end
134
-
135
- def assertions
136
- ERROR_MESSAGES[@operator][:assertions]
137
- end
138
-
139
- def diffs_to_compare
140
- diff_to_compare = @numericality_matcher.diff_to_compare
141
- values = [-1, 0, 1].map { |sign| @value + (diff_to_compare * sign) }
142
-
143
- if @numericality_matcher.given_numeric_column?
144
- values
145
- else
146
- values.map(&:to_s)
147
- end
148
- end
149
-
150
- def comparison_expectation
151
- ERROR_MESSAGES[@operator][:label].to_s.tr('_', ' ')
152
- end
153
- end
154
- end
155
- end
156
- end
157
- end