servactory 3.1.0.rc3 → 3.1.0.rc4
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.
- checksums.yaml +4 -4
- data/README.md +5 -0
- data/lib/generators/README.md +1 -1
- data/lib/generators/servactory/extension/USAGE +1 -1
- data/lib/generators/servactory/install/USAGE +2 -2
- data/lib/generators/servactory/rspec/USAGE +4 -4
- data/lib/generators/servactory/rspec/rspec_generator.rb +2 -2
- data/lib/generators/servactory/service/USAGE +5 -5
- data/lib/servactory/actions/collection.rb +21 -5
- data/lib/servactory/actions/stages/collection.rb +19 -1
- data/lib/servactory/actions/tools/rules.rb +1 -1
- data/lib/servactory/actions/tools/runner.rb +1 -1
- data/lib/servactory/configuration/collection_mode/class_names_collection.rb +1 -1
- data/lib/servactory/configuration/configurable.rb +1 -1
- data/lib/servactory/configuration/factory.rb +30 -3
- data/lib/servactory/configuration/hash_mode/class_names_collection.rb +2 -1
- data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +150 -37
- data/lib/servactory/context/warehouse/inputs.rb +1 -1
- data/lib/servactory/context/workspace/inputs.rb +2 -2
- data/lib/servactory/context/workspace/internals.rb +2 -2
- data/lib/servactory/context/workspace/outputs.rb +2 -2
- data/lib/servactory/context/workspace.rb +7 -7
- data/lib/servactory/info/builder.rb +4 -3
- data/lib/servactory/inputs/dsl.rb +5 -1
- data/lib/servactory/inputs/input.rb +1 -2
- data/lib/servactory/internals/dsl.rb +7 -1
- data/lib/servactory/maintenance/attributes/base.rb +4 -1
- data/lib/servactory/maintenance/attributes/collection.rb +61 -6
- data/lib/servactory/maintenance/options/collection.rb +46 -15
- data/lib/servactory/maintenance/options/option.rb +3 -12
- data/lib/servactory/maintenance/options/registrar.rb +2 -12
- data/lib/servactory/maintenance/validations/checkers/type.rb +23 -5
- data/lib/servactory/outputs/dsl.rb +7 -1
- data/lib/servactory/result.rb +21 -8
- data/lib/servactory/test_kit/rspec/helpers/exception_validator.rb +112 -0
- data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +3 -1
- data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +125 -129
- data/lib/servactory/test_kit/rspec/helpers/service_inputs_guard.rb +98 -0
- data/lib/servactory/test_kit/rspec/helpers/service_inputs_matcher.rb +75 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +110 -45
- data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +26 -8
- data/lib/servactory/test_kit/rspec/helpers/wrap_block_adapter.rb +74 -0
- data/lib/servactory/test_kit/rspec/helpers.rb +4 -0
- data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +72 -16
- data/lib/servactory/test_kit/rspec/matchers/base/message_expectation.rb +194 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +8 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/option_helper_rules.rb +148 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/proc_message.rb +117 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/required_message.rb +92 -0
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +27 -4
- data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +10 -3
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +58 -23
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +124 -59
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +133 -39
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +192 -17
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +4 -2
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +6 -0
- data/lib/servactory/test_kit/rspec/matchers.rb +15 -0
- data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +29 -8
- data/lib/servactory/tool_kit/dynamic_options/format.rb +2 -3
- data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +1 -1
- data/lib/servactory/tool_kit/dynamic_options/max.rb +1 -1
- data/lib/servactory/tool_kit/dynamic_options/min.rb +1 -1
- data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +33 -5
- data/lib/servactory/tool_kit/dynamic_options/must.rb +2 -3
- data/lib/servactory/tool_kit/dynamic_options/schema.rb +34 -21
- data/lib/servactory/tool_kit/dynamic_options/target.rb +1 -1
- data/lib/servactory/utils.rb +13 -3
- data/lib/servactory/version.rb +1 -1
- data/lib/servactory.rb +3 -0
- metadata +10 -2
|
@@ -18,21 +18,40 @@ module Servactory
|
|
|
18
18
|
# ```ruby
|
|
19
19
|
# it { is_expected.to have_service_input(:user_id).required }
|
|
20
20
|
# it { is_expected.to have_service_input(:name).required("Name is mandatory") }
|
|
21
|
+
# it { is_expected.to have_service_input(:name).required(/is mandatory/) }
|
|
22
|
+
# it { is_expected.to have_service_input(:name).required(:default) }
|
|
21
23
|
# ```
|
|
22
24
|
#
|
|
23
25
|
# ## Validation
|
|
24
26
|
#
|
|
25
27
|
# Checks the `:required` option in attribute data where `is: true`.
|
|
26
28
|
# If a custom message is provided, also validates the message matches.
|
|
29
|
+
#
|
|
30
|
+
# The expected message follows the rules of `.message`: a String is
|
|
31
|
+
# compared with the message exactly, a Regexp is matched against it,
|
|
32
|
+
# an RSpec matcher is applied to the message as defined, and
|
|
33
|
+
# `:default` checks that the input defines no custom message.
|
|
34
|
+
# The default required message does not depend on the given value,
|
|
35
|
+
# so a String or Regexp is compared with it when there is no
|
|
36
|
+
# custom message.
|
|
37
|
+
#
|
|
38
|
+
# A Proc message is called with the keyword arguments the library
|
|
39
|
+
# passes to it: `service:`, `input:` and `value:`, which is `nil`.
|
|
40
|
+
# A Proc message that raises or does not return a String does not
|
|
41
|
+
# match, and the failure message shows the error or the returned value.
|
|
27
42
|
class RequiredSubmatcher < Base::Submatcher
|
|
43
|
+
include Concerns::RequiredMessage
|
|
44
|
+
|
|
28
45
|
# Creates a new required submatcher.
|
|
29
46
|
#
|
|
30
47
|
# @param context [Base::SubmatcherContext] The submatcher context
|
|
31
|
-
# @param custom_message [String, nil] Optional expected error message
|
|
48
|
+
# @param custom_message [String, Regexp, Symbol, Object, nil] Optional expected error message,
|
|
49
|
+
# `:default` or an RSpec matcher
|
|
32
50
|
# @return [RequiredSubmatcher] New submatcher instance
|
|
51
|
+
# @raise [ArgumentError] If the expected message is of another kind
|
|
33
52
|
def initialize(context, custom_message = nil)
|
|
34
53
|
super(context)
|
|
35
|
-
@
|
|
54
|
+
@expectation = Base::MessageExpectation.new(custom_message) unless custom_message.nil?
|
|
36
55
|
end
|
|
37
56
|
|
|
38
57
|
# Returns description for RSpec output.
|
|
@@ -48,41 +67,57 @@ module Servactory
|
|
|
48
67
|
#
|
|
49
68
|
# @return [Boolean] True if input is required with matching message
|
|
50
69
|
def passes?
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
return false unless required?
|
|
71
|
+
return true if expectation.nil?
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
actual_message = required_data.fetch(:message) || default_required_message
|
|
58
|
-
actual_message.casecmp(custom_message).zero?
|
|
73
|
+
@message_mismatch = find_message_mismatch
|
|
74
|
+
@message_mismatch.nil?
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
# Builds the failure message for required validation.
|
|
62
78
|
#
|
|
63
79
|
# @return [String] Failure message with expected vs actual
|
|
64
80
|
def build_failure_message
|
|
65
|
-
|
|
66
|
-
should be required
|
|
81
|
+
return not_required_failure_message unless required?
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
got required: false
|
|
70
|
-
MESSAGE
|
|
83
|
+
"should be required with the expected message\n\n#{@message_mismatch.indent(2)}"
|
|
71
84
|
end
|
|
72
85
|
|
|
73
86
|
private
|
|
74
87
|
|
|
75
|
-
attr_reader :
|
|
88
|
+
attr_reader :expectation
|
|
89
|
+
|
|
90
|
+
# Checks if the input has `required: true`.
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] True if input is required
|
|
93
|
+
def required?
|
|
94
|
+
attribute_data.fetch(:required).fetch(:is) == true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Compares the message of the required option with the expected message.
|
|
98
|
+
#
|
|
99
|
+
# Uses the custom message from `required: { message: }` if provided,
|
|
100
|
+
# otherwise the default message. A Proc message is called with the
|
|
101
|
+
# keyword arguments the library passes to it.
|
|
102
|
+
#
|
|
103
|
+
# @return [String, nil] Explanation of the mismatch, or nil if the messages match
|
|
104
|
+
def find_message_mismatch
|
|
105
|
+
expectation.mismatch_for(
|
|
106
|
+
required_custom_message,
|
|
107
|
+
default_message: default_required_message
|
|
108
|
+
) { |message| call_required_message(message) }
|
|
109
|
+
end
|
|
76
110
|
|
|
77
|
-
#
|
|
111
|
+
# Builds the failure message for an optional input.
|
|
78
112
|
#
|
|
79
|
-
# @return [String]
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
# @return [String] Failure message with expected vs actual required state
|
|
114
|
+
def not_required_failure_message
|
|
115
|
+
<<~MESSAGE
|
|
116
|
+
should be required
|
|
117
|
+
|
|
118
|
+
expected required: true
|
|
119
|
+
got required: false
|
|
120
|
+
MESSAGE
|
|
86
121
|
end
|
|
87
122
|
end
|
|
88
123
|
end
|
|
@@ -35,6 +35,10 @@ module Servactory
|
|
|
35
35
|
# 5. `failure_inclusion_passes?` - fails with value outside inclusion
|
|
36
36
|
# 6. `failure_target_passes?` - fails with value outside target
|
|
37
37
|
class ValidWithSubmatcher < Base::Submatcher # rubocop:disable Metrics/ClassLength
|
|
38
|
+
include Concerns::OptionHelperRules
|
|
39
|
+
include Concerns::ProcMessage
|
|
40
|
+
include Concerns::RequiredMessage
|
|
41
|
+
|
|
38
42
|
# Creates a new valid_with submatcher.
|
|
39
43
|
#
|
|
40
44
|
# @param context [Base::SubmatcherContext] The submatcher context
|
|
@@ -93,44 +97,59 @@ module Servactory
|
|
|
93
97
|
# Checks that service fails with wrong type.
|
|
94
98
|
#
|
|
95
99
|
# @return [Boolean] True if type validation fails as expected
|
|
96
|
-
def failure_type_passes?
|
|
97
|
-
|
|
100
|
+
def failure_type_passes?
|
|
101
|
+
wrong_value = Servactory::TestKit::FakeType.new
|
|
98
102
|
|
|
99
103
|
prepared_attributes = attributes.dup
|
|
100
|
-
prepared_attributes[attribute_name] =
|
|
104
|
+
prepared_attributes[attribute_name] = wrong_value
|
|
105
|
+
|
|
106
|
+
expect_failure_with!(prepared_attributes, type_error_message_for(wrong_value))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Builds the message expected for a value of wrong type.
|
|
110
|
+
#
|
|
111
|
+
# Uses the custom message from `type: { is:, message: }` if provided,
|
|
112
|
+
# otherwise the default message.
|
|
113
|
+
#
|
|
114
|
+
# @param value [Object] The value of wrong type
|
|
115
|
+
# @return [String] Expected error message
|
|
116
|
+
def type_error_message_for(value)
|
|
117
|
+
message = attribute_data.fetch(:type).fetch(:message)
|
|
118
|
+
expected_type = attribute_data.fetch(:types).join(", ")
|
|
119
|
+
given_type = value.class.name
|
|
120
|
+
|
|
121
|
+
return default_type_error_message(expected_type:, given_type:) if message.blank?
|
|
122
|
+
return message unless message.is_a?(Proc)
|
|
123
|
+
|
|
124
|
+
call_proc_message(message, :type, value:, expected_type:, given_type:)
|
|
125
|
+
end
|
|
101
126
|
|
|
102
|
-
|
|
127
|
+
# Builds the default message for a value of wrong type.
|
|
128
|
+
#
|
|
129
|
+
# @param expected_type [String] Expected type names
|
|
130
|
+
# @param given_type [String] Name of the given value's class
|
|
131
|
+
# @return [String] Default error message
|
|
132
|
+
def default_type_error_message(expected_type:, given_type:)
|
|
133
|
+
I18n.t(
|
|
103
134
|
"#{i18n_root_key}.inputs.validations.type.default_error.default",
|
|
104
135
|
service_class_name: described_class.name,
|
|
105
136
|
input_name: attribute_name,
|
|
106
|
-
expected_type
|
|
107
|
-
given_type:
|
|
137
|
+
expected_type:,
|
|
138
|
+
given_type:
|
|
108
139
|
)
|
|
109
|
-
|
|
110
|
-
expect_failure_with!(prepared_attributes, input_required_message)
|
|
111
140
|
end
|
|
112
141
|
|
|
113
142
|
# Checks that required validation fails when input is nil.
|
|
114
143
|
#
|
|
115
144
|
# @return [Boolean] True if required validation fails as expected
|
|
116
|
-
def failure_required_passes?
|
|
145
|
+
def failure_required_passes?
|
|
117
146
|
input_required = attribute_data.fetch(:required).fetch(:is)
|
|
118
147
|
return true unless input_required
|
|
119
148
|
|
|
120
149
|
prepared_attributes = attributes.dup
|
|
121
150
|
prepared_attributes[attribute_name] = nil
|
|
122
151
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if input_required_message.nil?
|
|
126
|
-
input_required_message = I18n.t(
|
|
127
|
-
"#{i18n_root_key}.inputs.validations.required.default_error.default",
|
|
128
|
-
service_class_name: described_class.name,
|
|
129
|
-
input_name: attribute_name
|
|
130
|
-
)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
expect_failure_with!(prepared_attributes, input_required_message)
|
|
152
|
+
expect_failure_with!(prepared_attributes, required_message)
|
|
134
153
|
end
|
|
135
154
|
|
|
136
155
|
# Checks that optional input accepts nil without failure.
|
|
@@ -149,7 +168,7 @@ module Servactory
|
|
|
149
168
|
# Checks that inclusion validation fails with wrong value.
|
|
150
169
|
#
|
|
151
170
|
# @return [Boolean] True if inclusion validation fails as expected
|
|
152
|
-
def failure_inclusion_passes?
|
|
171
|
+
def failure_inclusion_passes?
|
|
153
172
|
input_inclusion_in = attribute_data.dig(:inclusion, :in)
|
|
154
173
|
return true if input_inclusion_in.blank?
|
|
155
174
|
|
|
@@ -158,31 +177,43 @@ module Servactory
|
|
|
158
177
|
prepared_attributes = attributes.dup
|
|
159
178
|
prepared_attributes[attribute_name] = wrong_value
|
|
160
179
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
# If message is a Proc, we can't easily evaluate it in the test context
|
|
164
|
-
# (it may require runtime args like input:, value:), so skip message comparison
|
|
165
|
-
if input_required_message.is_a?(Proc)
|
|
166
|
-
return expect_failure_with!(prepared_attributes, :skip_message_check)
|
|
167
|
-
end
|
|
180
|
+
expect_failure_with!(prepared_attributes, inclusion_error_message_for(wrong_value, input_inclusion_in))
|
|
181
|
+
end
|
|
168
182
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
# Builds the message expected for a value outside the inclusion set.
|
|
184
|
+
#
|
|
185
|
+
# Uses the custom message from `inclusion: { in:, message: }` if provided,
|
|
186
|
+
# otherwise the default message.
|
|
187
|
+
#
|
|
188
|
+
# @param value [Object] The value outside the inclusion set
|
|
189
|
+
# @param option_value [Range, Array, Object] The inclusion constraint
|
|
190
|
+
# @return [Object] Expected error message
|
|
191
|
+
def inclusion_error_message_for(value, option_value)
|
|
192
|
+
message = attribute_data.fetch(:inclusion).fetch(:message)
|
|
193
|
+
return default_inclusion_error_message(value:, option_value:) if message.blank?
|
|
194
|
+
|
|
195
|
+
dynamic_option_message(message, :inclusion, value:, option_value:)
|
|
196
|
+
end
|
|
178
197
|
|
|
179
|
-
|
|
198
|
+
# Builds the default message for a value outside the inclusion set.
|
|
199
|
+
#
|
|
200
|
+
# @param value [Object] The value outside the inclusion set
|
|
201
|
+
# @param option_value [Object] The inclusion constraint
|
|
202
|
+
# @return [String] Default error message
|
|
203
|
+
def default_inclusion_error_message(value:, option_value:)
|
|
204
|
+
I18n.t(
|
|
205
|
+
"#{i18n_root_key}.inputs.validations.must.dynamic_options.inclusion.default",
|
|
206
|
+
service_class_name: described_class.name,
|
|
207
|
+
input_name: attribute_name,
|
|
208
|
+
input_inclusion: option_value.inspect,
|
|
209
|
+
value: value.inspect
|
|
210
|
+
)
|
|
180
211
|
end
|
|
181
212
|
|
|
182
213
|
# Checks that target validation fails with wrong value.
|
|
183
214
|
#
|
|
184
215
|
# @return [Boolean] True if target validation fails as expected
|
|
185
|
-
def failure_target_passes?
|
|
216
|
+
def failure_target_passes?
|
|
186
217
|
input_target_in = attribute_data.dig(:target, :in)
|
|
187
218
|
return true if input_target_in.blank?
|
|
188
219
|
|
|
@@ -192,40 +223,74 @@ module Servactory
|
|
|
192
223
|
prepared_attributes = attributes.dup
|
|
193
224
|
prepared_attributes[attribute_name] = wrong_value
|
|
194
225
|
|
|
195
|
-
|
|
226
|
+
expect_failure_with!(prepared_attributes, target_error_message_for(wrong_value, input_target_in))
|
|
227
|
+
end
|
|
196
228
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
229
|
+
# Builds the message expected for a value outside the target set.
|
|
230
|
+
#
|
|
231
|
+
# Uses the custom message from `target: { in:, message: }` if provided,
|
|
232
|
+
# otherwise the default message.
|
|
233
|
+
#
|
|
234
|
+
# @param value [Object] The value outside the target set
|
|
235
|
+
# @param option_value [Array, Object] The target constraint
|
|
236
|
+
# @return [Object] Expected error message
|
|
237
|
+
def target_error_message_for(value, option_value)
|
|
238
|
+
message = attribute_data.fetch(:target).fetch(:message)
|
|
239
|
+
return default_target_error_message(value:, option_value:) if message.blank?
|
|
240
|
+
|
|
241
|
+
dynamic_option_message(message, :target, value:, option_value:)
|
|
242
|
+
end
|
|
202
243
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
244
|
+
# Builds the default message for a value outside the target set.
|
|
245
|
+
#
|
|
246
|
+
# @param value [Object] The value outside the target set
|
|
247
|
+
# @param option_value [Object] The target constraint
|
|
248
|
+
# @return [String] Default error message
|
|
249
|
+
def default_target_error_message(value:, option_value:)
|
|
250
|
+
I18n.t(
|
|
251
|
+
"#{i18n_root_key}.inputs.validations.must.dynamic_options.target.default",
|
|
252
|
+
service_class_name: described_class.name,
|
|
253
|
+
input_name: attribute_name,
|
|
254
|
+
expected_target: option_value.inspect,
|
|
255
|
+
value: value.inspect
|
|
256
|
+
)
|
|
257
|
+
end
|
|
212
258
|
|
|
213
|
-
|
|
259
|
+
# Returns the custom message of a dynamic option as the library builds it.
|
|
260
|
+
#
|
|
261
|
+
# A Proc message is called with the keywords the library passes to it
|
|
262
|
+
# when the value is outside the option, where the failure has no reason.
|
|
263
|
+
#
|
|
264
|
+
# @param message [Proc, Object] The custom message of the option
|
|
265
|
+
# @param option_name [Symbol] The option name, such as `inclusion`
|
|
266
|
+
# @param value [Object] The value outside the option
|
|
267
|
+
# @param option_value [Object] The value of the option
|
|
268
|
+
# @return [Object] Expected error message
|
|
269
|
+
def dynamic_option_message(message, option_name, value:, option_value:)
|
|
270
|
+
return message unless message.is_a?(Proc)
|
|
271
|
+
|
|
272
|
+
call_proc_message(
|
|
273
|
+
message,
|
|
274
|
+
:dynamic_option,
|
|
275
|
+
value:,
|
|
276
|
+
code: option_helper_rule_name(option_name),
|
|
277
|
+
reason: nil,
|
|
278
|
+
option_name:,
|
|
279
|
+
option_value:
|
|
280
|
+
)
|
|
214
281
|
end
|
|
215
282
|
|
|
216
283
|
# Calls service and verifies it fails with expected message.
|
|
217
284
|
#
|
|
218
285
|
# @param prepared_attributes [Hash] Attributes to pass to service
|
|
219
|
-
# @param expected_message [
|
|
286
|
+
# @param expected_message [Object, nil] Expected error message
|
|
220
287
|
# @return [Boolean] True if service fails with expected message
|
|
221
288
|
def expect_failure_with!(prepared_attributes, expected_message)
|
|
222
289
|
described_class.call!(prepared_attributes).success?
|
|
223
290
|
rescue Servactory::Exceptions::Input => e
|
|
224
|
-
return true if expected_message == :skip_message_check # Just verify error was raised
|
|
225
291
|
return false if expected_message.blank?
|
|
226
292
|
|
|
227
|
-
|
|
228
|
-
message_to_compare.to_s.casecmp(e.message.to_s).zero?
|
|
293
|
+
expected_message.to_s.casecmp(e.message.to_s).zero?
|
|
229
294
|
rescue Servactory::Exceptions::Internal, Servactory::Exceptions::Output
|
|
230
295
|
true
|
|
231
296
|
end
|
|
@@ -17,16 +17,64 @@ module Servactory
|
|
|
17
17
|
# ## Usage
|
|
18
18
|
#
|
|
19
19
|
# ```ruby
|
|
20
|
+
# it { is_expected.to have_service_input(:id).type(Integer).message("ID must be an Integer") }
|
|
20
21
|
# it { is_expected.to have_service_input(:email).inclusion(%w[a b]).message("Invalid email") }
|
|
21
22
|
# it { is_expected.to have_service_input(:data).schema({ key: String }).message("Invalid schema") }
|
|
23
|
+
# it { is_expected.to have_service_input(:email).inclusion(%w[a b]).message(/Invalid/) }
|
|
24
|
+
# it { is_expected.to have_service_input(:email).inclusion(%w[a b]).message(be_a(Proc)) }
|
|
25
|
+
# it { is_expected.to have_service_input(:email).inclusion(%w[a b]).message(:default) }
|
|
26
|
+
# it { is_expected.to have_service_input(:age).must(:be_adult).message("Must be an adult") }
|
|
22
27
|
# ```
|
|
23
28
|
#
|
|
24
29
|
# ## Note
|
|
25
30
|
#
|
|
26
31
|
# Requires `requires_last_submatcher: true` - must follow another
|
|
27
|
-
# submatcher. Uses the previous submatcher's OPTION_NAME constant
|
|
28
|
-
#
|
|
32
|
+
# submatcher. Uses the previous submatcher's OPTION_NAME constant,
|
|
33
|
+
# or its `option_name` for options with a configurable name such as
|
|
34
|
+
# `target`, to find the message field. Chaining it after a submatcher
|
|
35
|
+
# without such an option raises ArgumentError.
|
|
36
|
+
#
|
|
37
|
+
# After `.must`, the must submatcher stores the expected message
|
|
38
|
+
# itself (see MustSubmatcher).
|
|
39
|
+
#
|
|
40
|
+
# Every `.message` in a chain is checked, each against the option
|
|
41
|
+
# chained right before it:
|
|
42
|
+
#
|
|
43
|
+
# ```ruby
|
|
44
|
+
# it do
|
|
45
|
+
# is_expected.to have_service_input(:status)
|
|
46
|
+
# .type(Symbol).message("Status must be a Symbol")
|
|
47
|
+
# .inclusion(%i[active inactive]).message("Status must be active or inactive")
|
|
48
|
+
# end
|
|
49
|
+
# ```
|
|
50
|
+
#
|
|
51
|
+
# A String is compared with the message exactly, a Regexp is matched
|
|
52
|
+
# against it, and an RSpec matcher is applied to the message as
|
|
53
|
+
# defined, for example to check that it is a Proc.
|
|
54
|
+
#
|
|
55
|
+
# A Proc message is called before a String or Regexp comparison with
|
|
56
|
+
# every keyword argument the library passes to it while the service
|
|
57
|
+
# runs: `service:`, the attribute (`input:`, `internal:` or
|
|
58
|
+
# `output:`) and `value:`, plus `expected_type:` and `given_type:`
|
|
59
|
+
# for `type`, or `code:`, `reason:`, `option_name:` and
|
|
60
|
+
# `option_value:` for the other options, and `key_name:`,
|
|
61
|
+
# `expected_type:` and `given_type:` for `schema` as well (see
|
|
62
|
+
# Concerns::ProcMessage). The library passes the same keywords for
|
|
63
|
+
# every failure reason of an option. Values known only while the
|
|
64
|
+
# service runs, such as `value:` and `reason:`, are `nil`.
|
|
65
|
+
# A Proc message that raises with them, for example because it does
|
|
66
|
+
# not accept every keyword, or does not return a String does not
|
|
67
|
+
# match, and the failure message shows the error or the returned
|
|
68
|
+
# value.
|
|
69
|
+
#
|
|
70
|
+
# `:default` checks that the option defines no custom message.
|
|
71
|
+
# Without a custom message, a String or Regexp does not match:
|
|
72
|
+
# the default message of these options depends on values known
|
|
73
|
+
# only while the service runs. Check it with `raise_error` instead.
|
|
29
74
|
class MessageSubmatcher < Base::Submatcher
|
|
75
|
+
include Concerns::OptionHelperRules
|
|
76
|
+
include Concerns::ProcMessage
|
|
77
|
+
|
|
30
78
|
# Option name in attribute data (unused - uses last submatcher's)
|
|
31
79
|
OPTION_NAME = :message
|
|
32
80
|
# Key for the message within the option
|
|
@@ -35,18 +83,22 @@ module Servactory
|
|
|
35
83
|
# Creates a new message submatcher.
|
|
36
84
|
#
|
|
37
85
|
# @param context [Base::SubmatcherContext] The submatcher context
|
|
38
|
-
# @param custom_message [String] Expected error message
|
|
86
|
+
# @param custom_message [String, Regexp, Symbol, Object] Expected error message, `:default`
|
|
87
|
+
# or an RSpec matcher
|
|
39
88
|
# @return [MessageSubmatcher] New submatcher instance
|
|
89
|
+
# @raise [ArgumentError] If the previous submatcher checks no option with a message,
|
|
90
|
+
# or the expected message is of another kind
|
|
40
91
|
def initialize(context, custom_message)
|
|
41
92
|
super(context)
|
|
42
|
-
|
|
93
|
+
ensure_option_with_message!
|
|
94
|
+
@expectation = Base::MessageExpectation.new(custom_message)
|
|
43
95
|
end
|
|
44
96
|
|
|
45
97
|
# Returns description for RSpec output.
|
|
46
98
|
#
|
|
47
99
|
# @return [String] Human-readable description with message
|
|
48
100
|
def description
|
|
49
|
-
"message: #{
|
|
101
|
+
"message: #{expectation.description}"
|
|
50
102
|
end
|
|
51
103
|
|
|
52
104
|
protected
|
|
@@ -55,56 +107,98 @@ module Servactory
|
|
|
55
107
|
#
|
|
56
108
|
# @return [Boolean] True if messages match
|
|
57
109
|
def passes?
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@attribute_schema_is = attribute_schema.fetch(last_submatcher.class::OPTION_BODY_KEY)
|
|
61
|
-
@attribute_schema_message = attribute_schema.fetch(:message)
|
|
110
|
+
attribute_schema = attribute_data[option_name]
|
|
111
|
+
@mismatch = attribute_schema.is_a?(Hash) ? find_mismatch(attribute_schema) : missing_option_explanation
|
|
62
112
|
|
|
63
|
-
|
|
113
|
+
@mismatch.nil?
|
|
64
114
|
end
|
|
65
115
|
|
|
66
116
|
# Builds the failure message for message validation.
|
|
67
117
|
#
|
|
68
118
|
# @return [String] Failure message with expected vs actual message
|
|
69
119
|
def build_failure_message
|
|
70
|
-
return
|
|
120
|
+
"should return expected message in case of problem:\n\n#{@mismatch.indent(2)}"
|
|
121
|
+
end
|
|
71
122
|
|
|
72
|
-
|
|
73
|
-
should return expected message in case of problem:
|
|
123
|
+
private
|
|
74
124
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
125
|
+
attr_reader :expectation
|
|
126
|
+
|
|
127
|
+
# Ensures that the previous submatcher checks an option with a message.
|
|
128
|
+
#
|
|
129
|
+
# @return [void]
|
|
130
|
+
# @raise [ArgumentError] If there is no previous submatcher or its option has no message
|
|
131
|
+
def ensure_option_with_message!
|
|
132
|
+
last_submatcher = context.last_submatcher
|
|
133
|
+
return if last_submatcher.respond_to?(:option_name)
|
|
134
|
+
return if last_submatcher.is_a?(Base::Submatcher) && last_submatcher.class.const_defined?(:OPTION_NAME)
|
|
135
|
+
|
|
136
|
+
raise ArgumentError, option_with_message_error_message(last_submatcher)
|
|
78
137
|
end
|
|
79
138
|
|
|
80
|
-
|
|
139
|
+
# Builds the error message for a `message` chained without an option with a message.
|
|
140
|
+
#
|
|
141
|
+
# @param last_submatcher [Base::Submatcher, nil] The previous submatcher
|
|
142
|
+
# @return [String] Error message
|
|
143
|
+
def option_with_message_error_message(last_submatcher)
|
|
144
|
+
position = last_submatcher.nil? ? "first in the chain" : "after `#{last_submatcher.description}`"
|
|
145
|
+
required = last_submatcher.is_a?(Input::RequiredSubmatcher)
|
|
146
|
+
hint = required ? " To check the required message, pass it to `required`." : ""
|
|
81
147
|
|
|
82
|
-
|
|
148
|
+
"`message` checks the message of the option chained right before it: chain it after " \
|
|
149
|
+
"`type`, `consists_of`, `schema`, `inclusion`, `target` or `must`, not #{position}.#{hint}"
|
|
150
|
+
end
|
|
83
151
|
|
|
84
|
-
# Compares
|
|
152
|
+
# Compares the option's message with the expected message.
|
|
85
153
|
#
|
|
86
|
-
#
|
|
154
|
+
# @param attribute_schema [Hash] The option in attribute data
|
|
155
|
+
# @return [String, nil] Explanation of the mismatch, or nil if the messages match
|
|
156
|
+
def find_mismatch(attribute_schema)
|
|
157
|
+
@attribute_schema_is = attribute_schema[context.last_submatcher.class::OPTION_BODY_KEY]
|
|
158
|
+
@attribute_schema_message = attribute_schema[:message]
|
|
159
|
+
|
|
160
|
+
expectation.mismatch_for(@attribute_schema_message) { |message| call_message(message) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Explains a mismatch for an attribute without the option.
|
|
87
164
|
#
|
|
88
|
-
# @return [
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
false
|
|
165
|
+
# @return [String] Explanation of the mismatch
|
|
166
|
+
def missing_option_explanation
|
|
167
|
+
<<~MESSAGE
|
|
168
|
+
expected #{expectation.description}
|
|
169
|
+
got no `#{option_name}` option
|
|
170
|
+
MESSAGE
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Calls a Proc message with the keyword arguments the library passes to it.
|
|
174
|
+
#
|
|
175
|
+
# `code:` is the name of the must rule the option helper generates,
|
|
176
|
+
# such as `be_inclusion`.
|
|
177
|
+
#
|
|
178
|
+
# @param message [Proc] The attribute's message
|
|
179
|
+
# @return [Object] The message built by the Proc
|
|
180
|
+
def call_message(message)
|
|
181
|
+
if option_name == :type
|
|
182
|
+
return call_proc_message(message, :type, expected_type: @attribute_schema_is.join(", "))
|
|
107
183
|
end
|
|
184
|
+
|
|
185
|
+
call_proc_message(
|
|
186
|
+
message,
|
|
187
|
+
option_name == :schema ? :schema : :dynamic_option,
|
|
188
|
+
code: option_helper_rule_name(option_name),
|
|
189
|
+
option_name:,
|
|
190
|
+
option_value: @attribute_schema_is
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Returns the name of the option validated by the previous submatcher.
|
|
195
|
+
#
|
|
196
|
+
# @return [Symbol] Option name in attribute data
|
|
197
|
+
def option_name
|
|
198
|
+
last_submatcher = context.last_submatcher
|
|
199
|
+
return last_submatcher.option_name if last_submatcher.respond_to?(:option_name)
|
|
200
|
+
|
|
201
|
+
last_submatcher.class::OPTION_NAME
|
|
108
202
|
end
|
|
109
203
|
end
|
|
110
204
|
end
|