shoulda-matchers 3.0.0 → 3.0.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -1
  3. data/Gemfile +1 -2
  4. data/Gemfile.lock +8 -2
  5. data/NEWS.md +29 -1
  6. data/README.md +3 -11
  7. data/doc_config/yard/templates/default/fulldoc/html/css/global.css +17 -0
  8. data/doc_config/yard/templates/default/fulldoc/html/css/style.css +3 -4
  9. data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +1 -1
  10. data/doc_config/yard/templates/default/layout/html/footer.erb +6 -0
  11. data/docs/errors/NonCaseSwappableValueError.md +111 -0
  12. data/gemfiles/4.0.0.gemfile +1 -2
  13. data/gemfiles/4.0.0.gemfile.lock +5 -2
  14. data/gemfiles/4.0.1.gemfile +1 -2
  15. data/gemfiles/4.0.1.gemfile.lock +5 -2
  16. data/gemfiles/4.1.gemfile +1 -2
  17. data/gemfiles/4.1.gemfile.lock +5 -2
  18. data/gemfiles/4.2.gemfile +1 -2
  19. data/gemfiles/4.2.gemfile.lock +5 -2
  20. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +1 -5
  21. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +26 -4
  22. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +9 -8
  23. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +14 -8
  24. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +47 -12
  25. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +15 -9
  26. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +14 -7
  27. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +16 -4
  28. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +67 -5
  29. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +35 -0
  30. data/lib/shoulda/matchers/util.rb +2 -0
  31. data/lib/shoulda/matchers/util/word_wrap.rb +178 -0
  32. data/lib/shoulda/matchers/version.rb +1 -1
  33. data/lib/shoulda/matchers/warn.rb +1 -10
  34. data/spec/acceptance_spec_helper.rb +2 -10
  35. data/spec/doublespeak_spec_helper.rb +1 -17
  36. data/spec/spec_helper.rb +23 -0
  37. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +1 -1
  38. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +5 -2
  39. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +5 -2
  40. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +5 -1
  41. data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +91 -4
  42. data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +292 -2
  43. data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +16 -2
  44. data/spec/unit/shoulda/matchers/util/word_wrap_spec.rb +197 -0
  45. data/spec/unit_spec_helper.rb +1 -16
  46. data/tasks/documentation.rb +10 -17
  47. metadata +9 -2
@@ -412,6 +412,14 @@ module Shoulda
412
412
  if @options[:case_insensitive]
413
413
  disallows_value_of(swapcased_value, @expected_message)
414
414
  else
415
+ if value == swapcased_value
416
+ raise NonCaseSwappableValueError.create(
417
+ model: @subject.class,
418
+ attribute: @attribute,
419
+ value: value
420
+ )
421
+ end
422
+
415
423
  allows_value_of(swapcased_value, @expected_message)
416
424
  end
417
425
  else
@@ -551,6 +559,33 @@ module Shoulda
551
559
  def column_for(scope)
552
560
  @subject.class.columns_hash[scope.to_s]
553
561
  end
562
+
563
+ # @private
564
+ class NonCaseSwappableValueError < Shoulda::Matchers::Error
565
+ attr_accessor :model, :attribute, :value
566
+
567
+ def message
568
+ Shoulda::Matchers.word_wrap <<-MESSAGE
569
+ Your #{model.name} model has a uniqueness validation on :#{attribute} which is
570
+ declared to be case-sensitive, but the value the uniqueness matcher used,
571
+ #{value.inspect}, doesn't contain any alpha characters, so using it to
572
+ to test the case-sensitivity part of the validation is ineffective. There are
573
+ two possible solutions for this depending on what you're trying to do here:
574
+
575
+ a) If you meant for the validation to be case-sensitive, then you need to give
576
+ the uniqueness matcher a saved instance of #{model.name} with a value for
577
+ :#{attribute} that contains alpha characters.
578
+
579
+ b) If you meant for the validation to be case-insensitive, then you need to
580
+ add `case_sensitive: false` to the validation and add `case_insensitive` to
581
+ the matcher.
582
+
583
+ For more information, please see:
584
+
585
+ http://matchers.shoulda.io/docs/v#{Shoulda::Matchers::VERSION}/file.NonCaseSwappableValueError.html
586
+ MESSAGE
587
+ end
588
+ end
554
589
  end
555
590
  end
556
591
  end
@@ -1,3 +1,5 @@
1
+ require 'shoulda/matchers/util/word_wrap'
2
+
1
3
  module Shoulda
2
4
  module Matchers
3
5
  # @private
@@ -0,0 +1,178 @@
1
+ module Shoulda
2
+ module Matchers
3
+ # @private
4
+ def self.word_wrap(document)
5
+ Document.new(document).wrap
6
+ end
7
+
8
+ # @private
9
+ class Document
10
+ def initialize(document)
11
+ @document = document
12
+ end
13
+
14
+ def wrap
15
+ wrapped_paragraphs.map { |lines| lines.join("\n") }.join("\n\n")
16
+ end
17
+
18
+ protected
19
+
20
+ attr_reader :document
21
+
22
+ private
23
+
24
+ def paragraphs
25
+ document.split(/\n{2,}/)
26
+ end
27
+
28
+ def wrapped_paragraphs
29
+ paragraphs.map do |paragraph|
30
+ Paragraph.new(paragraph).wrap
31
+ end
32
+ end
33
+ end
34
+
35
+ # @private
36
+ class Text < ::String
37
+ LIST_ITEM_REGEXP = /\A((?:[a-z0-9]+(?:\)|\.)|\*) )/
38
+
39
+ def indented?
40
+ self =~ /\A[ ]+/
41
+ end
42
+
43
+ def list_item?
44
+ self =~ LIST_ITEM_REGEXP
45
+ end
46
+
47
+ def match_as_list_item
48
+ match(LIST_ITEM_REGEXP)
49
+ end
50
+ end
51
+
52
+ # @private
53
+ class Paragraph
54
+ def initialize(paragraph)
55
+ @paragraph = Text.new(paragraph)
56
+ end
57
+
58
+ def wrap
59
+ if paragraph.indented?
60
+ lines
61
+ elsif paragraph.list_item?
62
+ wrap_list_item
63
+ else
64
+ wrap_generic_paragraph
65
+ end
66
+ end
67
+
68
+ protected
69
+
70
+ attr_reader :paragraph
71
+
72
+ private
73
+
74
+ def wrap_list_item
75
+ wrap_lines(combine_list_item_lines(lines))
76
+ end
77
+
78
+ def lines
79
+ paragraph.split("\n").map { |line| Text.new(line) }
80
+ end
81
+
82
+ def combine_list_item_lines(lines)
83
+ lines.reduce([]) do |combined_lines, line|
84
+ if line.list_item?
85
+ combined_lines << line
86
+ else
87
+ combined_lines.last << (' ' + line).squeeze(' ')
88
+ end
89
+
90
+ combined_lines
91
+ end
92
+ end
93
+
94
+ def wrap_lines(lines)
95
+ lines.map { |line| Line.new(line).wrap }
96
+ end
97
+
98
+ def wrap_generic_paragraph
99
+ Line.new(combine_paragraph_into_one_line).wrap
100
+ end
101
+
102
+ def combine_paragraph_into_one_line
103
+ paragraph.gsub(/\n/, ' ')
104
+ end
105
+ end
106
+
107
+ # @private
108
+ class Line
109
+ TERMINAL_WIDTH = 72
110
+
111
+ def initialize(line)
112
+ @original_line = @line_to_wrap = Text.new(line)
113
+ @indentation = nil
114
+ end
115
+
116
+ def wrap
117
+ lines = []
118
+
119
+ if line_to_wrap.indented?
120
+ lines << line_to_wrap
121
+ else
122
+ loop do
123
+ new_line = (indentation || '') + line_to_wrap
124
+ result = wrap_line(new_line)
125
+ lines << result[:fitted_line].rstrip
126
+ @indentation ||= read_indentation
127
+ @line_to_wrap = result[:leftover]
128
+
129
+ if line_to_wrap.empty? || @original_line == @line_to_wrap
130
+ break
131
+ end
132
+ end
133
+ end
134
+
135
+ lines
136
+ end
137
+
138
+ protected
139
+
140
+ attr_reader :original_line, :line_to_wrap, :indentation
141
+
142
+ private
143
+
144
+ def read_indentation
145
+ match = line_to_wrap.match_as_list_item
146
+
147
+ if match
148
+ ' ' * match[1].length
149
+ else
150
+ ''
151
+ end
152
+ end
153
+
154
+ def wrap_line(line)
155
+ if line.length > TERMINAL_WIDTH
156
+ index = determine_where_to_break_line(line)
157
+ fitted_line = line[0 .. index].rstrip
158
+ leftover = line[index + 1 .. -1]
159
+ else
160
+ fitted_line = line
161
+ leftover = ''
162
+ end
163
+
164
+ { fitted_line: fitted_line, leftover: leftover }
165
+ end
166
+
167
+ def determine_where_to_break_line(line)
168
+ index = TERMINAL_WIDTH - 1
169
+
170
+ while line[index] !~ /\s/
171
+ index -= 1
172
+ end
173
+
174
+ index
175
+ end
176
+ end
177
+ end
178
+ end
@@ -1,6 +1,6 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- VERSION = '3.0.0'.freeze
4
+ VERSION = '3.0.1'.freeze
5
5
  end
6
6
  end
@@ -6,7 +6,7 @@ module Shoulda
6
6
  def self.warn(message)
7
7
  header = "Warning from shoulda-matchers:"
8
8
  divider = "*" * TERMINAL_MAX_WIDTH
9
- wrapped_message = word_wrap(message, TERMINAL_MAX_WIDTH)
9
+ wrapped_message = word_wrap(message)
10
10
  full_message = [
11
11
  divider,
12
12
  [header, wrapped_message.strip].join("\n\n"),
@@ -23,14 +23,5 @@ module Shoulda
23
23
  release. Please use #{new_method} instead.
24
24
  EOT
25
25
  end
26
-
27
- # Source: <https://www.ruby-forum.com/topic/57805>
28
- # @private
29
- def self.word_wrap(text, width=80)
30
- text.
31
- gsub(/\n+/, " ").
32
- gsub( /(\S{#{width}})(?=\S)/, '\1 ' ).
33
- gsub( /(.{1,#{width}})(?:\s+|$)/, "\\1\n" )
34
- end
35
26
  end
36
27
  end
@@ -5,20 +5,14 @@ Tests::CurrentBundle.instance.assert_appraisal!
5
5
  #---
6
6
 
7
7
  require 'rspec/core'
8
- require 'pry'
9
- require 'pry-byebug'
8
+
9
+ require 'spec_helper'
10
10
 
11
11
  Dir[ File.join(File.expand_path('../support/acceptance/**/*.rb', __FILE__)) ].sort.each do |file|
12
12
  require file
13
13
  end
14
14
 
15
15
  RSpec.configure do |config|
16
- config.order = :random
17
-
18
- config.expect_with :rspec do |c|
19
- c.syntax = :expect
20
- end
21
-
22
16
  if config.respond_to?(:infer_spec_type_from_file_location!)
23
17
  config.infer_spec_type_from_file_location!
24
18
  end
@@ -27,5 +21,3 @@ RSpec.configure do |config|
27
21
 
28
22
  config.include AcceptanceTests::Matchers
29
23
  end
30
-
31
- $VERBOSE = true
@@ -1,18 +1,2 @@
1
1
  require 'shoulda/matchers/doublespeak'
2
-
3
- PROJECT_ROOT = File.expand_path('../..', __FILE__)
4
- $LOAD_PATH << File.join(PROJECT_ROOT, 'lib')
5
-
6
- RSpec.configure do |config|
7
- config.order = :random
8
-
9
- config.expect_with :rspec do |c|
10
- c.syntax = :expect
11
- end
12
-
13
- if config.files_to_run.one?
14
- config.default_formatter = 'doc'
15
- end
16
-
17
- config.mock_with :rspec
18
- end
2
+ require 'spec_helper'
@@ -0,0 +1,23 @@
1
+ PROJECT_ROOT = File.expand_path('../..', __FILE__)
2
+ $LOAD_PATH << File.join(PROJECT_ROOT, 'lib')
3
+
4
+ require 'pry'
5
+ require 'pry-byebug'
6
+
7
+ require 'rspec'
8
+
9
+ RSpec.configure do |config|
10
+ config.order = :random
11
+
12
+ config.expect_with :rspec do |c|
13
+ c.syntax = :expect
14
+ end
15
+
16
+ if config.files_to_run.one?
17
+ config.default_formatter = 'doc'
18
+ end
19
+
20
+ config.mock_with :rspec
21
+ end
22
+
23
+ $VERBOSE = true
@@ -283,6 +283,6 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::ComparisonMatcher
283
283
  end
284
284
 
285
285
  def numericality_matcher
286
- double(diff_to_compare: 1)
286
+ double(diff_to_compare: 1, given_numeric_column?: nil)
287
287
  end
288
288
  end
@@ -1,7 +1,7 @@
1
1
  require 'unit_spec_helper'
2
2
 
3
3
  describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::EvenNumberMatcher do
4
- subject { described_class.new(:attr) }
4
+ subject { described_class.new(numericality_matcher, :attr) }
5
5
 
6
6
  it_behaves_like 'a numerical submatcher'
7
7
  it_behaves_like 'a numerical type submatcher'
@@ -84,6 +84,10 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::EvenNumberMatcher
84
84
  end
85
85
  end
86
86
 
87
+ def numericality_matcher
88
+ double(:numericality_matcher, given_numeric_column?: nil)
89
+ end
90
+
87
91
  def validating_even_number(options = {})
88
92
  define_model :example, attr: :string do
89
93
  validates_numericality_of :attr, { even: true }.merge(options)
@@ -93,5 +97,4 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::EvenNumberMatcher
93
97
  def not_validating_even_number
94
98
  define_model(:example, attr: :string).new
95
99
  end
96
-
97
100
  end
@@ -1,7 +1,7 @@
1
1
  require 'unit_spec_helper'
2
2
 
3
3
  describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OddNumberMatcher do
4
- subject { described_class.new(:attr) }
4
+ subject { described_class.new(numericality_matcher, :attr) }
5
5
 
6
6
  it_behaves_like 'a numerical submatcher'
7
7
  it_behaves_like 'a numerical type submatcher'
@@ -84,6 +84,10 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OddNumberMatcher
84
84
  end
85
85
  end
86
86
 
87
+ def numericality_matcher
88
+ double(:numericality_matcher, given_numeric_column?: nil)
89
+ end
90
+
87
91
  def validating_odd_number(options = {})
88
92
  define_model :example, attr: :string do
89
93
  validates_numericality_of :attr, { odd: true }.merge(options)
@@ -93,5 +97,4 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OddNumberMatcher
93
97
  def not_validating_odd_number
94
98
  define_model(:example, attr: :string).new
95
99
  end
96
-
97
100
  end
@@ -1,7 +1,7 @@
1
1
  require 'unit_spec_helper'
2
2
 
3
3
  describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OnlyIntegerMatcher do
4
- subject { described_class.new(:attr) }
4
+ subject { described_class.new(numericality_matcher, :attr) }
5
5
 
6
6
  it_behaves_like 'a numerical submatcher'
7
7
  it_behaves_like 'a numerical type submatcher'
@@ -84,6 +84,10 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OnlyIntegerMatche
84
84
  end
85
85
  end
86
86
 
87
+ def numericality_matcher
88
+ double(:numericality_matcher, given_numeric_column?: nil)
89
+ end
90
+
87
91
  def validating_only_integer(options = {})
88
92
  define_model :example, attr: :string do
89
93
  validates_numericality_of :attr, { only_integer: true }.merge(options)
@@ -75,7 +75,7 @@ describe Shoulda::Matchers::ActiveModel::ValidateInclusionOfMatcher, type: :mode
75
75
  end
76
76
  end
77
77
 
78
- context "against a float attribute" do
78
+ context 'against a float attribute' do
79
79
  it_behaves_like 'it supports in_array',
80
80
  possible_values: [1.0, 2.0, 3.0, 4.0, 5.0],
81
81
  zero: 0.0,
@@ -97,7 +97,7 @@ describe Shoulda::Matchers::ActiveModel::ValidateInclusionOfMatcher, type: :mode
97
97
  end
98
98
  end
99
99
 
100
- context "against a decimal attribute" do
100
+ context 'against a decimal attribute' do
101
101
  it_behaves_like 'it supports in_array',
102
102
  possible_values: [1.0, 2.0, 3.0, 4.0, 5.0].map { |number|
103
103
  BigDecimal.new(number.to_s)
@@ -121,6 +121,72 @@ describe Shoulda::Matchers::ActiveModel::ValidateInclusionOfMatcher, type: :mode
121
121
  end
122
122
  end
123
123
 
124
+ context 'against a date attribute' do
125
+ today = Date.today
126
+
127
+ it_behaves_like 'it supports in_array',
128
+ possible_values: (1..5).map { |n| today + n },
129
+ reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DATE
130
+
131
+ it_behaves_like 'it supports in_range',
132
+ possible_values: (today .. today + 5)
133
+
134
+ define_method :build_object do |options = {}, &block|
135
+ build_object_with_generic_attribute(
136
+ options.merge(column_type: :date, value: today),
137
+ &block
138
+ )
139
+ end
140
+
141
+ def add_outside_value_to(values)
142
+ values + [values.last + 1]
143
+ end
144
+ end
145
+
146
+ context 'against a datetime attribute' do
147
+ now = DateTime.now
148
+
149
+ it_behaves_like 'it supports in_array',
150
+ possible_values: (1..5).map { |n| now + n },
151
+ reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DATETIME
152
+
153
+ it_behaves_like 'it supports in_range',
154
+ possible_values: (now .. now + 5)
155
+
156
+ define_method :build_object do |options = {}, &block|
157
+ build_object_with_generic_attribute(
158
+ options.merge(column_type: :datetime, value: now),
159
+ &block
160
+ )
161
+ end
162
+
163
+ def add_outside_value_to(values)
164
+ values + [values.last + 1]
165
+ end
166
+ end
167
+
168
+ context 'against a time attribute' do
169
+ now = Time.now
170
+
171
+ it_behaves_like 'it supports in_array',
172
+ possible_values: (1..5).map { |n| now + n },
173
+ reserved_outside_value: described_class::ARBITRARY_OUTSIDE_TIME
174
+
175
+ it_behaves_like 'it supports in_range',
176
+ possible_values: (now .. now + 5)
177
+
178
+ define_method :build_object do |options = {}, &block|
179
+ build_object_with_generic_attribute(
180
+ options.merge(column_type: :time, value: now),
181
+ &block
182
+ )
183
+ end
184
+
185
+ def add_outside_value_to(values)
186
+ values + [values.last + 1]
187
+ end
188
+ end
189
+
124
190
  context 'against a string attribute' do
125
191
  it_behaves_like 'it supports in_array',
126
192
  possible_values: %w(foo bar baz),
@@ -270,7 +336,7 @@ describe Shoulda::Matchers::ActiveModel::ValidateInclusionOfMatcher, type: :mode
270
336
  end
271
337
 
272
338
  if zero
273
- it 'matches when one of the given values is a 0' do
339
+ it 'matches when one of the given values is a zero' do
274
340
  valid_values = possible_values + [zero]
275
341
  builder = build_object_allowing(valid_values)
276
342
  expect_to_match_on_values(builder, valid_values)
@@ -468,6 +534,28 @@ describe Shoulda::Matchers::ActiveModel::ValidateInclusionOfMatcher, type: :mode
468
534
  context 'for a database column' do
469
535
  include_context 'for a generic attribute'
470
536
 
537
+ context 'against a timestamp column' do
538
+ now = DateTime.now
539
+
540
+ it_behaves_like 'it supports in_array',
541
+ possible_values: (1..5).map { |n| now + n },
542
+ reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DATETIME
543
+
544
+ it_behaves_like 'it supports in_range',
545
+ possible_values: (now .. now + 5)
546
+
547
+ define_method :build_object do |options = {}, &block|
548
+ build_object_with_generic_attribute(
549
+ options.merge(column_type: :timestamp, value: now),
550
+ &block
551
+ )
552
+ end
553
+
554
+ def add_outside_value_to(values)
555
+ values + [values.last + 1]
556
+ end
557
+ end
558
+
471
559
  context 'against a boolean attribute' do
472
560
  context 'which is nullable' do
473
561
  include_context 'against a boolean attribute for true and false'
@@ -527,7 +615,6 @@ describe Shoulda::Matchers::ActiveModel::ValidateInclusionOfMatcher, type: :mode
527
615
  end
528
616
  end
529
617
 
530
-
531
618
  def build_object_with_generic_attribute(options = {}, &block)
532
619
  attribute_name = :attr
533
620
  column_type = options.fetch(:column_type)