servactory 3.1.0.rc2 → 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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +7 -2
  4. data/config/locales/ja.yml +134 -0
  5. data/lib/generators/README.md +2 -2
  6. data/lib/generators/servactory/extension/USAGE +1 -1
  7. data/lib/generators/servactory/install/USAGE +2 -2
  8. data/lib/generators/servactory/rspec/USAGE +4 -4
  9. data/lib/generators/servactory/rspec/rspec_generator.rb +2 -2
  10. data/lib/generators/servactory/service/USAGE +5 -5
  11. data/lib/servactory/actions/collection.rb +21 -5
  12. data/lib/servactory/actions/stages/collection.rb +19 -1
  13. data/lib/servactory/actions/tools/rules.rb +1 -1
  14. data/lib/servactory/actions/tools/runner.rb +1 -1
  15. data/lib/servactory/configuration/collection_mode/class_names_collection.rb +1 -1
  16. data/lib/servactory/configuration/config.rb +16 -0
  17. data/lib/servactory/configuration/configurable.rb +7 -4
  18. data/lib/servactory/configuration/factory.rb +30 -3
  19. data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -1
  20. data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +156 -29
  21. data/lib/servactory/context/warehouse/inputs.rb +1 -1
  22. data/lib/servactory/context/workspace/inputs.rb +2 -2
  23. data/lib/servactory/context/workspace/internals.rb +2 -2
  24. data/lib/servactory/context/workspace/outputs.rb +2 -2
  25. data/lib/servactory/context/workspace.rb +7 -7
  26. data/lib/servactory/info/builder.rb +4 -3
  27. data/lib/servactory/inputs/dsl.rb +5 -1
  28. data/lib/servactory/inputs/input.rb +1 -2
  29. data/lib/servactory/inputs/tools/validation.rb +10 -18
  30. data/lib/servactory/internals/dsl.rb +7 -1
  31. data/lib/servactory/maintenance/attributes/base.rb +4 -1
  32. data/lib/servactory/maintenance/attributes/collection.rb +61 -6
  33. data/lib/servactory/maintenance/options/collection.rb +71 -30
  34. data/lib/servactory/maintenance/options/option.rb +3 -12
  35. data/lib/servactory/maintenance/options/registrar.rb +3 -13
  36. data/lib/servactory/maintenance/validations/checkers/type.rb +23 -5
  37. data/lib/servactory/maintenance/validations/performer.rb +12 -23
  38. data/lib/servactory/outputs/dsl.rb +7 -1
  39. data/lib/servactory/result.rb +21 -8
  40. data/lib/servactory/test_kit/rspec/helpers/exception_validator.rb +112 -0
  41. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +3 -1
  42. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +125 -125
  43. data/lib/servactory/test_kit/rspec/helpers/service_inputs_guard.rb +98 -0
  44. data/lib/servactory/test_kit/rspec/helpers/service_inputs_matcher.rb +75 -0
  45. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +185 -50
  46. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +45 -10
  47. data/lib/servactory/test_kit/rspec/helpers/wrap_block_adapter.rb +74 -0
  48. data/lib/servactory/test_kit/rspec/helpers.rb +4 -0
  49. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +72 -16
  50. data/lib/servactory/test_kit/rspec/matchers/base/message_expectation.rb +194 -0
  51. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +8 -0
  52. data/lib/servactory/test_kit/rspec/matchers/concerns/option_helper_rules.rb +148 -0
  53. data/lib/servactory/test_kit/rspec/matchers/concerns/proc_message.rb +117 -0
  54. data/lib/servactory/test_kit/rspec/matchers/concerns/required_message.rb +92 -0
  55. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +27 -4
  56. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +10 -3
  57. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +58 -23
  58. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +124 -59
  59. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +133 -39
  60. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +192 -17
  61. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +4 -2
  62. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +6 -0
  63. data/lib/servactory/test_kit/rspec/matchers.rb +15 -0
  64. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +34 -11
  65. data/lib/servactory/tool_kit/dynamic_options/format.rb +3 -4
  66. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +3 -3
  67. data/lib/servactory/tool_kit/dynamic_options/max.rb +3 -3
  68. data/lib/servactory/tool_kit/dynamic_options/min.rb +3 -3
  69. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +35 -7
  70. data/lib/servactory/tool_kit/dynamic_options/must.rb +2 -3
  71. data/lib/servactory/tool_kit/dynamic_options/schema.rb +40 -25
  72. data/lib/servactory/tool_kit/dynamic_options/target.rb +4 -4
  73. data/lib/servactory/utils.rb +13 -3
  74. data/lib/servactory/version.rb +1 -1
  75. data/lib/servactory.rb +3 -4
  76. metadata +20 -4
@@ -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
  #
@@ -77,6 +107,15 @@ module Servactory
77
107
  include Concerns::ServiceClassValidation
78
108
  include Concerns::ErrorMessages
79
109
 
110
+ RESULT_TYPE_TO_METHOD = {
111
+ success: :succeeds,
112
+ failure: :fails,
113
+ call_original: :and_call_original,
114
+ wrap_original: :and_wrap_original
115
+ }.freeze
116
+
117
+ private_constant :RESULT_TYPE_TO_METHOD
118
+
80
119
  # @return [Class] The Servactory service class being mocked
81
120
  attr_reader :service_class
82
121
 
@@ -96,7 +135,7 @@ module Servactory
96
135
  @config = ServiceMockConfig.new(service_class:)
97
136
  @config.method_type = method_type
98
137
  @sequential_configs = []
99
- @executed = false
138
+ @stub = nil
100
139
  end
101
140
 
102
141
  # ============================================================
@@ -127,7 +166,7 @@ module Servactory
127
166
  validate_outputs!(outputs_hash)
128
167
  @config.result_type = :success
129
168
  @config.outputs = outputs_hash
130
- execute_or_re_execute_mock
169
+ execute_mock
131
170
  self
132
171
  end
133
172
 
@@ -157,7 +196,7 @@ module Servactory
157
196
 
158
197
  @config.result_type = :failure
159
198
  @config.exception = build_exception(exception_class, type:, message:, meta:)
160
- execute_or_re_execute_mock
199
+ execute_mock
161
200
  self
162
201
  end
163
202
 
@@ -168,6 +207,11 @@ module Servactory
168
207
  #
169
208
  # Inputs are automatically validated against service definition.
170
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
+ #
171
215
  # @param inputs_hash_or_matcher [Hash, Object] Service inputs to match or RSpec matcher
172
216
  # @return [ServiceMockBuilder] self for method chaining
173
217
  #
@@ -183,8 +227,73 @@ module Servactory
183
227
  # @raise [InputValidator::ValidationError] if inputs don't match service definition
184
228
  def with(inputs_hash_or_matcher)
185
229
  validate_inputs!(inputs_hash_or_matcher)
186
- @config.argument_matcher = inputs_hash_or_matcher
187
- re_execute_mock if @executed
230
+ all_configs.each { |config| config.argument_matcher = inputs_hash_or_matcher }
231
+ update_mock_arguments
232
+ self
233
+ end
234
+
235
+ # ============================================================
236
+ # RSpec Pass-Through API
237
+ # ============================================================
238
+
239
+ # Delegates to the original unmodified method.
240
+ # Useful for spy pattern or selective mocking.
241
+ #
242
+ # @return [ServiceMockBuilder] self for method chaining
243
+ #
244
+ # @example Pass-through (spy)
245
+ # allow_service(S).and_call_original
246
+ #
247
+ # @example Selective mocking with input matching
248
+ # allow_service(S).with(id: 1).and_call_original
249
+ def and_call_original
250
+ validate_not_in_sequential_mode!(:and_call_original)
251
+ validate_result_type_not_switched!(:and_call_original)
252
+
253
+ @config.result_type = :call_original
254
+ execute_mock
255
+ self
256
+ end
257
+
258
+ # Wraps the original method with custom logic.
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({})`.
264
+ #
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
269
+ # @return [ServiceMockBuilder] self for method chaining
270
+ #
271
+ # @example Modify result
272
+ # allow_service(S).and_wrap_original do |original, **inputs|
273
+ # result = original.call(**inputs)
274
+ # # custom logic
275
+ # result
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
289
+ def and_wrap_original(&block)
290
+ validate_block_given!(:and_wrap_original, block)
291
+ validate_not_in_sequential_mode!(:and_wrap_original)
292
+ validate_result_type_not_switched!(:and_wrap_original)
293
+
294
+ @config.result_type = :wrap_original
295
+ @config.wrap_block = block
296
+ execute_mock
188
297
  self
189
298
  end
190
299
 
@@ -208,6 +317,7 @@ module Servactory
208
317
  # @raise [ArgumentError] if called without first calling succeeds/fails
209
318
  # @raise [OutputValidator::ValidationError] if outputs don't match service definition
210
319
  def then_succeeds(outputs_hash = {})
320
+ validate_not_passthrough!(:then_succeeds)
211
321
  validate_result_type_defined!(:then_succeeds)
212
322
 
213
323
  validate_outputs!(outputs_hash)
@@ -216,7 +326,7 @@ module Servactory
216
326
  @config.result_type = :success
217
327
  @config.outputs = outputs_hash
218
328
  @config.method_type = @sequential_configs.last&.method_type || :call
219
- execute_sequential_mock
329
+ execute_mock
220
330
  self
221
331
  end
222
332
 
@@ -237,6 +347,7 @@ module Servactory
237
347
  #
238
348
  # @raise [ArgumentError] if called without first calling succeeds/fails
239
349
  def then_fails(exception_class = nil, type: :base, message:, meta: nil) # rubocop:disable Style/KeywordParametersOrder
350
+ validate_not_passthrough!(:then_fails)
240
351
  validate_result_type_defined!(:then_fails)
241
352
 
242
353
  finalize_current_to_sequence
@@ -244,7 +355,7 @@ module Servactory
244
355
  @config.result_type = :failure
245
356
  @config.exception = build_exception(exception_class, type:, message:, meta:)
246
357
  @config.method_type = @sequential_configs.last&.method_type || :call
247
- execute_sequential_mock
358
+ execute_mock
248
359
  self
249
360
  end
250
361
 
@@ -281,22 +392,51 @@ module Servactory
281
392
 
282
393
  # Validates that result type is not being switched.
283
394
  #
284
- # Prevents accidental switching between succeeds() and fails().
395
+ # Prevents accidental switching between different result types.
285
396
  #
286
- # @param new_type [Symbol] :succeeds or :fails
397
+ # @param new_type [Symbol] :succeeds, :fails, :and_call_original, or :and_wrap_original
287
398
  # @raise [ArgumentError] if trying to switch result type
288
399
  # @return [void]
289
- def validate_result_type_not_switched!(new_type) # rubocop:disable Metrics/CyclomaticComplexity
400
+ def validate_result_type_not_switched!(new_type)
290
401
  return unless @config.result_type_defined?
291
402
 
292
- current_type = @config.success? ? :succeeds : :fails
293
- return if (new_type == :succeeds && current_type == :succeeds) ||
294
- (new_type == :fails && current_type == :fails)
403
+ current_type = RESULT_TYPE_TO_METHOD[@config.result_type]
404
+ return if new_type == current_type
295
405
 
296
406
  raise ArgumentError,
297
407
  "Cannot call #{new_type}() after #{current_type}() was already called. " \
298
- "#{new_type == :succeeds ? 'succeeds()' : 'fails()'} replaces the result type, " \
299
- "which is likely a mistake. Create a new mock if you need different behavior."
408
+ "This replaces the result type, which is likely a mistake. " \
409
+ "Create a new mock if you need different behavior."
410
+ end
411
+
412
+ # Validates that pass-through methods are not mixed with sequential responses.
413
+ #
414
+ # @param method_name [Symbol] The method being called
415
+ # @raise [ArgumentError] if current config is a pass-through type
416
+ # @return [void]
417
+ def validate_not_passthrough!(method_name)
418
+ return unless @config.call_original? || @config.wrap_original?
419
+
420
+ passthrough = @config.call_original? ? :and_call_original : :and_wrap_original
421
+
422
+ raise ArgumentError,
423
+ "Cannot call #{method_name}() after #{passthrough}(). " \
424
+ "Pass-through methods are not compatible with sequential responses."
425
+ end
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) }."
300
440
  end
301
441
 
302
442
  # Validates outputs against service definition.
@@ -354,17 +494,6 @@ module Servactory
354
494
  # Mock Execution
355
495
  # ============================================================
356
496
 
357
- # Executes or re-executes mock depending on current state.
358
- #
359
- # @return [void]
360
- def execute_or_re_execute_mock
361
- if @executed
362
- re_execute_mock
363
- else
364
- execute_mock
365
- end
366
- end
367
-
368
497
  # Saves current config to sequential list.
369
498
  #
370
499
  # @return [void]
@@ -372,48 +501,54 @@ module Servactory
372
501
  @sequential_configs << @config.dup
373
502
  end
374
503
 
375
- # Executes the mock for the first time.
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.
376
515
  #
377
516
  # @return [void]
378
517
  def execute_mock
379
- return if @executed
380
-
381
- @executed = true
382
- MockExecutor.new(
383
- service_class:,
384
- configs: [@config],
385
- rspec_context: @rspec_context
386
- ).execute
518
+ @stub = mock_executor.execute(reconfigurable_stub)
387
519
  end
388
520
 
389
- # Re-executes the mock after configuration changes.
521
+ # Applies the current argument matcher to the registered stub.
522
+ #
523
+ # Registers a new stub if the registered one has already been invoked.
390
524
  #
391
525
  # @return [void]
392
- def re_execute_mock
393
- return unless @executed
526
+ def update_mock_arguments
527
+ return if @stub.nil?
394
528
 
395
- if @sequential_configs.any?
396
- execute_sequential_mock
529
+ if @stub.invoked?
530
+ execute_mock
397
531
  else
398
- MockExecutor.new(
399
- service_class:,
400
- configs: [@config],
401
- rspec_context: @rspec_context
402
- ).execute
532
+ mock_executor.update_arguments(@stub)
403
533
  end
404
534
  end
405
535
 
406
- # Executes the mock with all sequential configurations.
536
+ # Returns the registered stub unless it has already been invoked.
407
537
  #
408
- # @return [void]
409
- def execute_sequential_mock
410
- all_configs = @sequential_configs + [@config]
538
+ # @return [MockExecutor::Stub, nil] Stub that can be reconfigured in place
539
+ def reconfigurable_stub
540
+ @stub unless @stub&.invoked?
541
+ end
411
542
 
543
+ # Builds an executor for the current configs.
544
+ #
545
+ # @return [MockExecutor] Executor for all configs
546
+ def mock_executor
412
547
  MockExecutor.new(
413
548
  service_class:,
414
549
  configs: all_configs,
415
550
  rspec_context: @rspec_context
416
- ).execute
551
+ )
417
552
  end
418
553
  end
419
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!), outputs,
13
- # exceptions, and argument matchers. Used by ServiceMockBuilder and
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,18 +27,20 @@ module Servactory
27
27
  # ## Attributes
28
28
  #
29
29
  # - `service_class` - The service class being mocked
30
- # - `result_type` - :success or :failure
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,
38
39
  :method_type,
39
40
  :outputs,
40
41
  :exception,
41
- :argument_matcher
42
+ :argument_matcher,
43
+ :wrap_block
42
44
 
43
45
  # Creates a new mock configuration.
44
46
  #
@@ -51,6 +53,7 @@ module Servactory
51
53
  @outputs = {}
52
54
  @exception = nil
53
55
  @argument_matcher = nil
56
+ @wrap_block = nil
54
57
  end
55
58
 
56
59
  # Checks if this is a success mock.
@@ -67,6 +70,27 @@ module Servactory
67
70
  result_type == :failure
68
71
  end
69
72
 
73
+ # Checks if this is a call_original mock.
74
+ #
75
+ # @return [Boolean] True if result_type is :call_original
76
+ def call_original?
77
+ result_type == :call_original
78
+ end
79
+
80
+ # Checks if this is a wrap_original mock.
81
+ #
82
+ # @return [Boolean] True if result_type is :wrap_original
83
+ def wrap_original?
84
+ result_type == :wrap_original
85
+ end
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
+
70
94
  # Checks if this mocks the .call! method.
71
95
  #
72
96
  # @return [Boolean] True if method_type is :call!
@@ -97,29 +121,40 @@ module Servactory
97
121
 
98
122
  # Builds RSpec argument matcher from config.
99
123
  #
100
- # If no matcher specified, builds one from service input names.
124
+ # If no matcher specified, any arguments are accepted by RSpec
125
+ # and verified against the matcher from #build_inputs_matcher.
101
126
  #
102
127
  # @param rspec_context [Object] The RSpec test context
103
128
  # @return [Object] RSpec argument matcher
104
129
  def build_argument_matcher(rspec_context)
105
130
  return argument_matcher if argument_matcher.present?
106
131
 
107
- input_names = service_class.info.inputs.keys
108
- return rspec_context.no_args if input_names.empty?
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?
109
143
 
110
- input_names.to_h { |input_name| [input_name, rspec_context.anything] }
144
+ ServiceInputsMatcher.new(service_class.info.inputs)
111
145
  end
112
146
 
113
147
  # Creates a deep copy of this config.
114
148
  #
115
149
  # @return [ServiceMockConfig] New config with copied values
116
- def dup
150
+ def dup # rubocop:disable Metrics/AbcSize
117
151
  copy = self.class.new(service_class:)
118
152
  copy.result_type = result_type
119
153
  copy.method_type = method_type
120
154
  copy.outputs = outputs.dup
121
155
  copy.exception = exception
122
156
  copy.argument_matcher = argument_matcher
157
+ copy.wrap_block = wrap_block
123
158
  copy
124
159
  end
125
160
  end
@@ -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"