servactory 2.16.1 → 3.0.0.rc2

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -9
  3. data/config/locales/de.yml +134 -0
  4. data/config/locales/en.yml +15 -0
  5. data/config/locales/es.yml +134 -0
  6. data/config/locales/fr.yml +134 -0
  7. data/config/locales/it.yml +134 -0
  8. data/config/locales/ru.yml +15 -0
  9. data/lib/generators/README.md +45 -0
  10. data/lib/generators/servactory/base.rb +82 -0
  11. data/lib/generators/servactory/extension/USAGE +54 -0
  12. data/lib/generators/servactory/extension/extension_generator.rb +41 -0
  13. data/lib/generators/servactory/extension/templates/extension.rb.tt +62 -0
  14. data/lib/generators/servactory/install/USAGE +27 -0
  15. data/lib/generators/servactory/install/install_generator.rb +94 -0
  16. data/lib/generators/servactory/{templates/services/application_service/base.rb → install/templates/application_service/base.rb.tt} +29 -19
  17. data/lib/generators/servactory/{templates/services/application_service/exceptions.rb → install/templates/application_service/exceptions.rb.tt} +1 -1
  18. data/lib/generators/servactory/{templates/services/application_service/result.rb → install/templates/application_service/result.rb.tt} +1 -1
  19. data/lib/generators/servactory/rspec/USAGE +46 -0
  20. data/lib/generators/servactory/rspec/rspec_generator.rb +95 -0
  21. data/lib/generators/servactory/rspec/templates/service_spec.rb.tt +58 -0
  22. data/lib/generators/servactory/service/USAGE +51 -0
  23. data/lib/generators/servactory/service/service_generator.rb +56 -0
  24. data/lib/generators/servactory/service/templates/service.rb.tt +22 -0
  25. data/lib/servactory/actions/collection.rb +56 -1
  26. data/lib/servactory/actions/dsl.rb +11 -11
  27. data/lib/servactory/actions/tools/rules.rb +1 -1
  28. data/lib/servactory/actions/tools/runner.rb +111 -28
  29. data/lib/servactory/base.rb +1 -7
  30. data/lib/servactory/configuration/actions/aliases/collection.rb +5 -0
  31. data/lib/servactory/configuration/actions/rescue_handlers/collection.rb +5 -0
  32. data/lib/servactory/configuration/actions/shortcuts/collection.rb +5 -0
  33. data/lib/servactory/configuration/collection_mode/class_names_collection.rb +5 -0
  34. data/lib/servactory/configuration/config.rb +36 -0
  35. data/lib/servactory/configuration/configurable.rb +95 -0
  36. data/lib/servactory/configuration/dsl.rb +3 -27
  37. data/lib/servactory/configuration/factory.rb +20 -20
  38. data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -0
  39. data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +5 -0
  40. data/lib/servactory/context/warehouse/inputs.rb +2 -2
  41. data/lib/servactory/context/workspace/inputs.rb +2 -2
  42. data/lib/servactory/context/workspace/internals.rb +2 -2
  43. data/lib/servactory/context/workspace/outputs.rb +2 -2
  44. data/lib/servactory/context/workspace.rb +11 -7
  45. data/lib/servactory/dsl.rb +10 -8
  46. data/lib/servactory/maintenance/attributes/tools/validation.rb +1 -1
  47. data/lib/servactory/result.rb +2 -2
  48. data/lib/servactory/test_kit/fake_type.rb +23 -0
  49. data/lib/servactory/test_kit/result.rb +45 -0
  50. data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
  51. data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
  52. data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
  53. data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
  54. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
  55. data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
  56. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
  57. data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
  58. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
  59. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
  60. data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
  61. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
  62. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
  63. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
  64. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
  65. data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
  66. data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
  67. data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
  68. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
  69. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
  70. data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
  71. data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
  72. data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
  73. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
  74. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
  75. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
  76. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
  77. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
  78. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
  79. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
  80. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
  81. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
  82. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
  83. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
  84. data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
  85. data/lib/servactory/test_kit/utils/faker.rb +62 -2
  86. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
  87. data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
  88. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
  89. data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
  90. data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
  91. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
  92. data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
  93. data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
  94. data/lib/servactory/tool_kit/dynamic_options/target.rb +248 -0
  95. data/lib/servactory/version.rb +4 -4
  96. data/lib/servactory.rb +6 -0
  97. metadata +57 -48
  98. data/lib/generators/servactory/install_generator.rb +0 -21
  99. data/lib/generators/servactory/rspec_generator.rb +0 -88
  100. data/lib/generators/servactory/service_generator.rb +0 -49
  101. data/lib/servactory/configuration/setup.rb +0 -97
  102. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
  103. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
  104. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
  105. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
  106. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
  107. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
  108. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
  109. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
  110. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
  111. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +0 -199
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Validates mock output values against service definitions.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Ensures that mocked output values match the service's output
12
+ # definitions in terms of names and types. Helps catch configuration
13
+ # errors early in tests.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Called automatically when `succeeds()` is used on builder:
18
+ #
19
+ # ```ruby
20
+ # allow_service(MyService)
21
+ # .succeeds(user: user)
22
+ # ```
23
+ #
24
+ # Can also be called directly:
25
+ #
26
+ # ```ruby
27
+ # OutputValidator.validate!(
28
+ # service_class: MyService,
29
+ # outputs: { user: user }
30
+ # )
31
+ # ```
32
+ #
33
+ # ## Validations
34
+ #
35
+ # 1. **Output names** - all provided outputs must be defined in service
36
+ # 2. **Output types** - values must match expected types (if defined)
37
+ class OutputValidator
38
+ include Concerns::ErrorMessages
39
+
40
+ # Error raised when validation fails
41
+ class ValidationError < StandardError; end
42
+
43
+ class << self
44
+ # Validates outputs and raises on failure.
45
+ #
46
+ # @param service_class [Class] The service class
47
+ # @param outputs [Hash] Output values to validate
48
+ # @raise [ValidationError] If validation fails
49
+ # @return [void]
50
+ def validate!(service_class:, outputs:)
51
+ new(service_class:, outputs:).validate!
52
+ end
53
+ end
54
+
55
+ # Creates a new validator instance.
56
+ #
57
+ # @param service_class [Class] The service class
58
+ # @param outputs [Hash] Output values to validate
59
+ # @return [OutputValidator] New validator
60
+ def initialize(service_class:, outputs:)
61
+ @service_class = service_class
62
+ @outputs = outputs
63
+ end
64
+
65
+ # Runs all validations.
66
+ #
67
+ # @raise [ValidationError] If any validation fails
68
+ # @return [void]
69
+ def validate!
70
+ validate_output_names!
71
+ validate_output_types!
72
+ end
73
+
74
+ private
75
+
76
+ # Validates all output names are defined in service.
77
+ #
78
+ # @raise [ValidationError] If unknown output names provided
79
+ # @return [void]
80
+ def validate_output_names!
81
+ defined_outputs = @service_class.info.outputs.keys
82
+ provided_outputs = @outputs.keys
83
+
84
+ unknown_outputs = provided_outputs - defined_outputs
85
+
86
+ return if unknown_outputs.empty?
87
+
88
+ raise ValidationError, unknown_outputs_message(
89
+ service_class: @service_class,
90
+ unknown_outputs:,
91
+ defined_outputs:
92
+ )
93
+ end
94
+
95
+ # Validates output values match expected types.
96
+ #
97
+ # @raise [ValidationError] If type mismatch found
98
+ # @return [void]
99
+ def validate_output_types! # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
100
+ @outputs.each do |name, value|
101
+ output_info = @service_class.info.outputs[name]
102
+ next unless output_info
103
+
104
+ expected_types = output_info[:types]
105
+ next if value.nil?
106
+ next if expected_types.nil? || expected_types.empty?
107
+ next if expected_types.any? { |type| value.is_a?(type) }
108
+
109
+ raise ValidationError, type_mismatch_message(
110
+ service_class: @service_class,
111
+ output_name: name,
112
+ expected_types:,
113
+ actual_value: value
114
+ )
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,422 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Fluent builder for configuring Servactory service mocks in RSpec tests.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # ServiceMockBuilder provides a fluent API for stubbing Servactory service calls
12
+ # in tests. It handles both success and failure scenarios, output configuration,
13
+ # input argument matching, and sequential call responses.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Basic success mock:
18
+ #
19
+ # ```ruby
20
+ # allow_service(MyService)
21
+ # .succeeds(result: "value")
22
+ # ```
23
+ #
24
+ # Failure mock:
25
+ #
26
+ # ```ruby
27
+ # allow_service(MyService)
28
+ # .fails(type: :base, message: "Error")
29
+ # ```
30
+ #
31
+ # Failure mock with custom exception class:
32
+ #
33
+ # ```ruby
34
+ # allow_service(MyService)
35
+ # .fails(CustomException, type: :base, message: "Error")
36
+ # ```
37
+ #
38
+ # Sequential returns (first call succeeds, second fails):
39
+ #
40
+ # ```ruby
41
+ # allow_service(MyService)
42
+ # .succeeds(count: 1)
43
+ # .then_succeeds(count: 2)
44
+ # .then_fails(type: :base, message: "Error")
45
+ # ```
46
+ #
47
+ # With input matching:
48
+ #
49
+ # ```ruby
50
+ # allow_service(MyService)
51
+ # .with(user_id: 123)
52
+ # .succeeds(user: user)
53
+ #
54
+ # # Or order doesn't matter:
55
+ # allow_service(MyService)
56
+ # .succeeds(user: user)
57
+ # .with(user_id: 123)
58
+ # ```
59
+ #
60
+ # ## Features
61
+ #
62
+ # - **Fluent API** - chainable methods for readable test setup
63
+ # - **Success/Failure** - configure expected result type in one method
64
+ # - **Exception Handling** - auto-creates exceptions with type, message, meta
65
+ # - **Input Matching** - match specific service inputs with `.with()`
66
+ # - **Sequential Responses** - different results for consecutive calls
67
+ # - **Automatic Validation** - validates inputs and outputs against service definition
68
+ #
69
+ # ## Architecture
70
+ #
71
+ # Works with:
72
+ # - ServiceMockConfig - holds mock configuration state
73
+ # - MockExecutor - executes the actual RSpec stubbing
74
+ # - OutputValidator - validates outputs match service definition
75
+ # - InputValidator - validates inputs match service definition
76
+ class ServiceMockBuilder # rubocop:disable Metrics/ClassLength
77
+ include Concerns::ServiceClassValidation
78
+ include Concerns::ErrorMessages
79
+
80
+ # @return [Class] The Servactory service class being mocked
81
+ attr_reader :service_class
82
+
83
+ # @return [ServiceMockConfig] Current mock configuration
84
+ attr_reader :config
85
+
86
+ # Creates a new service mock builder.
87
+ #
88
+ # @param service_class [Class] The Servactory service class to mock
89
+ # @param method_type [Symbol] The method to stub (:call or :call!)
90
+ # @param rspec_context [Object] The RSpec example context for stubbing
91
+ def initialize(service_class, method_type:, rspec_context:)
92
+ validate_service_class!(service_class)
93
+
94
+ @service_class = service_class
95
+ @rspec_context = rspec_context
96
+ @config = ServiceMockConfig.new(service_class:)
97
+ @config.method_type = method_type
98
+ @sequential_configs = []
99
+ @executed = false
100
+ end
101
+
102
+ # ============================================================
103
+ # Primary Fluent API
104
+ # ============================================================
105
+
106
+ # Configures the mock to return a successful result with outputs.
107
+ #
108
+ # Outputs are automatically validated against service definition.
109
+ #
110
+ # @param outputs_hash [Hash{Symbol => Object}] Output name-value pairs
111
+ # @return [ServiceMockBuilder] self for method chaining
112
+ #
113
+ # @example Basic success
114
+ # allow_service(PaymentService).succeeds(status: :completed)
115
+ #
116
+ # @example With input matching
117
+ # allow_service(PaymentService)
118
+ # .succeeds(transaction_id: "txn_123")
119
+ # .with(amount: 100)
120
+ #
121
+ # @raise [ArgumentError] if called after then_succeeds/then_fails
122
+ # @raise [OutputValidator::ValidationError] if outputs don't match service definition
123
+ def succeeds(outputs_hash = {})
124
+ validate_not_in_sequential_mode!(:succeeds)
125
+ validate_result_type_not_switched!(:succeeds)
126
+
127
+ validate_outputs!(outputs_hash)
128
+ @config.result_type = :success
129
+ @config.outputs = outputs_hash
130
+ execute_or_re_execute_mock
131
+ self
132
+ end
133
+
134
+ # Configures the mock to return a failure result with exception.
135
+ #
136
+ # @param exception_class [Class, nil] Exception class (default: service config.failure_class)
137
+ # @param type [Symbol] Error type (default: :base)
138
+ # @param message [String] Error message (required)
139
+ # @param meta [Object, nil] Optional metadata for the exception
140
+ # @return [ServiceMockBuilder] self for method chaining
141
+ #
142
+ # @example Minimal failure
143
+ # allow_service(PaymentService).fails(message: "Card declined")
144
+ #
145
+ # @example With type
146
+ # allow_service(PaymentService)
147
+ # .fails(type: :payment_declined, message: "Insufficient funds")
148
+ #
149
+ # @example With custom exception class
150
+ # allow_service(PaymentService)
151
+ # .fails(CustomException, type: :error, message: "Error")
152
+ #
153
+ # @raise [ArgumentError] if called after then_succeeds/then_fails
154
+ def fails(exception_class = nil, type: :base, message:, meta: nil) # rubocop:disable Style/KeywordParametersOrder
155
+ validate_not_in_sequential_mode!(:fails)
156
+ validate_result_type_not_switched!(:fails)
157
+
158
+ @config.result_type = :failure
159
+ @config.exception = build_exception(exception_class, type:, message:, meta:)
160
+ execute_or_re_execute_mock
161
+ self
162
+ end
163
+
164
+ # Configures input matching for the mock.
165
+ #
166
+ # Can be called at any position in the chain (before/after succeeds/fails,
167
+ # or after then_* methods). Applies to the entire mock chain.
168
+ #
169
+ # Inputs are automatically validated against service definition.
170
+ #
171
+ # @param inputs_hash_or_matcher [Hash, Object] Service inputs to match or RSpec matcher
172
+ # @return [ServiceMockBuilder] self for method chaining
173
+ #
174
+ # @example Exact match
175
+ # allow_service(S).with(amount: 100).succeeds(result: :ok)
176
+ #
177
+ # @example With matchers
178
+ # allow_service(S).with(including(amount: 100)).succeeds(result: :ok)
179
+ #
180
+ # @example Any position in chain
181
+ # allow_service(S).succeeds(result: :ok).with(amount: 100)
182
+ #
183
+ # @raise [InputValidator::ValidationError] if inputs don't match service definition
184
+ def with(inputs_hash_or_matcher)
185
+ validate_inputs!(inputs_hash_or_matcher)
186
+ @config.argument_matcher = inputs_hash_or_matcher
187
+ re_execute_mock if @executed
188
+ self
189
+ end
190
+
191
+ # ============================================================
192
+ # Sequential Call API
193
+ # ============================================================
194
+
195
+ # Adds a successful result for sequential call handling.
196
+ #
197
+ # Use for testing code that calls the same service multiple times.
198
+ # Outputs are automatically validated against service definition.
199
+ #
200
+ # @param outputs_hash [Hash{Symbol => Object}] Output name-value pairs
201
+ # @return [ServiceMockBuilder] self for method chaining
202
+ #
203
+ # @example Multiple successes
204
+ # allow_service(RetryService)
205
+ # .succeeds(status: :pending)
206
+ # .then_succeeds(status: :completed)
207
+ #
208
+ # @raise [ArgumentError] if called without first calling succeeds/fails
209
+ # @raise [OutputValidator::ValidationError] if outputs don't match service definition
210
+ def then_succeeds(outputs_hash = {})
211
+ validate_result_type_defined!(:then_succeeds)
212
+
213
+ validate_outputs!(outputs_hash)
214
+ finalize_current_to_sequence
215
+ @config = ServiceMockConfig.new(service_class:)
216
+ @config.result_type = :success
217
+ @config.outputs = outputs_hash
218
+ @config.method_type = @sequential_configs.last&.method_type || :call
219
+ execute_sequential_mock
220
+ self
221
+ end
222
+
223
+ # Adds a failure result for sequential call handling.
224
+ #
225
+ # Use for testing code that calls the same service multiple times.
226
+ #
227
+ # @param exception_class [Class, nil] Exception class (default: service config.failure_class)
228
+ # @param type [Symbol] Error type (default: :base)
229
+ # @param message [String] Error message (required)
230
+ # @param meta [Object, nil] Optional metadata for the exception
231
+ # @return [ServiceMockBuilder] self for method chaining
232
+ #
233
+ # @example Success then failure
234
+ # allow_service(RetryService)
235
+ # .succeeds(status: :pending)
236
+ # .then_fails(type: :timeout, message: "Request timed out")
237
+ #
238
+ # @raise [ArgumentError] if called without first calling succeeds/fails
239
+ def then_fails(exception_class = nil, type: :base, message:, meta: nil) # rubocop:disable Style/KeywordParametersOrder
240
+ validate_result_type_defined!(:then_fails)
241
+
242
+ finalize_current_to_sequence
243
+ @config = ServiceMockConfig.new(service_class:)
244
+ @config.result_type = :failure
245
+ @config.exception = build_exception(exception_class, type:, message:, meta:)
246
+ @config.method_type = @sequential_configs.last&.method_type || :call
247
+ execute_sequential_mock
248
+ self
249
+ end
250
+
251
+ private
252
+
253
+ # ============================================================
254
+ # Validation
255
+ # ============================================================
256
+
257
+ # Validates that we're not in sequential mode.
258
+ #
259
+ # @param method_name [Symbol] The method being called
260
+ # @raise [ArgumentError] if in sequential mode
261
+ # @return [void]
262
+ def validate_not_in_sequential_mode!(method_name)
263
+ return if @sequential_configs.empty?
264
+
265
+ raise ArgumentError,
266
+ "Cannot call #{method_name}() after then_succeeds/then_fails. " \
267
+ "Use then_succeeds() or then_fails() to add sequential responses."
268
+ end
269
+
270
+ # Validates that result type is defined.
271
+ #
272
+ # @param method_name [Symbol] The method being called
273
+ # @raise [ArgumentError] if result type is not defined
274
+ # @return [void]
275
+ def validate_result_type_defined!(method_name)
276
+ return if @config.result_type_defined?
277
+
278
+ raise ArgumentError,
279
+ "Cannot call #{method_name}() without first calling succeeds() or fails()."
280
+ end
281
+
282
+ # Validates that result type is not being switched.
283
+ #
284
+ # Prevents accidental switching between succeeds() and fails().
285
+ #
286
+ # @param new_type [Symbol] :succeeds or :fails
287
+ # @raise [ArgumentError] if trying to switch result type
288
+ # @return [void]
289
+ def validate_result_type_not_switched!(new_type) # rubocop:disable Metrics/CyclomaticComplexity
290
+ return unless @config.result_type_defined?
291
+
292
+ current_type = @config.success? ? :succeeds : :fails
293
+ return if (new_type == :succeeds && current_type == :succeeds) ||
294
+ (new_type == :fails && current_type == :fails)
295
+
296
+ raise ArgumentError,
297
+ "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."
300
+ end
301
+
302
+ # Validates outputs against service definition.
303
+ #
304
+ # @param outputs_hash [Hash] Outputs to validate
305
+ # @return [void]
306
+ # @raise [OutputValidator::ValidationError] If outputs don't match service definition
307
+ def validate_outputs!(outputs_hash)
308
+ return if outputs_hash.empty?
309
+
310
+ OutputValidator.validate!(
311
+ service_class:,
312
+ outputs: outputs_hash
313
+ )
314
+ end
315
+
316
+ # Validates inputs against service definition.
317
+ #
318
+ # @param inputs_matcher [Hash, Object] Inputs or matcher to validate
319
+ # @return [void]
320
+ # @raise [InputValidator::ValidationError] If inputs don't match service definition
321
+ def validate_inputs!(inputs_matcher)
322
+ InputValidator.validate!(
323
+ service_class:,
324
+ inputs_matcher:
325
+ )
326
+ end
327
+
328
+ # ============================================================
329
+ # Exception Building
330
+ # ============================================================
331
+
332
+ # Builds exception instance for failure mocks.
333
+ #
334
+ # Uses service's configured failure_class if no exception_class provided.
335
+ #
336
+ # @param exception_class [Class, nil] Exception class to use
337
+ # @param type [Symbol] Error type
338
+ # @param message [String] Error message
339
+ # @param meta [Object, nil] Optional metadata
340
+ # @return [Exception] The constructed exception instance
341
+ def build_exception(exception_class, type:, message:, meta:)
342
+ klass = exception_class || default_failure_class
343
+ klass.new(type:, message:, meta:)
344
+ end
345
+
346
+ # Gets the default failure class from service configuration.
347
+ #
348
+ # @return [Class] The service's configured failure class
349
+ def default_failure_class
350
+ service_class.config.failure_class
351
+ end
352
+
353
+ # ============================================================
354
+ # Mock Execution
355
+ # ============================================================
356
+
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
+ # Saves current config to sequential list.
369
+ #
370
+ # @return [void]
371
+ def finalize_current_to_sequence
372
+ @sequential_configs << @config.dup
373
+ end
374
+
375
+ # Executes the mock for the first time.
376
+ #
377
+ # @return [void]
378
+ 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
387
+ end
388
+
389
+ # Re-executes the mock after configuration changes.
390
+ #
391
+ # @return [void]
392
+ def re_execute_mock
393
+ return unless @executed
394
+
395
+ if @sequential_configs.any?
396
+ execute_sequential_mock
397
+ else
398
+ MockExecutor.new(
399
+ service_class:,
400
+ configs: [@config],
401
+ rspec_context: @rspec_context
402
+ ).execute
403
+ end
404
+ end
405
+
406
+ # Executes the mock with all sequential configurations.
407
+ #
408
+ # @return [void]
409
+ def execute_sequential_mock
410
+ all_configs = @sequential_configs + [@config]
411
+
412
+ MockExecutor.new(
413
+ service_class:,
414
+ configs: all_configs,
415
+ rspec_context: @rspec_context
416
+ ).execute
417
+ end
418
+ end
419
+ end
420
+ end
421
+ end
422
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Configuration object for a single service mock.
8
+ #
9
+ # ## Purpose
10
+ #
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.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # Usually created internally by ServiceMockBuilder:
19
+ #
20
+ # ```ruby
21
+ # config = ServiceMockConfig.new(service_class: MyService)
22
+ # config.result_type = :success
23
+ # config.outputs = { user: user }
24
+ # config.method_type = :call
25
+ # ```
26
+ #
27
+ # ## Attributes
28
+ #
29
+ # - `service_class` - The service class being mocked
30
+ # - `result_type` - :success or :failure
31
+ # - `method_type` - :call or :call!
32
+ # - `outputs` - Hash of output values
33
+ # - `exception` - Exception for failure mocks
34
+ # - `argument_matcher` - RSpec argument matcher or Hash
35
+ class ServiceMockConfig
36
+ attr_accessor :service_class,
37
+ :result_type,
38
+ :method_type,
39
+ :outputs,
40
+ :exception,
41
+ :argument_matcher
42
+
43
+ # Creates a new mock configuration.
44
+ #
45
+ # @param service_class [Class] The service class to mock
46
+ # @return [ServiceMockConfig] New config instance
47
+ def initialize(service_class:)
48
+ @service_class = service_class
49
+ @result_type = nil
50
+ @method_type = :call
51
+ @outputs = {}
52
+ @exception = nil
53
+ @argument_matcher = nil
54
+ end
55
+
56
+ # Checks if this is a success mock.
57
+ #
58
+ # @return [Boolean] True if result_type is :success
59
+ def success?
60
+ result_type == :success
61
+ end
62
+
63
+ # Checks if this is a failure mock.
64
+ #
65
+ # @return [Boolean] True if result_type is :failure
66
+ def failure?
67
+ result_type == :failure
68
+ end
69
+
70
+ # Checks if this mocks the .call! method.
71
+ #
72
+ # @return [Boolean] True if method_type is :call!
73
+ def bang_method?
74
+ method_type == :call!
75
+ end
76
+
77
+ # Checks if result type has been set.
78
+ #
79
+ # @return [Boolean] True if result_type is not nil
80
+ def result_type_defined?
81
+ !result_type.nil?
82
+ end
83
+
84
+ # Builds a Servactory::TestKit::Result from this config.
85
+ #
86
+ # @return [Servactory::TestKit::Result] Mock result object
87
+ def build_result
88
+ result_attrs = outputs.merge(service_class:)
89
+
90
+ if success?
91
+ Servactory::TestKit::Result.as_success(**result_attrs)
92
+ else
93
+ result_attrs[:exception] = exception if exception
94
+ Servactory::TestKit::Result.as_failure(**result_attrs)
95
+ end
96
+ end
97
+
98
+ # Builds RSpec argument matcher from config.
99
+ #
100
+ # If no matcher specified, builds one from service input names.
101
+ #
102
+ # @param rspec_context [Object] The RSpec test context
103
+ # @return [Object] RSpec argument matcher
104
+ def build_argument_matcher(rspec_context)
105
+ return argument_matcher if argument_matcher.present?
106
+
107
+ input_names = service_class.info.inputs.keys
108
+ return rspec_context.no_args if input_names.empty?
109
+
110
+ input_names.to_h { |input_name| [input_name, rspec_context.anything] }
111
+ end
112
+
113
+ # Creates a deep copy of this config.
114
+ #
115
+ # @return [ServiceMockConfig] New config with copied values
116
+ def dup
117
+ copy = self.class.new(service_class:)
118
+ copy.result_type = result_type
119
+ copy.method_type = method_type
120
+ copy.outputs = outputs.dup
121
+ copy.exception = exception
122
+ copy.argument_matcher = argument_matcher
123
+ copy
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end