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
@@ -11,6 +11,9 @@ module Servactory
11
11
  # MockExecutor translates ServiceMockConfig objects into actual RSpec
12
12
  # stub setups using `allow(...).to receive(...)`. It handles single
13
13
  # and sequential call scenarios, applying appropriate return behaviors.
14
+ # A stub returned by a previous execution can be passed back to be
15
+ # reconfigured in place instead of registering another stub, as long
16
+ # as it has not been invoked.
14
17
  #
15
18
  # ## Usage
16
19
  #
@@ -22,12 +25,28 @@ module Servactory
22
25
  # configs: [config1, config2],
23
26
  # rspec_context: self
24
27
  # )
25
- # executor.execute
28
+ # stub = executor.execute
29
+ #
30
+ # # After configs change, while the stub has not been invoked
31
+ # executor.execute(stub)
32
+ # executor.update_arguments(stub)
26
33
  # ```
27
34
  #
35
+ # ## Argument Matching
36
+ #
37
+ # - **With `.with()`** - RSpec matches the arguments against the configured matcher
38
+ # - **Without `.with()`** - RSpec accepts any arguments, and ServiceInputsGuard
39
+ # verifies them against the service inputs before the stub responds
40
+ #
41
+ # RSpec responds with the most recently registered stub that matches a call.
42
+ # A stub without `.with()` matches every call, so it handles every later
43
+ # call of the stubbed method, and ServiceInputsGuard rejects invalid inputs
44
+ # instead of deferring to stubs registered before it.
45
+ #
28
46
  # ## Execution Strategies
29
47
  #
30
48
  # - **Single Config** - uses `and_return` or `and_raise` directly
49
+ # - **Pass-Through** - uses `and_wrap_original` to call the original or the wrap block
31
50
  # - **Sequential Returns** - uses `and_return(*values)` for multiple values
32
51
  # - **Sequential with Raises** - uses `and_invoke(*callables)` for mixed behavior
33
52
  #
@@ -36,9 +55,33 @@ module Servactory
36
55
  # Works with:
37
56
  # - ServiceMockConfig - provides configuration for each stub
38
57
  # - ServiceMockBuilder - creates executor with configs
58
+ # - ExceptionValidator - validates exceptions of the configs
59
+ # - ServiceInputsGuard - verifies inputs of calls accepted with any arguments
60
+ # - WrapBlockAdapter - adapts wrap blocks to the arguments of service calls
39
61
  # - RSpec Context - provides allow/receive/etc. methods
40
62
  class MockExecutor
41
- include Concerns::ErrorMessages
63
+ # Registered RSpec stub and the guard verifying the inputs it receives.
64
+ #
65
+ # @!attribute [r] message_expectation
66
+ # @return [RSpec::Mocks::MessageExpectation] The RSpec stub
67
+ # @!attribute [r] inputs_guard
68
+ # @return [ServiceInputsGuard] The guard called by the stub implementation
69
+ Stub = Data.define(:message_expectation, :inputs_guard) do
70
+ # Checks whether the stub has received a call.
71
+ #
72
+ # RSpec does not allow modifying an invoked stub.
73
+ #
74
+ # @return [Boolean] True if the stub has been invoked
75
+ def invoked?
76
+ inputs_guard.invoked?
77
+ end
78
+ end
79
+
80
+ CALL_ORIGINAL = lambda do |original, *arguments, &block|
81
+ original.call(*arguments, &block)
82
+ end.ruby2_keywords
83
+
84
+ private_constant :CALL_ORIGINAL
42
85
 
43
86
  # Creates a new mock executor.
44
87
  #
@@ -51,21 +94,37 @@ module Servactory
51
94
  @rspec_context = rspec_context
52
95
  end
53
96
 
54
- # Executes the stub setup based on configurations.
97
+ # Registers the stub, or reconfigures the stub from a previous execution.
55
98
  #
56
- # Validates all configs first, then applies appropriate stubbing
57
- # strategy (single or sequential).
99
+ # Validates all configs first, then applies the argument matcher and
100
+ # the appropriate return behavior (single or sequential).
58
101
  #
59
- # @return [void]
102
+ # @param stub [Stub, nil] Stub to reconfigure, which must not have been invoked
103
+ # @return [Stub] The configured stub
60
104
  # @raise [ArgumentError] If any config is invalid
61
- def execute
105
+ def execute(stub = nil)
62
106
  validate_configs!
63
107
 
108
+ stub = stub.nil? ? register_stub : update_arguments(stub)
109
+
64
110
  if sequential?
65
- execute_sequential
111
+ apply_sequential_behavior(stub.message_expectation)
66
112
  else
67
- execute_single
113
+ apply_return_behavior(stub, @configs.first)
68
114
  end
115
+
116
+ stub
117
+ end
118
+
119
+ # Replaces the argument matcher of a registered stub
120
+ # and the inputs matcher of its guard.
121
+ #
122
+ # @param stub [Stub] Stub to update, which must not have been invoked
123
+ # @return [Stub] The updated stub
124
+ def update_arguments(stub)
125
+ stub.message_expectation.with(argument_matcher)
126
+ stub.inputs_guard.inputs_matcher = @configs.first.build_inputs_matcher
127
+ stub
69
128
  end
70
129
 
71
130
  private
@@ -77,35 +136,50 @@ module Servactory
77
136
  @configs.size > 1
78
137
  end
79
138
 
80
- # Executes a single-config stub.
139
+ # Registers a new stub constrained by the argument matcher.
81
140
  #
82
- # @return [void]
83
- def execute_single
84
- config = @configs.first
85
- method_name = config.method_type
86
- arg_matcher = config.build_argument_matcher(@rspec_context)
141
+ # Return behaviors run after the guard, which is registered as the stub
142
+ # implementation. Pass-through behaviors replace the stub implementation,
143
+ # so they call the guard themselves.
144
+ #
145
+ # @return [Stub] The registered stub
146
+ def register_stub
147
+ inputs_guard = ServiceInputsGuard.new(service_class: @service_class, method_type:)
148
+ implementation = ->(*arguments) { inputs_guard.verify!(arguments) } unless @configs.first.pass_through?
87
149
 
88
150
  message_expectation = @rspec_context.allow(@service_class).to(
89
- @rspec_context.receive(method_name).with(arg_matcher)
151
+ @rspec_context.receive(method_type, &implementation)
90
152
  )
91
153
 
92
- apply_return_behavior(message_expectation, config)
154
+ update_arguments(Stub.new(message_expectation:, inputs_guard:))
93
155
  end
94
156
 
95
- # Executes a multi-config sequential stub.
157
+ # Returns the stubbed method shared by all configs.
96
158
  #
97
- # Chooses between and_return (for simple returns) and and_invoke
98
- # (when exceptions need to be raised).
159
+ # @return [Symbol] :call or :call!
160
+ def method_type
161
+ @configs.first.method_type
162
+ end
163
+
164
+ # Builds the argument matcher shared by all configs.
99
165
  #
100
- # @return [void]
101
- def execute_sequential
102
- method_name = @configs.first.method_type
103
- arg_matcher = @configs.first.build_argument_matcher(@rspec_context)
166
+ # @return [Object] RSpec argument matcher
167
+ def argument_matcher
168
+ @configs.first.build_argument_matcher(@rspec_context)
169
+ end
104
170
 
171
+ # Applies sequential return behavior to a message expectation.
172
+ #
173
+ # Uses and_return for simple returns and and_invoke when
174
+ # exceptions need to be raised.
175
+ #
176
+ # @param message_expectation [RSpec::Mocks::MessageExpectation] The stub to configure
177
+ # @return [void]
178
+ def apply_sequential_behavior(message_expectation)
105
179
  if all_returns?
106
- execute_sequential_returns(method_name, arg_matcher)
180
+ message_expectation.and_return(*@configs.map(&:build_result))
107
181
  else
108
- execute_sequential_invoke(method_name, arg_matcher)
182
+ message_expectation.and_invoke(*@configs.map { |config| build_callable(config) })
109
183
  end
110
184
  end
111
185
 
@@ -119,38 +193,6 @@ module Servactory
119
193
  @configs.none? { |config| config.failure? && config.bang_method? }
120
194
  end
121
195
 
122
- # Executes sequential stub with and_return for all values.
123
- #
124
- # @param method_name [Symbol] The method being stubbed
125
- # @param arg_matcher [Object] RSpec argument matcher
126
- # @return [void]
127
- def execute_sequential_returns(method_name, arg_matcher)
128
- returns = @configs.map(&:build_result)
129
-
130
- @rspec_context.allow(@service_class).to(
131
- @rspec_context.receive(method_name)
132
- .with(arg_matcher)
133
- .and_return(*returns)
134
- )
135
- end
136
-
137
- # Executes sequential stub with and_invoke for mixed behavior.
138
- #
139
- # Uses callables to handle both returns and raises.
140
- #
141
- # @param method_name [Symbol] The method being stubbed
142
- # @param arg_matcher [Object] RSpec argument matcher
143
- # @return [void]
144
- def execute_sequential_invoke(method_name, arg_matcher)
145
- callables = @configs.map { |config| build_callable(config) }
146
-
147
- @rspec_context.allow(@service_class).to(
148
- @rspec_context.receive(method_name)
149
- .with(arg_matcher)
150
- .and_invoke(*callables)
151
- )
152
- end
153
-
154
196
  # Builds a callable lambda for and_invoke.
155
197
  #
156
198
  # @param config [ServiceMockConfig] The config to build callable for
@@ -164,90 +206,48 @@ module Servactory
164
206
  end
165
207
  end
166
208
 
167
- # Applies return or raise behavior to a message expectation.
209
+ # Applies return, raise, or pass-through behavior to a stub.
168
210
  #
169
- # @param message_expectation [Object] RSpec message expectation
211
+ # @param stub [Stub] The stub to configure
170
212
  # @param config [ServiceMockConfig] Configuration with result/exception
171
213
  # @return [void]
172
- def apply_return_behavior(message_expectation, config)
173
- if config.failure? && config.bang_method?
214
+ def apply_return_behavior(stub, config)
215
+ message_expectation = stub.message_expectation
216
+
217
+ if config.pass_through?
218
+ message_expectation.and_wrap_original(&build_pass_through(stub.inputs_guard, config))
219
+ elsif config.failure? && config.bang_method?
174
220
  message_expectation.and_raise(config.exception)
175
221
  else
176
222
  message_expectation.and_return(config.build_result)
177
223
  end
178
224
  end
179
225
 
180
- # Validates all configurations before executing.
226
+ # Builds the and_wrap_original block for a pass-through config.
181
227
  #
182
- # @return [void]
183
- # @raise [ArgumentError] If any config is invalid
184
- def validate_configs!
185
- @configs.each { |config| validate_config!(config) }
186
- end
187
-
188
- # Validates a single configuration.
228
+ # Verifies the received inputs, then delegates to the original method
229
+ # or to the wrap block.
189
230
  #
190
- # @param config [ServiceMockConfig] The config to validate
191
- # @return [void]
192
- # @raise [ArgumentError] If config is invalid
193
- def validate_config!(config)
194
- validate_failure_has_exception!(config)
195
- validate_exception_type!(config)
196
- end
231
+ # @param inputs_guard [ServiceInputsGuard] Guard of the stub
232
+ # @param config [ServiceMockConfig] Pass-through configuration
233
+ # @return [Proc] Block for RSpec's and_wrap_original
234
+ def build_pass_through(inputs_guard, config)
235
+ delegate = config.wrap_original? ? WrapBlockAdapter.adapt(config.wrap_block) : CALL_ORIGINAL
197
236
 
198
- # Validates that failure configs have an exception.
199
- #
200
- # @param config [ServiceMockConfig] The config to validate
201
- # @return [void]
202
- # @raise [ArgumentError] If failure config is missing exception
203
- def validate_failure_has_exception!(config)
204
- return unless config.failure? && config.exception.nil?
237
+ pass_through = lambda do |original, *arguments, &block|
238
+ inputs_guard.verify!(arguments)
239
+ delegate.call(original, *arguments, &block)
240
+ end
205
241
 
206
- raise ArgumentError, missing_exception_for_failure_message(config.service_class)
242
+ pass_through.ruby2_keywords
207
243
  end
208
244
 
209
- # Validates that exception is the correct type for the service.
245
+ # Validates all configurations before executing.
210
246
  #
211
- # @param config [ServiceMockConfig] The config to validate
212
247
  # @return [void]
213
- # @raise [ArgumentError] If exception type is wrong
214
- def validate_exception_type!(config)
215
- return if config.exception.nil?
216
- return if valid_exception_type?(config)
217
-
218
- raise ArgumentError, invalid_exception_type_message(
219
- service_class: config.service_class,
220
- expected_class: failure_class_for(config),
221
- actual_class: config.exception.class
222
- )
223
- end
224
-
225
- # Checks if exception is the correct type.
226
- #
227
- # Uses different validation strategies based on method type:
228
- # - For `.call` (non-bang): Relaxed validation - accepts any Servactory::Exceptions::Failure subclass
229
- # because the exception is wrapped in Result and never raised, so type doesn't matter.
230
- # - For `.call!` (bang): Strict validation - requires the service's configured failure_class
231
- # because the exception IS raised and type matters for rescue clauses.
232
- #
233
- # @param config [ServiceMockConfig] The config to check
234
- # @return [Boolean] True if exception is valid type
235
- def valid_exception_type?(config)
236
- if config.bang_method?
237
- # Strict validation for call! - exception will be raised
238
- config.exception.is_a?(failure_class_for(config))
239
- else
240
- # Relaxed validation for call - exception is only wrapped in Result
241
- config.exception.is_a?(Servactory::Exceptions::Failure)
242
- end
243
- end
244
-
245
- # Returns the expected failure class for a service.
246
- #
247
- # @param config [ServiceMockConfig] The config with service class
248
- # @return [Class] The service's failure class
249
- def failure_class_for(config)
250
- config.service_class.config.failure_class
248
+ # @raise [ArgumentError] If any config is invalid
249
+ def validate_configs!
250
+ @configs.each { |config| ExceptionValidator.validate!(config) }
251
251
  end
252
252
  end
253
253
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Verifies the inputs of calls received by a service mock.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # RSpec compares the number of arguments before applying an argument
12
+ # matcher, so a stub constrained by ServiceInputsMatcher would reject
13
+ # a call without arguments. Service mocks configured without `.with()`
14
+ # therefore accept any arguments on the RSpec level, and the guard
15
+ # verifies them inside the stub implementation before the mock responds.
16
+ # Calls whose inputs the service would not accept raise the same error
17
+ # as an RSpec argument mismatch.
18
+ #
19
+ # Verification is skipped while no inputs matcher is set, which is the
20
+ # case for mocks constrained with `.with()`.
21
+ #
22
+ # The stub calls the guard on every call it receives, so the guard also
23
+ # tells whether the stub has been invoked. RSpec does not allow modifying
24
+ # an invoked stub.
25
+ #
26
+ # ## Usage
27
+ #
28
+ # Used internally by MockExecutor:
29
+ #
30
+ # ```ruby
31
+ # guard = ServiceInputsGuard.new(service_class: MyService, method_type: :call)
32
+ # guard.inputs_matcher = ServiceInputsMatcher.new(MyService.info.inputs)
33
+ #
34
+ # guard.verify!([{ user_id: 1 }])
35
+ # ```
36
+ class ServiceInputsGuard
37
+ # @return [ServiceInputsMatcher, nil] Matcher applied to received arguments, nil skips verification
38
+ attr_accessor :inputs_matcher
39
+
40
+ # Creates a guard without an inputs matcher.
41
+ #
42
+ # @param service_class [Class] The mocked service class
43
+ # @param method_type [Symbol] The mocked method (:call or :call!)
44
+ def initialize(service_class:, method_type:)
45
+ @service_class = service_class
46
+ @method_type = method_type
47
+ @inputs_matcher = nil
48
+ @invoked = false
49
+ end
50
+
51
+ # Checks whether the guarded stub has received a call.
52
+ #
53
+ # @return [Boolean] True if #verify! was called
54
+ def invoked?
55
+ @invoked
56
+ end
57
+
58
+ # Records the call and verifies the arguments received by the mock.
59
+ #
60
+ # @param arguments [Array<Object>] The arguments the service was called with
61
+ # @return [void]
62
+ # @raise [RSpec::Mocks::MockExpectationError] If the arguments do not match the inputs matcher
63
+ def verify!(arguments)
64
+ @invoked = true
65
+
66
+ return if @inputs_matcher.nil? || @inputs_matcher.args_match?(*arguments)
67
+
68
+ raise RSpec::Mocks::MockExpectationError, unexpected_arguments_message(arguments)
69
+ end
70
+
71
+ private
72
+
73
+ # Builds the message of RSpec's unexpected arguments error.
74
+ #
75
+ # @param arguments [Array<Object>] The arguments the service was called with
76
+ # @return [String] Error message with expected and received arguments
77
+ def unexpected_arguments_message(arguments)
78
+ <<~MESSAGE.chomp
79
+ #<#{@service_class.inspect} (class)> received #{@method_type.inspect} with unexpected arguments
80
+ expected: (#{@inputs_matcher.description})
81
+ got: #{format_arguments(arguments)}
82
+ MESSAGE
83
+ end
84
+
85
+ # Formats arguments the way RSpec does in failure messages.
86
+ #
87
+ # @param arguments [Array<Object>] The arguments the service was called with
88
+ # @return [String] Formatted argument list
89
+ def format_arguments(arguments)
90
+ return "(no args)" if arguments.empty?
91
+
92
+ "(#{arguments.map { |argument| RSpec::Support::ObjectFormatter.format(argument) }.join(', ')})"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Argument list matcher for inputs accepted by a service.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Default argument matcher for service mocks configured without `.with()`.
12
+ # A call matches when every required input is present and no undeclared
13
+ # inputs are passed. Optional inputs may be omitted, so a call without
14
+ # arguments matches when the service has no required inputs. Input values
15
+ # are not constrained.
16
+ #
17
+ # ## Usage
18
+ #
19
+ # Built internally by ServiceMockConfig when no argument matcher is set
20
+ # and applied to every call by ServiceInputsGuard:
21
+ #
22
+ # ```ruby
23
+ # allow_service(MyService).succeeds(result: "ok")
24
+ #
25
+ # MyService.call(user_id: 1)
26
+ # MyService.call({ user_id: 1, locale: "en" })
27
+ # ```
28
+ #
29
+ # Keyword arguments, a positional Hash and a Datory object are matched,
30
+ # as the service accepts them. Keys are symbolized the way the service
31
+ # does it, so String keys and HashWithIndifferentAccess are accepted
32
+ # as well.
33
+ class ServiceInputsMatcher
34
+ # Creates a matcher from service input definitions.
35
+ #
36
+ # @param inputs [Hash{Symbol => Hash}] Input definitions from `service_class.info.inputs`
37
+ def initialize(inputs)
38
+ @input_names = inputs.keys
39
+ @required_input_names = inputs.filter_map do |input_name, input|
40
+ input_name if input.fetch(:actor).required?
41
+ end
42
+ end
43
+
44
+ # Checks whether the call arguments contain acceptable service inputs.
45
+ #
46
+ # The inputs are passed as a single argument accepted by the service,
47
+ # a Hash or a Datory object. Input names are compared after converting
48
+ # it with Servactory::Utils.adapt.
49
+ #
50
+ # @param arguments [Array<Object>] The arguments the service was called with
51
+ # @return [Boolean] True if all required inputs are present and no unknown inputs are passed
52
+ def args_match?(*arguments)
53
+ return @required_input_names.empty? if arguments.empty?
54
+ return false unless arguments.one? && Servactory::Utils.adaptable?(arguments.first)
55
+
56
+ inputs = Servactory::Utils.adapt(arguments.first)
57
+
58
+ @required_input_names.all? { |input_name| inputs.key?(input_name) } &&
59
+ inputs.each_key.all? { |input_name| @input_names.include?(input_name) }
60
+ end
61
+
62
+ # Describes the matcher for RSpec failure messages.
63
+ #
64
+ # @return [String] Matcher description
65
+ def description
66
+ "service_inputs(required: #{@required_input_names.inspect}, " \
67
+ "optional: #{(@input_names - @required_input_names).inspect})"
68
+ end
69
+
70
+ alias inspect description
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end