shoulda-matchers 5.3.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 (31) 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/range_matcher.rb +1 -1
  8. data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +21 -6
  9. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +534 -0
  10. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +3 -3
  11. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +4 -3
  12. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +64 -9
  13. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +32 -86
  14. data/lib/shoulda/matchers/active_model/validation_matcher.rb +6 -0
  15. data/lib/shoulda/matchers/active_model/validator.rb +4 -0
  16. data/lib/shoulda/matchers/active_model.rb +2 -1
  17. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +0 -8
  18. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
  19. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +14 -3
  20. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
  21. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  22. data/lib/shoulda/matchers/active_record.rb +1 -0
  23. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +1 -1
  24. data/lib/shoulda/matchers/rails_shim.rb +8 -6
  25. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  26. data/lib/shoulda/matchers/util.rb +1 -1
  27. data/lib/shoulda/matchers/version.rb +1 -1
  28. data/lib/shoulda/matchers.rb +2 -2
  29. data/shoulda-matchers.gemspec +1 -1
  30. metadata +10 -8
  31. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -136
@@ -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
@@ -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 +/
@@ -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.3.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.3.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-12-16 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,12 +69,12 @@ 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
@@ -88,6 +88,7 @@ files:
88
88
  - lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb
89
89
  - lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb
90
90
  - lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb
91
+ - lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb
91
92
  - lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb
92
93
  - lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb
93
94
  - lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb
@@ -122,6 +123,7 @@ files:
122
123
  - lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb
123
124
  - lib/shoulda/matchers/active_record/have_rich_text_matcher.rb
124
125
  - lib/shoulda/matchers/active_record/have_secure_token_matcher.rb
126
+ - lib/shoulda/matchers/active_record/normalize_matcher.rb
125
127
  - lib/shoulda/matchers/active_record/serialize_matcher.rb
126
128
  - lib/shoulda/matchers/active_record/uniqueness.rb
127
129
  - lib/shoulda/matchers/active_record/uniqueness/model.rb
@@ -180,7 +182,7 @@ metadata:
180
182
  documentation_uri: https://matchers.shoulda.io/docs
181
183
  homepage_uri: https://matchers.shoulda.io
182
184
  source_code_uri: https://github.com/thoughtbot/shoulda-matchers
183
- post_install_message:
185
+ post_install_message:
184
186
  rdoc_options: []
185
187
  require_paths:
186
188
  - lib
@@ -188,15 +190,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
188
190
  requirements:
189
191
  - - ">="
190
192
  - !ruby/object:Gem::Version
191
- version: 2.6.0
193
+ version: 3.0.5
192
194
  required_rubygems_version: !ruby/object:Gem::Requirement
193
195
  requirements:
194
196
  - - ">="
195
197
  - !ruby/object:Gem::Version
196
198
  version: '0'
197
199
  requirements: []
198
- rubygems_version: 3.3.7
199
- signing_key:
200
+ rubygems_version: 3.4.20
201
+ signing_key:
200
202
  specification_version: 4
201
203
  summary: Simple one-liner tests for common Rails functionality
202
204
  test_files: []
@@ -1,136 +0,0 @@
1
- require 'active_support/core_ext/module/delegation'
2
-
3
- module Shoulda
4
- module Matchers
5
- module ActiveModel
6
- module NumericalityMatchers
7
- # @private
8
- class ComparisonMatcher < ValidationMatcher
9
- ERROR_MESSAGES = {
10
- :> => {
11
- label: :greater_than,
12
- assertions: [false, false, true],
13
- },
14
- :>= => {
15
- label: :greater_than_or_equal_to,
16
- assertions: [false, true, true],
17
- },
18
- :< => {
19
- label: :less_than,
20
- assertions: [true, false, false],
21
- },
22
- :<= => {
23
- label: :less_than_or_equal_to,
24
- assertions: [true, true, false],
25
- },
26
- :== => {
27
- label: :equal_to,
28
- assertions: [false, true, false],
29
- },
30
- :!= => {
31
- label: :other_than,
32
- assertions: [true, false, true],
33
- },
34
- }.freeze
35
-
36
- delegate :failure_message, :failure_message_when_negated, to: :submatchers
37
-
38
- def initialize(numericality_matcher, value, operator)
39
- super(nil)
40
- unless numericality_matcher.respond_to? :diff_to_compare
41
- raise ArgumentError, 'numericality_matcher is invalid'
42
- end
43
-
44
- @numericality_matcher = numericality_matcher
45
- @value = value
46
- @operator = operator
47
- @message = ERROR_MESSAGES[operator][:label]
48
- end
49
-
50
- def simple_description
51
- description = ''
52
-
53
- if expects_strict?
54
- description << ' strictly'
55
- end
56
-
57
- description +
58
- "disallow :#{attribute} from being a number that is not " +
59
- "#{comparison_expectation} #{@value}"
60
- end
61
-
62
- def for(attribute)
63
- @attribute = attribute
64
- self
65
- end
66
-
67
- def with_message(message)
68
- @expects_custom_validation_message = true
69
- @message = message
70
- self
71
- end
72
-
73
- def expects_custom_validation_message?
74
- @expects_custom_validation_message
75
- end
76
-
77
- def matches?(subject)
78
- @subject = subject
79
- submatchers.matches?(subject)
80
- end
81
-
82
- def comparison_description
83
- "#{comparison_expectation} #{@value}"
84
- end
85
-
86
- def submatchers
87
- @_submatchers ||= NumericalityMatchers::Submatchers.new(build_submatchers)
88
- end
89
-
90
- private
91
-
92
- def build_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: @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 diffs_to_compare
119
- diff_to_compare = @numericality_matcher.diff_to_compare
120
- values = [-1, 0, 1].map { |sign| @value + (diff_to_compare * sign) }
121
-
122
- if @numericality_matcher.given_numeric_column?
123
- values
124
- else
125
- values.map(&:to_s)
126
- end
127
- end
128
-
129
- def comparison_expectation
130
- ERROR_MESSAGES[@operator][:label].to_s.tr('_', ' ')
131
- end
132
- end
133
- end
134
- end
135
- end
136
- end