nobrainer-rspec 1.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.
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoBrainer
4
+ module Matchers
5
+ # The `have_index_for` matcher tests that the table that backs your model
6
+ # has a specific index.
7
+ #
8
+ # class User
9
+ # include NoBrainer::Document
10
+ #
11
+ # field :name, index: true
12
+ # field :lastname
13
+ #
14
+ # index :lastname
15
+ # end
16
+ #
17
+ # # RSpec
18
+ # RSpec.describe User, type: :model do
19
+ # it { is_expected.to have_index_for(:name) }
20
+ # it { is_expected.to have_index_for(:lastname) }
21
+ # end
22
+ #
23
+ # #### Qualifiers
24
+ #
25
+ # ##### with_multi
26
+ #
27
+ # Use `with_multi` to assert that an index is defined as multi for a certain
28
+ # field.
29
+ #
30
+ # class User
31
+ # include NoBrainer::Document
32
+ #
33
+ # field :name, index: :multi
34
+ # field :lastname
35
+ #
36
+ # index :lastname, multi: true
37
+ # end
38
+ #
39
+ # # RSpec
40
+ # RSpec.describe User, type: :model do
41
+ # it do
42
+ # it { is_expected.to have_index_for(:name).with_multi }
43
+ # it { is_expected.to have_index_for(:lastname).with_multi }
44
+ # end
45
+ # end
46
+ #
47
+ # ##### named
48
+ #
49
+ # Use `named` to assert that an index has a certain name.
50
+ #
51
+ # class User
52
+ # include NoBrainer::Document
53
+ #
54
+ # field :firstname
55
+ # field :lastname
56
+ #
57
+ # index :full_name_compound, %i[firstname lastname]
58
+ # end
59
+ #
60
+ # # RSpec
61
+ # RSpec.describe User, type: :model do
62
+ # it do
63
+ # is_expected.to have_index_for(%i[firstname lastname])
64
+ # .named(:full_name_compound)
65
+ # end
66
+ # end
67
+ #
68
+ class HaveIndexFor # :nodoc:
69
+ def initialize(*attrs)
70
+ @attributes = attrs.collect do |attributes|
71
+ if attributes.is_a?(Array)
72
+ attributes.collect(&:to_sym)
73
+ else
74
+ attributes.to_sym
75
+ end
76
+ end
77
+ end
78
+
79
+ def with_multi(multi = true)
80
+ @multi = multi
81
+ self
82
+ end
83
+
84
+ def named(index_name)
85
+ @index_name = index_name
86
+ self
87
+ end
88
+
89
+ def matches?(klass)
90
+ @klass = klass.is_a?(Class) ? klass : klass.class
91
+ @errors = []
92
+ @attributes.each do |attr|
93
+ if attr.is_a?(Array)
94
+ missing_fields = attr - @klass.fields.keys
95
+ if missing_fields.empty?
96
+ index_key = @index_name || attr.join('_').to_sym
97
+ if @klass.indexes.include?(index_key)
98
+ error = ''
99
+
100
+ # Checking multi
101
+ if @multi && (@klass.indexes[index_key][:multi] != @multi)
102
+ error += " with multi #{@klass.indexes[index_key][:multi].inspect}"
103
+ end
104
+
105
+ @errors.push("field #{attr.inspect}" + error) unless error.blank?
106
+ else
107
+ if @index_name
108
+ @errors.push "no compound index named #{@index_name}"
109
+ else
110
+ @errors.push "no compound index for fields #{attr.to_sentence}"
111
+ end
112
+ end
113
+ else
114
+ @errors.push "the field#{'s' if missing_fields.size > 1} " \
115
+ "#{missing_fields.to_sentence} " \
116
+ "#{missing_fields.size > 1 ? 'are' : 'is'} missing"
117
+ end
118
+ else
119
+ if @klass.fields.include?(attr)
120
+ if @klass.indexes.include?(attr)
121
+ error = ''
122
+
123
+ # Checking multi
124
+ if @multi && (@klass.indexes[attr][:multi] != @multi)
125
+ error += " with multi #{@klass.indexes[attr][:multi].inspect}"
126
+ end
127
+
128
+ @errors.push("field #{attr.inspect}" + error) unless error.blank?
129
+
130
+ else
131
+ @errors.push "no index for field #{attr.inspect}"
132
+ end
133
+ else
134
+ @errors.push "no field named #{attr.inspect}"
135
+ end
136
+ end
137
+ end
138
+ @errors.empty?
139
+ end
140
+
141
+ def failure_message_for_should
142
+ "Expected #{@klass.inspect} to #{description}, got #{@errors.to_sentence}"
143
+ end
144
+
145
+ def failure_message_for_should_not
146
+ "Expected #{@klass.inspect} to not #{description}, got #{@klass.inspect} to #{description}"
147
+ end
148
+
149
+ alias failure_message failure_message_for_should
150
+ alias failure_message_when_negated failure_message_for_should_not
151
+
152
+ def description
153
+ desc = "have index for #{@attributes.collect(&:inspect).to_sentence}"
154
+ desc += ' to be multi' if @multi
155
+ desc += " named #{@index_name}" if @index_name
156
+ desc
157
+ end
158
+ end
159
+
160
+ def have_index_for(*args)
161
+ HaveIndexFor.new(*args)
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoBrainer
4
+ module Matchers
5
+ def have_timestamps
6
+ HaveTimestamps.new
7
+ end
8
+
9
+ class HaveTimestamps
10
+ def initialize
11
+ @root_module = 'NoBrainer::Document::Timestamps'
12
+ end
13
+
14
+ def matches?(actual)
15
+ @model = actual.is_a?(Class) ? actual : actual.class
16
+ @model.included_modules.include?(expected_module)
17
+ end
18
+
19
+ def description
20
+ 'be a NoBrainer document with timestamps'
21
+ end
22
+
23
+ def failure_message
24
+ "Expected #{@model.inspect} class to #{description}"
25
+ end
26
+
27
+ def failure_message_when_negated
28
+ "Expected #{@model.inspect} class to not #{description}"
29
+ end
30
+
31
+ private
32
+
33
+ def expected_module
34
+ @root_module.constantize
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoBrainer
4
+ module Matchers
5
+ module Validations
6
+ class HaveValidationMatcher
7
+ def initialize(field, validation_type)
8
+ @field = field.to_s
9
+ @type = validation_type.to_s
10
+ @options = {}
11
+ end
12
+
13
+ def matches?(actual)
14
+ @klass = actual.is_a?(Class) ? actual : actual.class
15
+
16
+ @validator = @klass.validators_on(@field).detect do |v|
17
+ (v.kind.to_s == @type) && (!v.options[:on] || on_options_matches?(v))
18
+ end
19
+
20
+ if @validator
21
+ @negative_result_message = "#{@type.inspect} validator on #{@field.inspect}"
22
+ @positive_result_message = "#{@type.inspect} validator on #{@field.inspect}"
23
+ else
24
+ @negative_result_message = "no #{@type.inspect} validator on #{@field.inspect}"
25
+ return false
26
+ end
27
+
28
+ true
29
+ end
30
+
31
+ def failure_message_for_should
32
+ "Expected #{@klass.inspect} to #{description}; instead got #{@negative_result_message}"
33
+ end
34
+
35
+ def failure_message_for_should_not
36
+ "Expected #{@klass.inspect} to not #{description}; instead got #{@positive_result_message}"
37
+ end
38
+
39
+ alias failure_message failure_message_for_should
40
+ alias failure_message_when_negated failure_message_for_should_not
41
+
42
+ def description
43
+ desc = "have #{@type.inspect} validator on #{@field.inspect}"
44
+ desc += " on #{@options[:on]}" if @options[:on]
45
+ desc
46
+ end
47
+
48
+ def on(*on_method)
49
+ @options[:on] = on_method.flatten
50
+ self
51
+ end
52
+
53
+ private
54
+
55
+ def check_on
56
+ validator_on_methods = [@validator.options[:on]].flatten
57
+
58
+ if validator_on_methods.any?
59
+ message = " on methods: #{validator_on_methods}"
60
+
61
+ if on_options_covered_by?(@validator)
62
+ @positive_result_message += message
63
+ else
64
+ @negative_result_message += message
65
+ @result = false
66
+ end
67
+ end
68
+ end
69
+
70
+ def on_options_matches?(validator)
71
+ @options[:on] && validator.options[:on] && on_options_covered_by?(validator)
72
+ end
73
+
74
+ def on_options_covered_by?(validator)
75
+ ([@options[:on]].flatten - [validator.options[:on]].flatten).empty?
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoBrainer
4
+ module Matchers
5
+ module Validations
6
+ def validate_acceptance_of(field)
7
+ HaveValidationMatcher.new(field, :acceptance)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoBrainer
4
+ module Matchers
5
+ module Validations
6
+ class ValidateConfirmationOfMatcher < HaveValidationMatcher
7
+ def initialize(name)
8
+ super(name, :confirmation)
9
+ end
10
+ end
11
+
12
+ def validate_confirmation_of(field)
13
+ ValidateConfirmationOfMatcher.new(field)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoBrainer
4
+ module Matchers
5
+ module Validations
6
+ class ValidateExclusionOfMatcher < HaveValidationMatcher
7
+ def initialize(name)
8
+ super(name, :exclusion)
9
+ end
10
+
11
+ def to_not_allow(*values)
12
+ @not_allowed_values = [values].flatten
13
+ self
14
+ end
15
+
16
+ def matches?(actual)
17
+ return false unless result = super(actual)
18
+
19
+ if @not_allowed_values
20
+ raw_validator_not_allowed_values = @validator.options[:in]
21
+
22
+ validator_not_allowed_values = case raw_validator_not_allowed_values
23
+ when Range then raw_validator_not_allowed_values.to_a
24
+ when Proc then raw_validator_not_allowed_values.call(actual)
25
+ else raw_validator_not_allowed_values end
26
+
27
+ allowed_values = @not_allowed_values - validator_not_allowed_values
28
+ if allowed_values.empty?
29
+ @positive_result_message = @positive_result_message += ' not allowing all values mentioned'
30
+ else
31
+ @negative_result_message = @negative_result_message += " allowing the following the ff. values: #{allowed_values.inspect}"
32
+ result = false
33
+ end
34
+ end
35
+
36
+ result
37
+ end
38
+
39
+ def description
40
+ options_desc = []
41
+ options_desc << " not allowing the ff. values: #{@not_allowed_values}" if @not_allowed_values
42
+ "#{super}#{options_desc.to_sentence}"
43
+ end
44
+ end
45
+
46
+ def validate_exclusion_of(field)
47
+ ValidateExclusionOfMatcher.new(field)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoBrainer
4
+ module Matchers
5
+ module Validations
6
+ class ValidateFormatOfMatcher < HaveValidationMatcher
7
+ def initialize(field)
8
+ super(field, :format)
9
+ end
10
+
11
+ def with_format(format)
12
+ @format = format
13
+ self
14
+ end
15
+
16
+ def to_allow(valid_value)
17
+ @valid_value = valid_value
18
+ self
19
+ end
20
+
21
+ def not_to_allow(invalid_value)
22
+ @invalid_value = invalid_value
23
+ self
24
+ end
25
+
26
+ def matches?(actual)
27
+ return false unless result = super(actual)
28
+
29
+ if @format
30
+ if @validator.options[:with] == @format
31
+ @positive_result_message = @positive_result_message += " with format #{@validator.options[:format].inspect}"
32
+ else
33
+ @negative_result_message = @negative_result_message += " with format #{@validator.options[:format].inspect}"
34
+ result = false
35
+ end
36
+ end
37
+
38
+ if @valid_value
39
+ if @validator.options[:with] =~ @valid_value
40
+ @positive_result_message = @positive_result_message += " with #{@valid_value.inspect} as a valid value"
41
+ else
42
+ @negative_result_message = @negative_result_message += " with #{@valid_value.inspect} as an invalid value"
43
+ result = false
44
+ end
45
+ end
46
+
47
+ if @invalid_value
48
+ if @invalid_value !~ @validator.options[:with]
49
+ @positive_result_message = @positive_result_message += " with #{@invalid_value.inspect} as an invalid value"
50
+ else
51
+ @negative_result_message = @negative_result_message += " with #{@invalid_value.inspect} as a valid value"
52
+ result = false
53
+ end
54
+ end
55
+
56
+ result
57
+ end
58
+
59
+ def description
60
+ options_desc = []
61
+ options_desc << " with format #{@format.inspect}" if @format
62
+ options_desc << " allowing the value #{@valid_value.inspect}" if @valid_value
63
+ options_desc << " not allowing the value #{@invalid_value.inspect}" if @invalid_value
64
+ "#{super}#{options_desc.to_sentence}"
65
+ end
66
+ end
67
+
68
+ def validate_format_of(field)
69
+ ValidateFormatOfMatcher.new(field)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoBrainer
4
+ module Matchers
5
+ module Validations
6
+ class ValidateInclusionOfMatcher < HaveValidationMatcher
7
+ def initialize(name)
8
+ super(name, :inclusion)
9
+ end
10
+
11
+ def to_allow(*values)
12
+ @allowed_values = values.map { |v| v.respond_to?(:to_a) ? v.to_a : v }.flatten
13
+ self
14
+ end
15
+
16
+ def matches?(actual)
17
+ return false unless result = super(actual)
18
+
19
+ if @allowed_values
20
+ raw_validator_allowed_values = @validator.options[:in]
21
+
22
+ validator_allowed_values = case raw_validator_allowed_values
23
+ when Range then raw_validator_allowed_values.to_a
24
+ when Proc then raw_validator_allowed_values.call(actual)
25
+ else raw_validator_allowed_values end
26
+
27
+ not_allowed_values = @allowed_values - validator_allowed_values
28
+ if not_allowed_values.empty?
29
+ @positive_result_message = @positive_result_message += ' allowing all values mentioned'
30
+ else
31
+ @negative_result_message = @negative_result_message += " not allowing these values: #{not_allowed_values.inspect}"
32
+ result = false
33
+ end
34
+ end
35
+
36
+ result
37
+ end
38
+
39
+ def description
40
+ options_desc = []
41
+ options_desc << " allowing these values: #{@allowed_values}" if @allowed_values
42
+ "#{super}#{options_desc.to_sentence}"
43
+ end
44
+ end
45
+
46
+ def validate_inclusion_of(field)
47
+ ValidateInclusionOfMatcher.new(field)
48
+ end
49
+ end
50
+ end
51
+ end