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,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Validates mock input values against service definitions.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Ensures that mocked input arguments match the service's input
12
+ # definitions in terms of names. Called automatically when using
13
+ # `.with()` on the fluent builder.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Called automatically from ServiceMockBuilder:
18
+ #
19
+ # ```ruby
20
+ # allow_service(MyService)
21
+ # .with(user_id: 123)
22
+ # .succeeds(result: "ok")
23
+ # ```
24
+ #
25
+ # Can also be called directly:
26
+ #
27
+ # ```ruby
28
+ # InputValidator.validate!(
29
+ # service_class: MyService,
30
+ # inputs_matcher: { user_id: 123 }
31
+ # )
32
+ # ```
33
+ #
34
+ # ## Supported Matcher Types
35
+ #
36
+ # - Hash - validates all keys against service inputs
37
+ # - `including(hash)` - validates specified keys only
38
+ # - `any_inputs` - skips validation (accepts anything)
39
+ # - `no_inputs` - validates service has no required inputs
40
+ # - `excluding(hash)` - skips validation (cannot validate exclusion)
41
+ class InputValidator
42
+ include Concerns::ErrorMessages
43
+
44
+ # Error raised when validation fails
45
+ class ValidationError < StandardError; end
46
+
47
+ class << self
48
+ # Validates inputs and raises on failure.
49
+ #
50
+ # @param service_class [Class] The service class
51
+ # @param inputs_matcher [Hash, Object] Input values or matcher
52
+ # @raise [ValidationError] If validation fails
53
+ # @return [void]
54
+ def validate!(service_class:, inputs_matcher:)
55
+ new(service_class:, inputs_matcher:).validate!
56
+ end
57
+ end
58
+
59
+ # Creates a new validator instance.
60
+ #
61
+ # @param service_class [Class] The service class
62
+ # @param inputs_matcher [Hash, Object] Input values or matcher
63
+ # @return [InputValidator] New validator
64
+ def initialize(service_class:, inputs_matcher:)
65
+ @service_class = service_class
66
+ @inputs_matcher = inputs_matcher
67
+ end
68
+
69
+ # Runs validation based on matcher type.
70
+ #
71
+ # @raise [ValidationError] If validation fails
72
+ # @return [void]
73
+ def validate!
74
+ keys_to_validate = extract_keys_for_validation
75
+
76
+ return if keys_to_validate.nil?
77
+
78
+ if keys_to_validate == :no_args
79
+ validate_service_has_no_required_inputs!
80
+ else
81
+ validate_input_names!(keys_to_validate)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # Extracts keys to validate based on matcher type.
88
+ #
89
+ # @return [Array<Symbol>, Symbol, nil] Keys to validate, :no_args marker, or nil to skip
90
+ def extract_keys_for_validation # rubocop:disable Metrics/MethodLength
91
+ case @inputs_matcher
92
+ when Hash
93
+ @inputs_matcher.keys
94
+ when RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher
95
+ extract_hash_including_keys
96
+ when RSpec::Mocks::ArgumentMatchers::NoArgsMatcher
97
+ :no_args
98
+ when RSpec::Mocks::ArgumentMatchers::AnyArgMatcher,
99
+ RSpec::Mocks::ArgumentMatchers::HashExcludingMatcher
100
+ nil
101
+ end
102
+ end
103
+
104
+ # Extracts keys from HashIncludingMatcher.
105
+ #
106
+ # @return [Array<Symbol>, nil] Keys from the matcher or nil
107
+ def extract_hash_including_keys
108
+ @inputs_matcher.instance_variable_get(:@expected)&.keys
109
+ rescue StandardError
110
+ nil
111
+ end
112
+
113
+ # Validates all input names are defined in service.
114
+ #
115
+ # @param provided_keys [Array<Symbol>] Keys to validate
116
+ # @raise [ValidationError] If unknown input names provided
117
+ # @return [void]
118
+ def validate_input_names!(provided_keys)
119
+ defined_inputs = @service_class.info.inputs.keys
120
+ unknown_inputs = provided_keys - defined_inputs
121
+
122
+ return if unknown_inputs.empty?
123
+
124
+ raise ValidationError, unknown_inputs_message(
125
+ service_class: @service_class,
126
+ unknown_inputs:,
127
+ defined_inputs:
128
+ )
129
+ end
130
+
131
+ # Validates service has no required inputs when no_inputs used.
132
+ #
133
+ # @raise [ValidationError] If service has required inputs
134
+ # @return [void]
135
+ def validate_service_has_no_required_inputs!
136
+ required_inputs = @service_class.info.inputs.reject { |_, v| v[:required] == false }.keys
137
+
138
+ return if required_inputs.empty?
139
+
140
+ raise ValidationError, no_inputs_but_required_message(
141
+ service_class: @service_class,
142
+ required_inputs:
143
+ )
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Backward-compatible API for mocking Servactory services in RSpec tests.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Provides legacy helper methods for mocking Servactory service calls.
12
+ # These methods are maintained for backward compatibility with existing tests.
13
+ # For new tests, consider using the fluent API via {Fluent} module.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Include in RSpec configuration via the main Helpers module:
18
+ #
19
+ # ```ruby
20
+ # RSpec.configure do |config|
21
+ # config.include Servactory::TestKit::Rspec::Helpers, type: :service
22
+ # end
23
+ # ```
24
+ #
25
+ # ## Available Methods
26
+ #
27
+ # - `allow_service_as_success!` - mock `.call!` method for success
28
+ # - `allow_service_as_success` - mock `.call` method for success
29
+ # - `allow_service_as_failure!` - mock `.call!` method for failure
30
+ # - `allow_service_as_failure` - mock `.call` method for failure
31
+ #
32
+ # ## Examples
33
+ #
34
+ # ```ruby
35
+ # # Mock call method (returns Result)
36
+ # allow_service_as_success(PaymentService) do
37
+ # { transaction_id: "txn_123" }
38
+ # end
39
+ #
40
+ # # Mock call! method (raises on failure)
41
+ # allow_service_as_success!(PaymentService, with: { amount: 100 }) do
42
+ # { transaction_id: "txn_123" }
43
+ # end
44
+ #
45
+ # # Mock failure
46
+ # allow_service_as_failure(PaymentService) do
47
+ # {
48
+ # exception: ApplicationService::Exceptions::Failure.new(
49
+ # type: :base,
50
+ # message: "Failed"
51
+ # )
52
+ # }
53
+ # end
54
+ # ```
55
+ #
56
+ # @see Fluent for the recommended fluent API
57
+ module Legacy
58
+ # Mock the `call!` method to return a successful result.
59
+ #
60
+ # @param service_class [Class] The service class to mock
61
+ # @param with [Hash, nil] Expected arguments matcher
62
+ # @yield Block returning Hash of output attributes
63
+ # @return [void]
64
+ #
65
+ # @example Basic success mock
66
+ # allow_service_as_success!(PaymentService) do
67
+ # { transaction_id: "txn_123" }
68
+ # end
69
+ #
70
+ # @example With argument matcher
71
+ # allow_service_as_success!(PaymentService, with: { amount: 100 }) do
72
+ # { transaction_id: "txn_123" }
73
+ # end
74
+ #
75
+ def allow_service_as_success!(service_class, with: nil, &block)
76
+ allow_service_legacy!(service_class, :as_success, with:, &block)
77
+ end
78
+
79
+ # Mock the `call` method to return a successful result.
80
+ #
81
+ # @param service_class [Class] The service class to mock
82
+ # @param with [Hash, nil] Expected arguments matcher
83
+ # @yield Block returning Hash of output attributes
84
+ # @return [void]
85
+ #
86
+ # @example Basic success mock
87
+ # allow_service_as_success(PaymentService) do
88
+ # { transaction_id: "txn_123" }
89
+ # end
90
+ #
91
+ def allow_service_as_success(service_class, with: nil, &block)
92
+ allow_service_legacy(service_class, :as_success, with:, &block)
93
+ end
94
+
95
+ # Mock the `call!` method to raise a failure exception.
96
+ #
97
+ # @param service_class [Class] The service class to mock
98
+ # @param with [Hash, nil] Expected arguments matcher
99
+ # @yield Block returning an exception or Hash with `:exception` key
100
+ # @return [void]
101
+ #
102
+ # @example Failure mock
103
+ # allow_service_as_failure!(PaymentService) do
104
+ # ApplicationService::Exceptions::Failure.new(
105
+ # type: :payment_declined,
106
+ # message: "Card declined"
107
+ # )
108
+ # end
109
+ #
110
+ def allow_service_as_failure!(service_class, with: nil, &block)
111
+ allow_service_legacy!(service_class, :as_failure, with:, &block)
112
+ end
113
+
114
+ # Mock the `call` method to return a failure result.
115
+ #
116
+ # @param service_class [Class] The service class to mock
117
+ # @param with [Hash, nil] Expected arguments matcher
118
+ # @yield Block returning an exception or Hash with `:exception` key
119
+ # @return [void]
120
+ #
121
+ # @example Failure mock with exception in hash
122
+ # allow_service_as_failure(PaymentService) do
123
+ # {
124
+ # exception: ApplicationService::Exceptions::Failure.new(
125
+ # type: :base,
126
+ # message: "Failed"
127
+ # )
128
+ # }
129
+ # end
130
+ #
131
+ def allow_service_as_failure(service_class, with: nil, &block)
132
+ allow_service_legacy(service_class, :as_failure, with:, &block)
133
+ end
134
+
135
+ private
136
+
137
+ # Builds legacy mock for .call! method.
138
+ #
139
+ # @param service_class [Class] The service class to mock
140
+ # @param result_type [Symbol] :as_success or :as_failure
141
+ # @param with [Hash, nil] Argument matcher
142
+ # @return [void]
143
+ def allow_service_legacy!(service_class, result_type, with: nil, &block)
144
+ allow_servactory_legacy(service_class, :call!, result_type, with:, &block)
145
+ end
146
+
147
+ # Builds legacy mock for .call method.
148
+ #
149
+ # @param service_class [Class] The service class to mock
150
+ # @param result_type [Symbol] :as_success or :as_failure
151
+ # @param with [Hash, nil] Argument matcher
152
+ # @return [void]
153
+ def allow_service_legacy(service_class, result_type, with: nil, &block)
154
+ allow_servactory_legacy(service_class, :call, result_type, with:, &block)
155
+ end
156
+
157
+ # Core legacy mock implementation.
158
+ #
159
+ # @param service_class [Class] The service class to mock
160
+ # @param method_call [Symbol] :call or :call!
161
+ # @param result_type [Symbol] :as_success or :as_failure
162
+ # @param with [Hash, nil] Argument matcher
163
+ # @return [void]
164
+ def allow_servactory_legacy(service_class, method_call, result_type, with: nil)
165
+ config = build_legacy_config(service_class, method_call, result_type, with, block_given? ? yield : nil)
166
+
167
+ Helpers::MockExecutor.new(
168
+ service_class:,
169
+ configs: [config],
170
+ rspec_context: self
171
+ ).execute
172
+ end
173
+
174
+ # Builds ServiceMockConfig from legacy parameters.
175
+ #
176
+ # @param service_class [Class] The service class
177
+ # @param method_call [Symbol] :call or :call!
178
+ # @param result_type [Symbol] :as_success or :as_failure
179
+ # @param with_arg [Hash, nil] Argument matcher
180
+ # @param block_result [Hash, Object, nil] Block return value
181
+ # @return [ServiceMockConfig] Configured mock config
182
+ def build_legacy_config(service_class, method_call, result_type, with_arg, block_result)
183
+ config = Helpers::ServiceMockConfig.new(service_class:)
184
+ config.method_type = method_call.to_sym
185
+ config.result_type = result_type == :as_success ? :success : :failure
186
+ config.argument_matcher = with_arg
187
+
188
+ process_legacy_block_result(config, block_result) if block_result
189
+
190
+ config
191
+ end
192
+
193
+ # Processes block result into config outputs/exception.
194
+ #
195
+ # @param config [ServiceMockConfig] The config to update
196
+ # @param block_result [Hash, Object] Block return value
197
+ # @return [void]
198
+ def process_legacy_block_result(config, block_result) # rubocop:disable Metrics/MethodLength
199
+ validate_legacy_block_result!(block_result, config.success?)
200
+
201
+ if block_result.is_a?(Hash)
202
+ if config.success?
203
+ config.outputs = block_result
204
+ else
205
+ config.exception = block_result[:exception] if block_result[:exception]
206
+ config.outputs = block_result.except(:exception)
207
+ end
208
+ else
209
+ config.exception = block_result
210
+ end
211
+ end
212
+
213
+ # Validates block result format for legacy API.
214
+ #
215
+ # @param block_result [Object] The block return value
216
+ # @param is_success [Boolean] Whether this is a success mock
217
+ # @raise [ArgumentError] If success mock block doesn't return Hash
218
+ # @return [void]
219
+ def validate_legacy_block_result!(block_result, is_success)
220
+ return unless is_success && !block_result.is_a?(Hash)
221
+
222
+ raise ArgumentError, "Invalid value for block. Must be a Hash with attributes."
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Executes RSpec stubbing based on mock configurations.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # MockExecutor translates ServiceMockConfig objects into actual RSpec
12
+ # stub setups using `allow(...).to receive(...)`. It handles single
13
+ # and sequential call scenarios, applying appropriate return behaviors.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Typically used internally by ServiceMockBuilder:
18
+ #
19
+ # ```ruby
20
+ # executor = MockExecutor.new(
21
+ # service_class: MyService,
22
+ # configs: [config1, config2],
23
+ # rspec_context: self
24
+ # )
25
+ # executor.execute
26
+ # ```
27
+ #
28
+ # ## Execution Strategies
29
+ #
30
+ # - **Single Config** - uses `and_return` or `and_raise` directly
31
+ # - **Sequential Returns** - uses `and_return(*values)` for multiple values
32
+ # - **Sequential with Raises** - uses `and_invoke(*callables)` for mixed behavior
33
+ #
34
+ # ## Architecture
35
+ #
36
+ # Works with:
37
+ # - ServiceMockConfig - provides configuration for each stub
38
+ # - ServiceMockBuilder - creates executor with configs
39
+ # - RSpec Context - provides allow/receive/etc. methods
40
+ class MockExecutor
41
+ include Concerns::ErrorMessages
42
+
43
+ # Creates a new mock executor.
44
+ #
45
+ # @param service_class [Class] The Servactory service class to stub
46
+ # @param configs [Array<ServiceMockConfig>] Configurations for each call
47
+ # @param rspec_context [Object] RSpec example context with stubbing methods
48
+ def initialize(service_class:, configs:, rspec_context:)
49
+ @service_class = service_class
50
+ @configs = configs
51
+ @rspec_context = rspec_context
52
+ end
53
+
54
+ # Executes the stub setup based on configurations.
55
+ #
56
+ # Validates all configs first, then applies appropriate stubbing
57
+ # strategy (single or sequential).
58
+ #
59
+ # @return [void]
60
+ # @raise [ArgumentError] If any config is invalid
61
+ def execute
62
+ validate_configs!
63
+
64
+ if sequential?
65
+ execute_sequential
66
+ else
67
+ execute_single
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # Checks if this is a sequential mock (multiple configs).
74
+ #
75
+ # @return [Boolean] True if more than one config
76
+ def sequential?
77
+ @configs.size > 1
78
+ end
79
+
80
+ # Executes a single-config stub.
81
+ #
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)
87
+
88
+ message_expectation = @rspec_context.allow(@service_class).to(
89
+ @rspec_context.receive(method_name).with(arg_matcher)
90
+ )
91
+
92
+ apply_return_behavior(message_expectation, config)
93
+ end
94
+
95
+ # Executes a multi-config sequential stub.
96
+ #
97
+ # Chooses between and_return (for simple returns) and and_invoke
98
+ # (when exceptions need to be raised).
99
+ #
100
+ # @return [void]
101
+ def execute_sequential
102
+ method_name = @configs.first.method_type
103
+ arg_matcher = @configs.first.build_argument_matcher(@rspec_context)
104
+
105
+ if all_returns?
106
+ execute_sequential_returns(method_name, arg_matcher)
107
+ else
108
+ execute_sequential_invoke(method_name, arg_matcher)
109
+ end
110
+ end
111
+
112
+ # Checks if all configs can use simple and_return.
113
+ #
114
+ # Returns false if any config is a failure with bang method,
115
+ # which requires raising an exception.
116
+ #
117
+ # @return [Boolean] True if all configs are simple returns
118
+ def all_returns?
119
+ @configs.none? { |config| config.failure? && config.bang_method? }
120
+ end
121
+
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
+ # Builds a callable lambda for and_invoke.
155
+ #
156
+ # @param config [ServiceMockConfig] The config to build callable for
157
+ # @return [Proc] Lambda that returns result or raises exception
158
+ def build_callable(config)
159
+ if config.failure? && config.bang_method?
160
+ ->(*_args) { raise config.exception }
161
+ else
162
+ result = config.build_result
163
+ ->(*_args) { result }
164
+ end
165
+ end
166
+
167
+ # Applies return or raise behavior to a message expectation.
168
+ #
169
+ # @param message_expectation [Object] RSpec message expectation
170
+ # @param config [ServiceMockConfig] Configuration with result/exception
171
+ # @return [void]
172
+ def apply_return_behavior(message_expectation, config)
173
+ if config.failure? && config.bang_method?
174
+ message_expectation.and_raise(config.exception)
175
+ else
176
+ message_expectation.and_return(config.build_result)
177
+ end
178
+ end
179
+
180
+ # Validates all configurations before executing.
181
+ #
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.
189
+ #
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
197
+
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?
205
+
206
+ raise ArgumentError, missing_exception_for_failure_message(config.service_class)
207
+ end
208
+
209
+ # Validates that exception is the correct type for the service.
210
+ #
211
+ # @param config [ServiceMockConfig] The config to validate
212
+ # @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
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end