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
|
@@ -35,6 +35,18 @@ module Servactory
|
|
|
35
35
|
# .fails(CustomException, type: :base, message: "Error")
|
|
36
36
|
# ```
|
|
37
37
|
#
|
|
38
|
+
# Pass-through to the original service (spy pattern):
|
|
39
|
+
#
|
|
40
|
+
# ```ruby
|
|
41
|
+
# allow_service(MyService)
|
|
42
|
+
# .and_call_original
|
|
43
|
+
#
|
|
44
|
+
# allow_service(MyService)
|
|
45
|
+
# .and_wrap_original do |original, **inputs|
|
|
46
|
+
# original.call(**inputs)
|
|
47
|
+
# end
|
|
48
|
+
# ```
|
|
49
|
+
#
|
|
38
50
|
# Sequential returns (first call succeeds, second fails):
|
|
39
51
|
#
|
|
40
52
|
# ```ruby
|
|
@@ -57,12 +69,30 @@ module Servactory
|
|
|
57
69
|
# .with(user_id: 123)
|
|
58
70
|
# ```
|
|
59
71
|
#
|
|
72
|
+
# Configuring the mock after the service has been called registers
|
|
73
|
+
# a new stub, which takes precedence over the earlier one for the calls
|
|
74
|
+
# it matches.
|
|
75
|
+
#
|
|
76
|
+
# A mock without `.with()` (default mock) accepts any arguments on the
|
|
77
|
+
# RSpec level, so it handles every later call of the stubbed method,
|
|
78
|
+
# including calls that mocks configured before it would match. Calls
|
|
79
|
+
# with inputs the service would not accept are rejected instead of
|
|
80
|
+
# deferred to those earlier mocks. Mocks with `.with()` configured after
|
|
81
|
+
# a default mock take precedence for the calls they match:
|
|
82
|
+
#
|
|
83
|
+
# ```ruby
|
|
84
|
+
# allow_service(MyService).succeeds(user: default_user)
|
|
85
|
+
# allow_service(MyService).with(user_id: 123).succeeds(user: user)
|
|
86
|
+
# ```
|
|
87
|
+
#
|
|
60
88
|
# ## Features
|
|
61
89
|
#
|
|
62
90
|
# - **Fluent API** - chainable methods for readable test setup
|
|
63
91
|
# - **Success/Failure** - configure expected result type in one method
|
|
64
92
|
# - **Exception Handling** - auto-creates exceptions with type, message, meta
|
|
65
93
|
# - **Input Matching** - match specific service inputs with `.with()`
|
|
94
|
+
# - **Pass-Through** - delegate to the original service with `.and_call_original`
|
|
95
|
+
# or `.and_wrap_original`
|
|
66
96
|
# - **Sequential Responses** - different results for consecutive calls
|
|
67
97
|
# - **Automatic Validation** - validates inputs and outputs against service definition
|
|
68
98
|
#
|
|
@@ -105,7 +135,7 @@ module Servactory
|
|
|
105
135
|
@config = ServiceMockConfig.new(service_class:)
|
|
106
136
|
@config.method_type = method_type
|
|
107
137
|
@sequential_configs = []
|
|
108
|
-
@
|
|
138
|
+
@stub = nil
|
|
109
139
|
end
|
|
110
140
|
|
|
111
141
|
# ============================================================
|
|
@@ -136,7 +166,7 @@ module Servactory
|
|
|
136
166
|
validate_outputs!(outputs_hash)
|
|
137
167
|
@config.result_type = :success
|
|
138
168
|
@config.outputs = outputs_hash
|
|
139
|
-
|
|
169
|
+
execute_mock
|
|
140
170
|
self
|
|
141
171
|
end
|
|
142
172
|
|
|
@@ -166,7 +196,7 @@ module Servactory
|
|
|
166
196
|
|
|
167
197
|
@config.result_type = :failure
|
|
168
198
|
@config.exception = build_exception(exception_class, type:, message:, meta:)
|
|
169
|
-
|
|
199
|
+
execute_mock
|
|
170
200
|
self
|
|
171
201
|
end
|
|
172
202
|
|
|
@@ -177,6 +207,11 @@ module Servactory
|
|
|
177
207
|
#
|
|
178
208
|
# Inputs are automatically validated against service definition.
|
|
179
209
|
#
|
|
210
|
+
# Without `.with()`, a call matches when all required inputs are present
|
|
211
|
+
# and no unknown inputs are passed; optional inputs may be omitted.
|
|
212
|
+
# Such a mock handles every later call of the stubbed method and rejects
|
|
213
|
+
# invalid inputs instead of deferring to mocks configured before it.
|
|
214
|
+
#
|
|
180
215
|
# @param inputs_hash_or_matcher [Hash, Object] Service inputs to match or RSpec matcher
|
|
181
216
|
# @return [ServiceMockBuilder] self for method chaining
|
|
182
217
|
#
|
|
@@ -192,8 +227,8 @@ module Servactory
|
|
|
192
227
|
# @raise [InputValidator::ValidationError] if inputs don't match service definition
|
|
193
228
|
def with(inputs_hash_or_matcher)
|
|
194
229
|
validate_inputs!(inputs_hash_or_matcher)
|
|
195
|
-
|
|
196
|
-
|
|
230
|
+
all_configs.each { |config| config.argument_matcher = inputs_hash_or_matcher }
|
|
231
|
+
update_mock_arguments
|
|
197
232
|
self
|
|
198
233
|
end
|
|
199
234
|
|
|
@@ -216,14 +251,21 @@ module Servactory
|
|
|
216
251
|
validate_result_type_not_switched!(:and_call_original)
|
|
217
252
|
|
|
218
253
|
@config.result_type = :call_original
|
|
219
|
-
|
|
254
|
+
execute_mock
|
|
220
255
|
self
|
|
221
256
|
end
|
|
222
257
|
|
|
223
258
|
# Wraps the original method with custom logic.
|
|
224
|
-
# Block receives the original method and
|
|
259
|
+
# Block receives the original method and the service inputs as keywords,
|
|
260
|
+
# whether the service was called with keywords, a positional Hash or
|
|
261
|
+
# a Datory object. Input names are symbolized the way the service does it.
|
|
262
|
+
# A block without keyword parameters receives the call arguments as given,
|
|
263
|
+
# e.g. `{}` for `.call({})`.
|
|
225
264
|
#
|
|
226
265
|
# @yield [original, **inputs] Block wrapping the original
|
|
266
|
+
# @yieldparam original [Method] The original `.call` or `.call!` method
|
|
267
|
+
# @yieldparam inputs [Hash{Symbol => Object}] Service inputs
|
|
268
|
+
# @yieldreturn [Object] Value returned to the service caller
|
|
227
269
|
# @return [ServiceMockBuilder] self for method chaining
|
|
228
270
|
#
|
|
229
271
|
# @example Modify result
|
|
@@ -232,13 +274,26 @@ module Servactory
|
|
|
232
274
|
# # custom logic
|
|
233
275
|
# result
|
|
234
276
|
# end
|
|
277
|
+
#
|
|
278
|
+
# @example Modify inputs
|
|
279
|
+
# allow_service(S).and_wrap_original do |original, **inputs|
|
|
280
|
+
# original.call(**inputs, locale: "en")
|
|
281
|
+
# end
|
|
282
|
+
#
|
|
283
|
+
# @example Receive the call arguments as given
|
|
284
|
+
# allow_service(S).and_wrap_original do |original, *arguments|
|
|
285
|
+
# original.call(*arguments)
|
|
286
|
+
# end
|
|
287
|
+
#
|
|
288
|
+
# @raise [ArgumentError] if called without a block
|
|
235
289
|
def and_wrap_original(&block)
|
|
290
|
+
validate_block_given!(:and_wrap_original, block)
|
|
236
291
|
validate_not_in_sequential_mode!(:and_wrap_original)
|
|
237
292
|
validate_result_type_not_switched!(:and_wrap_original)
|
|
238
293
|
|
|
239
294
|
@config.result_type = :wrap_original
|
|
240
295
|
@config.wrap_block = block
|
|
241
|
-
|
|
296
|
+
execute_mock
|
|
242
297
|
self
|
|
243
298
|
end
|
|
244
299
|
|
|
@@ -271,7 +326,7 @@ module Servactory
|
|
|
271
326
|
@config.result_type = :success
|
|
272
327
|
@config.outputs = outputs_hash
|
|
273
328
|
@config.method_type = @sequential_configs.last&.method_type || :call
|
|
274
|
-
|
|
329
|
+
execute_mock
|
|
275
330
|
self
|
|
276
331
|
end
|
|
277
332
|
|
|
@@ -300,7 +355,7 @@ module Servactory
|
|
|
300
355
|
@config.result_type = :failure
|
|
301
356
|
@config.exception = build_exception(exception_class, type:, message:, meta:)
|
|
302
357
|
@config.method_type = @sequential_configs.last&.method_type || :call
|
|
303
|
-
|
|
358
|
+
execute_mock
|
|
304
359
|
self
|
|
305
360
|
end
|
|
306
361
|
|
|
@@ -369,6 +424,21 @@ module Servactory
|
|
|
369
424
|
"Pass-through methods are not compatible with sequential responses."
|
|
370
425
|
end
|
|
371
426
|
|
|
427
|
+
# Validates that a block is given.
|
|
428
|
+
#
|
|
429
|
+
# @param method_name [Symbol] The method being called
|
|
430
|
+
# @param block [Proc, nil] The block given to the method
|
|
431
|
+
# @raise [ArgumentError] if block is missing
|
|
432
|
+
# @return [void]
|
|
433
|
+
def validate_block_given!(method_name, block)
|
|
434
|
+
return unless block.nil?
|
|
435
|
+
|
|
436
|
+
raise ArgumentError,
|
|
437
|
+
"Cannot call #{method_name}() without a block. " \
|
|
438
|
+
"Pass a block that receives the original method and the service inputs, " \
|
|
439
|
+
"e.g. { |original, **inputs| original.call(**inputs) }."
|
|
440
|
+
end
|
|
441
|
+
|
|
372
442
|
# Validates outputs against service definition.
|
|
373
443
|
#
|
|
374
444
|
# @param outputs_hash [Hash] Outputs to validate
|
|
@@ -424,17 +494,6 @@ module Servactory
|
|
|
424
494
|
# Mock Execution
|
|
425
495
|
# ============================================================
|
|
426
496
|
|
|
427
|
-
# Executes or re-executes mock depending on current state.
|
|
428
|
-
#
|
|
429
|
-
# @return [void]
|
|
430
|
-
def execute_or_re_execute_mock
|
|
431
|
-
if @executed
|
|
432
|
-
re_execute_mock
|
|
433
|
-
else
|
|
434
|
-
execute_mock
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
|
|
438
497
|
# Saves current config to sequential list.
|
|
439
498
|
#
|
|
440
499
|
# @return [void]
|
|
@@ -442,48 +501,54 @@ module Servactory
|
|
|
442
501
|
@sequential_configs << @config.dup
|
|
443
502
|
end
|
|
444
503
|
|
|
445
|
-
#
|
|
504
|
+
# Returns all configs in call order.
|
|
505
|
+
#
|
|
506
|
+
# @return [Array<ServiceMockConfig>] Sequential configs followed by the current config
|
|
507
|
+
def all_configs
|
|
508
|
+
@sequential_configs + [@config]
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Registers the mock, or reconfigures the already registered stub.
|
|
512
|
+
#
|
|
513
|
+
# A stub that has already been invoked cannot be reconfigured,
|
|
514
|
+
# so a new stub is registered instead.
|
|
446
515
|
#
|
|
447
516
|
# @return [void]
|
|
448
517
|
def execute_mock
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
@executed = true
|
|
452
|
-
MockExecutor.new(
|
|
453
|
-
service_class:,
|
|
454
|
-
configs: [@config],
|
|
455
|
-
rspec_context: @rspec_context
|
|
456
|
-
).execute
|
|
518
|
+
@stub = mock_executor.execute(reconfigurable_stub)
|
|
457
519
|
end
|
|
458
520
|
|
|
459
|
-
#
|
|
521
|
+
# Applies the current argument matcher to the registered stub.
|
|
522
|
+
#
|
|
523
|
+
# Registers a new stub if the registered one has already been invoked.
|
|
460
524
|
#
|
|
461
525
|
# @return [void]
|
|
462
|
-
def
|
|
463
|
-
return
|
|
526
|
+
def update_mock_arguments
|
|
527
|
+
return if @stub.nil?
|
|
464
528
|
|
|
465
|
-
if @
|
|
466
|
-
|
|
529
|
+
if @stub.invoked?
|
|
530
|
+
execute_mock
|
|
467
531
|
else
|
|
468
|
-
|
|
469
|
-
service_class:,
|
|
470
|
-
configs: [@config],
|
|
471
|
-
rspec_context: @rspec_context
|
|
472
|
-
).execute
|
|
532
|
+
mock_executor.update_arguments(@stub)
|
|
473
533
|
end
|
|
474
534
|
end
|
|
475
535
|
|
|
476
|
-
#
|
|
536
|
+
# Returns the registered stub unless it has already been invoked.
|
|
477
537
|
#
|
|
478
|
-
# @return [
|
|
479
|
-
def
|
|
480
|
-
|
|
538
|
+
# @return [MockExecutor::Stub, nil] Stub that can be reconfigured in place
|
|
539
|
+
def reconfigurable_stub
|
|
540
|
+
@stub unless @stub&.invoked?
|
|
541
|
+
end
|
|
481
542
|
|
|
543
|
+
# Builds an executor for the current configs.
|
|
544
|
+
#
|
|
545
|
+
# @return [MockExecutor] Executor for all configs
|
|
546
|
+
def mock_executor
|
|
482
547
|
MockExecutor.new(
|
|
483
548
|
service_class:,
|
|
484
549
|
configs: all_configs,
|
|
485
550
|
rspec_context: @rspec_context
|
|
486
|
-
)
|
|
551
|
+
)
|
|
487
552
|
end
|
|
488
553
|
end
|
|
489
554
|
end
|
|
@@ -9,9 +9,9 @@ module Servactory
|
|
|
9
9
|
# ## Purpose
|
|
10
10
|
#
|
|
11
11
|
# Holds all configuration for mocking a single service call, including
|
|
12
|
-
# result type (success/failure), method type (call/call!),
|
|
13
|
-
# exceptions,
|
|
14
|
-
# MockExecutor.
|
|
12
|
+
# result type (success/failure/pass-through), method type (call/call!),
|
|
13
|
+
# outputs, exceptions, argument matchers, and the wrap block.
|
|
14
|
+
# Used by ServiceMockBuilder and MockExecutor.
|
|
15
15
|
#
|
|
16
16
|
# ## Usage
|
|
17
17
|
#
|
|
@@ -27,11 +27,12 @@ module Servactory
|
|
|
27
27
|
# ## Attributes
|
|
28
28
|
#
|
|
29
29
|
# - `service_class` - The service class being mocked
|
|
30
|
-
# - `result_type` - :success or :
|
|
30
|
+
# - `result_type` - :success, :failure, :call_original, or :wrap_original
|
|
31
31
|
# - `method_type` - :call or :call!
|
|
32
32
|
# - `outputs` - Hash of output values
|
|
33
33
|
# - `exception` - Exception for failure mocks
|
|
34
34
|
# - `argument_matcher` - RSpec argument matcher or Hash
|
|
35
|
+
# - `wrap_block` - Block for :wrap_original mocks
|
|
35
36
|
class ServiceMockConfig
|
|
36
37
|
attr_accessor :service_class,
|
|
37
38
|
:result_type,
|
|
@@ -83,6 +84,13 @@ module Servactory
|
|
|
83
84
|
result_type == :wrap_original
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
# Checks if this is a pass-through mock.
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] True if result_type is :call_original or :wrap_original
|
|
90
|
+
def pass_through?
|
|
91
|
+
call_original? || wrap_original?
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
# Checks if this mocks the .call! method.
|
|
87
95
|
#
|
|
88
96
|
# @return [Boolean] True if method_type is :call!
|
|
@@ -113,17 +121,27 @@ module Servactory
|
|
|
113
121
|
|
|
114
122
|
# Builds RSpec argument matcher from config.
|
|
115
123
|
#
|
|
116
|
-
# If no matcher specified,
|
|
124
|
+
# If no matcher specified, any arguments are accepted by RSpec
|
|
125
|
+
# and verified against the matcher from #build_inputs_matcher.
|
|
117
126
|
#
|
|
118
127
|
# @param rspec_context [Object] The RSpec test context
|
|
119
128
|
# @return [Object] RSpec argument matcher
|
|
120
129
|
def build_argument_matcher(rspec_context)
|
|
121
130
|
return argument_matcher if argument_matcher.present?
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
rspec_context.any_args
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Builds matcher of service inputs used when no argument matcher is specified.
|
|
136
|
+
#
|
|
137
|
+
# Required inputs must be present, optional inputs may be omitted,
|
|
138
|
+
# and unknown inputs are rejected.
|
|
139
|
+
#
|
|
140
|
+
# @return [ServiceInputsMatcher, nil] Inputs matcher, or nil if an argument matcher is specified
|
|
141
|
+
def build_inputs_matcher
|
|
142
|
+
return if argument_matcher.present?
|
|
125
143
|
|
|
126
|
-
|
|
144
|
+
ServiceInputsMatcher.new(service_class.info.inputs)
|
|
127
145
|
end
|
|
128
146
|
|
|
129
147
|
# Creates a deep copy of this config.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Adapts blocks given to `and_wrap_original` to the arguments of service calls.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Services accept inputs as keywords, as a positional Hash or as
|
|
12
|
+
# a Datory object. A block accepting keywords receives the inputs as
|
|
13
|
+
# keywords in each case, converted with Servactory::Utils.adapt the way
|
|
14
|
+
# the service converts them, so String keys and HashWithIndifferentAccess
|
|
15
|
+
# arrive as Symbol keys. Any other block receives the arguments as given,
|
|
16
|
+
# the way RSpec's `and_wrap_original` passes them, so a call with an
|
|
17
|
+
# empty positional Hash passes `{}` to it.
|
|
18
|
+
#
|
|
19
|
+
# The adaptation is chosen once from the block parameters.
|
|
20
|
+
#
|
|
21
|
+
# ## Usage
|
|
22
|
+
#
|
|
23
|
+
# Used internally by MockExecutor:
|
|
24
|
+
#
|
|
25
|
+
# ```ruby
|
|
26
|
+
# delegate = WrapBlockAdapter.adapt(wrap_block)
|
|
27
|
+
#
|
|
28
|
+
# delegate.call(original, *arguments, &block)
|
|
29
|
+
# ```
|
|
30
|
+
class WrapBlockAdapter
|
|
31
|
+
KEYWORD_PARAMETER_TYPES = %i[key keyreq keyrest].freeze
|
|
32
|
+
|
|
33
|
+
private_constant :KEYWORD_PARAMETER_TYPES
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Adapts a wrap block to the arguments of service calls.
|
|
37
|
+
#
|
|
38
|
+
# @param wrap_block [Proc] Block given to and_wrap_original
|
|
39
|
+
# @return [Proc] Callable receiving the original method and the call arguments
|
|
40
|
+
def adapt(wrap_block)
|
|
41
|
+
return wrap_block unless accepts_keywords?(wrap_block)
|
|
42
|
+
|
|
43
|
+
with_keyword_inputs(wrap_block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Checks whether a block declares keyword parameters.
|
|
49
|
+
#
|
|
50
|
+
# @param wrap_block [Proc] Block given to and_wrap_original
|
|
51
|
+
# @return [Boolean] True if the block accepts keywords
|
|
52
|
+
def accepts_keywords?(wrap_block)
|
|
53
|
+
wrap_block.parameters.any? { |type, _name| KEYWORD_PARAMETER_TYPES.include?(type) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Adapts a block to receive service inputs as keywords.
|
|
57
|
+
#
|
|
58
|
+
# @param wrap_block [Proc] Block accepting keywords
|
|
59
|
+
# @return [Proc] Callable passing the inputs of a call to the block as Symbol keywords
|
|
60
|
+
def with_keyword_inputs(wrap_block)
|
|
61
|
+
lambda do |original, *arguments, &block|
|
|
62
|
+
if arguments.one? && Servactory::Utils.adaptable?(arguments.first)
|
|
63
|
+
wrap_block.call(original, **Servactory::Utils.adapt(arguments.first), &block)
|
|
64
|
+
else
|
|
65
|
+
wrap_block.call(original, *arguments, &block)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "helpers/concerns/error_messages"
|
|
4
4
|
require_relative "helpers/concerns/service_class_validation"
|
|
5
|
+
require_relative "helpers/service_inputs_matcher"
|
|
6
|
+
require_relative "helpers/service_inputs_guard"
|
|
5
7
|
require_relative "helpers/service_mock_config"
|
|
8
|
+
require_relative "helpers/exception_validator"
|
|
9
|
+
require_relative "helpers/wrap_block_adapter"
|
|
6
10
|
require_relative "helpers/mock_executor"
|
|
7
11
|
require_relative "helpers/input_validator"
|
|
8
12
|
require_relative "helpers/output_validator"
|
|
@@ -48,6 +48,9 @@ module Servactory
|
|
|
48
48
|
# - **Fluent API** - chain methods for readable test assertions
|
|
49
49
|
# - **Dynamic Chain Methods** - generated from submatcher registry
|
|
50
50
|
# - **Mutual Exclusivity** - conflicting options replace each other
|
|
51
|
+
# - **Attached Checks** - every `.message` checks the option chained
|
|
52
|
+
# right before it and is removed when that option is replaced;
|
|
53
|
+
# after `.must` it becomes the expected message of the rule
|
|
51
54
|
# - **Composable** - works with RSpec's compound matchers
|
|
52
55
|
# - **Block Expectations** - supports `expect { }.to` syntax
|
|
53
56
|
class AttributeMatcher # rubocop:disable Metrics/ClassLength
|
|
@@ -204,12 +207,39 @@ module Servactory
|
|
|
204
207
|
submatchers << submatcher
|
|
205
208
|
end
|
|
206
209
|
|
|
207
|
-
#
|
|
210
|
+
# Adds a submatcher that checks the option of the last submatcher.
|
|
211
|
+
#
|
|
212
|
+
# Attached submatchers are kept along with each other, so every one
|
|
213
|
+
# of them is checked, and they are removed with the submatcher they
|
|
214
|
+
# are attached to.
|
|
215
|
+
#
|
|
216
|
+
# @param submatcher [Submatcher] The submatcher to attach
|
|
217
|
+
# @return [void]
|
|
218
|
+
def attach_submatcher(submatcher)
|
|
219
|
+
submatchers << submatcher
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Adds a submatcher, or attaches it to the last submatcher if its definition requires one.
|
|
223
|
+
#
|
|
224
|
+
# @param submatcher [Submatcher] The submatcher to store
|
|
225
|
+
# @param definition [SubmatcherDefinition] The submatcher definition
|
|
226
|
+
# @return [void]
|
|
227
|
+
def store_submatcher(submatcher, definition)
|
|
228
|
+
return attach_submatcher(submatcher) if definition.requires_last_submatcher
|
|
229
|
+
|
|
230
|
+
add_submatcher(submatcher)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Removes all submatchers of the specified class and the submatchers attached to them.
|
|
208
234
|
#
|
|
209
235
|
# @param matcher_class [Class] The submatcher class to remove
|
|
210
236
|
# @return [void]
|
|
211
237
|
def remove_submatcher(matcher_class)
|
|
212
|
-
|
|
238
|
+
removed = submatchers.grep(matcher_class)
|
|
239
|
+
|
|
240
|
+
submatchers.delete_if do |sm|
|
|
241
|
+
sm.is_a?(matcher_class) || removed.any? { |removed_sm| sm.attached_to?(removed_sm) }
|
|
242
|
+
end
|
|
213
243
|
end
|
|
214
244
|
|
|
215
245
|
private
|
|
@@ -232,24 +262,15 @@ module Servactory
|
|
|
232
262
|
# 1. Extracts options hash if applicable
|
|
233
263
|
# 2. Removes mutually exclusive submatchers
|
|
234
264
|
# 3. Stores option_types if needed
|
|
235
|
-
# 4. Builds and adds the submatcher
|
|
265
|
+
# 4. Builds and adds the submatcher, or attaches it to the last
|
|
266
|
+
# submatcher if it requires one, unless the last submatcher
|
|
267
|
+
# takes the arguments itself
|
|
236
268
|
#
|
|
237
269
|
# @param definition [SubmatcherDefinition] The submatcher definition
|
|
238
270
|
# @return [void]
|
|
239
|
-
def define_chain_method_for(definition)
|
|
271
|
+
def define_chain_method_for(definition)
|
|
240
272
|
define_singleton_method(definition.chain_method) do |*args|
|
|
241
|
-
|
|
242
|
-
# we need to extract it. We use a heuristic: if the method expects options AND
|
|
243
|
-
# the last arg is a Hash with Symbol keys that look like option keys.
|
|
244
|
-
options = extract_options_hash(args, definition)
|
|
245
|
-
|
|
246
|
-
handle_mutually_exclusive(definition)
|
|
247
|
-
|
|
248
|
-
@option_types = definition.transform_args.call(args, options) if definition.stores_option_types
|
|
249
|
-
|
|
250
|
-
submatcher = build_submatcher(definition, args, options)
|
|
251
|
-
add_submatcher(submatcher)
|
|
252
|
-
self
|
|
273
|
+
apply_chain_method(definition, args)
|
|
253
274
|
end
|
|
254
275
|
|
|
255
276
|
definition.chain_aliases.each do |alias_name|
|
|
@@ -259,6 +280,41 @@ module Servactory
|
|
|
259
280
|
end
|
|
260
281
|
end
|
|
261
282
|
|
|
283
|
+
# Applies a call of a chain method.
|
|
284
|
+
#
|
|
285
|
+
# @param definition [SubmatcherDefinition] The submatcher definition
|
|
286
|
+
# @param args [Array] The method arguments
|
|
287
|
+
# @return [AttributeMatcher] self, for chaining
|
|
288
|
+
def apply_chain_method(definition, args)
|
|
289
|
+
# For methods that accept trailing options hash (like target(value, name: :option)),
|
|
290
|
+
# we need to extract it. We use a heuristic: if the method expects options AND
|
|
291
|
+
# the last arg is a Hash with Symbol keys that look like option keys.
|
|
292
|
+
options = extract_options_hash(args, definition)
|
|
293
|
+
|
|
294
|
+
if expected_by_last_submatcher?(definition)
|
|
295
|
+
last_submatcher.expect_message(*definition.transform_args.call(args, options))
|
|
296
|
+
return self
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
handle_mutually_exclusive(definition)
|
|
300
|
+
|
|
301
|
+
@option_types = definition.transform_args.call(args, options) if definition.stores_option_types
|
|
302
|
+
|
|
303
|
+
store_submatcher(build_submatcher(definition, args, options), definition)
|
|
304
|
+
self
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Checks whether the last submatcher stores the expected message itself.
|
|
308
|
+
#
|
|
309
|
+
# `.must(:rule).message(expected)` stores the expected message in the must
|
|
310
|
+
# submatcher instead of adding a message submatcher.
|
|
311
|
+
#
|
|
312
|
+
# @param definition [SubmatcherDefinition] The submatcher definition
|
|
313
|
+
# @return [Boolean] True if the last submatcher takes the expected message
|
|
314
|
+
def expected_by_last_submatcher?(definition)
|
|
315
|
+
definition.requires_last_submatcher && last_submatcher.respond_to?(:expect_message)
|
|
316
|
+
end
|
|
317
|
+
|
|
262
318
|
# Extracts trailing options hash from arguments if the definition accepts them.
|
|
263
319
|
#
|
|
264
320
|
# @param args [Array] The method arguments
|