servactory 2.16.1 → 3.0.0.rc1

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 (128) 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/stroma/dsl.rb +118 -0
  49. data/lib/servactory/stroma/entry.rb +32 -0
  50. data/lib/servactory/stroma/exceptions/base.rb +45 -0
  51. data/lib/servactory/stroma/exceptions/invalid_hook_type.rb +29 -0
  52. data/lib/servactory/stroma/exceptions/key_already_registered.rb +32 -0
  53. data/lib/servactory/stroma/exceptions/registry_frozen.rb +33 -0
  54. data/lib/servactory/stroma/exceptions/registry_not_finalized.rb +33 -0
  55. data/lib/servactory/stroma/exceptions/unknown_hook_target.rb +39 -0
  56. data/lib/servactory/stroma/hooks/applier.rb +63 -0
  57. data/lib/servactory/stroma/hooks/collection.rb +103 -0
  58. data/lib/servactory/stroma/hooks/factory.rb +80 -0
  59. data/lib/servactory/stroma/hooks/hook.rb +74 -0
  60. data/lib/servactory/stroma/registry.rb +94 -0
  61. data/lib/servactory/stroma/settings/collection.rb +90 -0
  62. data/lib/servactory/stroma/settings/registry_settings.rb +88 -0
  63. data/lib/servactory/stroma/settings/setting.rb +113 -0
  64. data/lib/servactory/stroma/state.rb +59 -0
  65. data/lib/servactory/test_kit/fake_type.rb +23 -0
  66. data/lib/servactory/test_kit/result.rb +45 -0
  67. data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
  68. data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
  69. data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
  70. data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
  71. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
  72. data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
  73. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
  74. data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
  75. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
  76. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
  77. data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
  78. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
  79. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
  80. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
  81. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
  82. data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
  83. data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
  84. data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
  85. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
  86. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
  87. data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
  88. data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
  89. data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
  90. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
  91. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
  92. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
  93. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
  94. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
  95. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
  96. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
  97. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
  98. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
  99. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
  100. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
  101. data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
  102. data/lib/servactory/test_kit/utils/faker.rb +62 -2
  103. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
  104. data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
  105. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
  106. data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
  107. data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
  108. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
  109. data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
  110. data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
  111. data/lib/servactory/tool_kit/dynamic_options/target.rb +252 -0
  112. data/lib/servactory/version.rb +4 -4
  113. data/lib/servactory.rb +4 -0
  114. metadata +73 -19
  115. data/lib/generators/servactory/install_generator.rb +0 -21
  116. data/lib/generators/servactory/rspec_generator.rb +0 -88
  117. data/lib/generators/servactory/service_generator.rb +0 -49
  118. data/lib/servactory/configuration/setup.rb +0 -97
  119. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
  120. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
  121. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
  122. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
  123. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
  124. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
  125. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
  126. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
  127. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
  128. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +0 -199
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ # RSpec matcher for validating service result output values.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Validates that a service result contains an expected output value with
12
+ # specific type, nested attributes, and content. Unlike input/internal
13
+ # matchers that validate definitions, this matcher validates actual runtime
14
+ # output values on a service result object.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # ```ruby
19
+ # RSpec.describe MyService, type: :service do
20
+ # let(:result) { described_class.call(user_id: 123) }
21
+ #
22
+ # it "returns expected output" do
23
+ # expect(result).to have_service_output(:user)
24
+ # .instance_of(User)
25
+ # .contains(name: "John")
26
+ # end
27
+ #
28
+ # it "validates nested attributes" do
29
+ # expect(result).to have_service_output(:data)
30
+ # .nested(:settings, :theme)
31
+ # .contains("dark")
32
+ # end
33
+ # end
34
+ # ```
35
+ #
36
+ # ## Chain Methods
37
+ #
38
+ # - `.instance_of(Class)` - validates output is instance of class
39
+ # - `.nested(*methods)` - traverses nested attributes before comparison
40
+ # - `.contains(value)` - validates output value or structure
41
+ #
42
+ # ## Value Comparison
43
+ #
44
+ # The `.contains` method uses type-aware comparison:
45
+ # - Array - uses RSpec's `contain_exactly`
46
+ # - Hash - uses RSpec's `match`
47
+ # - Boolean - uses RSpec's `equal` (identity)
48
+ # - nil - uses RSpec's `be_nil`
49
+ # - Other - uses RSpec's `eq` (equality)
50
+ class HaveServiceOutputMatcher # rubocop:disable Metrics/ClassLength
51
+ include RSpec::Matchers::Composable
52
+
53
+ # Creates a new output matcher for the given output name.
54
+ #
55
+ # @param output_name [Symbol] The name of the output to validate
56
+ # @return [HaveServiceOutputMatcher] New matcher instance
57
+ def initialize(output_name)
58
+ @output_name = output_name
59
+ @instance_of_class = nil
60
+ @nested_methods = []
61
+ @expected_value = nil
62
+ @value_defined = false
63
+ end
64
+
65
+ # Indicates this matcher does not support block expectations.
66
+ #
67
+ # @return [Boolean] Always false
68
+ def supports_block_expectations?
69
+ false
70
+ end
71
+
72
+ # Performs the match against the actual service result.
73
+ #
74
+ # @param actual [Servactory::Result] The service result to validate
75
+ # @return [Boolean] True if all checks pass
76
+ def matches?(actual)
77
+ @actual = actual
78
+ @given_value = actual.public_send(output_name)
79
+
80
+ check_instance_of && check_nested && check_contains
81
+ end
82
+
83
+ # Returns a description of what this matcher validates.
84
+ #
85
+ # @return [String] Human-readable matcher description
86
+ def description
87
+ "service output #{output_name}"
88
+ end
89
+
90
+ # Returns the failure message when the match fails.
91
+ #
92
+ # @return [String] Detailed failure message from RSpec matcher
93
+ def failure_message
94
+ match_for_failure
95
+ end
96
+
97
+ # Returns the failure message for negated expectations.
98
+ #
99
+ # @return [String] Negated failure message
100
+ def failure_message_when_negated
101
+ "Expected result not to have output #{output_name}"
102
+ end
103
+
104
+ # Chain Methods
105
+ # -------------
106
+
107
+ # Specifies the expected class of the output value.
108
+ #
109
+ # @param class_or_name [Class, String] Expected class or class name
110
+ # @return [self] For method chaining
111
+ def instance_of(class_or_name)
112
+ @instance_of_class = Servactory::Utils.constantize_class(class_or_name)
113
+ self
114
+ end
115
+
116
+ # Specifies nested method chain to traverse before comparison.
117
+ #
118
+ # Allows validating deeply nested attributes by chaining method calls
119
+ # on the output value before performing the final comparison.
120
+ #
121
+ # @param methods [Array<Symbol>] Method names to call in sequence
122
+ # @return [self] For method chaining
123
+ #
124
+ # @example Validate nested attribute
125
+ # expect(result).to have_output(:user).nested(:profile, :settings).contains(theme: "dark")
126
+ def nested(*methods)
127
+ @nested_methods = methods
128
+ self
129
+ end
130
+
131
+ # Specifies the expected value or structure of the output.
132
+ #
133
+ # @param value [Object] Expected value (uses type-aware comparison)
134
+ # @return [self] For method chaining
135
+ def contains(value)
136
+ @expected_value = value
137
+ @value_defined = true
138
+ self
139
+ end
140
+
141
+ private
142
+
143
+ attr_reader :output_name,
144
+ :actual,
145
+ :given_value,
146
+ :instance_of_class,
147
+ :nested_methods,
148
+ :expected_value
149
+
150
+ # Validates output value is an instance of expected class.
151
+ #
152
+ # @return [Boolean] True if class check passes or no class specified
153
+ def check_instance_of # rubocop:disable Naming/PredicateMethod
154
+ return true unless instance_of_class
155
+
156
+ matcher = RSpec::Matchers::BuiltIn::BeAnInstanceOf.new(instance_of_class)
157
+ matcher.matches?(@given_value)
158
+ end
159
+
160
+ # Traverses nested methods to get the value for comparison.
161
+ #
162
+ # @return [Boolean] Always true after traversing
163
+ def check_nested # rubocop:disable Naming/PredicateMethod
164
+ return true if nested_methods.empty?
165
+
166
+ nested_methods.each do |method_name|
167
+ next unless @given_value.respond_to?(method_name)
168
+
169
+ @given_value = @given_value.public_send(method_name)
170
+ end
171
+ true
172
+ end
173
+
174
+ # Validates output value matches expected using type-aware comparison.
175
+ #
176
+ # @return [Boolean] True if value matches or no value specified
177
+ def check_contains # rubocop:disable Metrics/MethodLength, Naming/PredicateMethod
178
+ return true unless @value_defined
179
+
180
+ matcher = case expected_value
181
+ when Array
182
+ RSpec::Matchers::BuiltIn::ContainExactly.new(expected_value)
183
+ when Hash
184
+ RSpec::Matchers::BuiltIn::Match.new(expected_value)
185
+ when TrueClass, FalseClass
186
+ RSpec::Matchers::BuiltIn::Equal.new(expected_value)
187
+ when NilClass
188
+ RSpec::Matchers::BuiltIn::BeNil.new(expected_value)
189
+ else
190
+ RSpec::Matchers::BuiltIn::Eq.new(expected_value)
191
+ end
192
+
193
+ matcher.matches?(@given_value)
194
+ end
195
+
196
+ # Builds detailed failure message by re-running all checks.
197
+ #
198
+ # @return [String, Boolean] Failure message or true if no failure found
199
+ def match_for_failure # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
200
+ given_value_for_check = actual.public_send(output_name)
201
+
202
+ if instance_of_class
203
+ matcher = RSpec::Matchers::BuiltIn::BeAnInstanceOf.new(instance_of_class)
204
+ return matcher.failure_message unless matcher.matches?(given_value_for_check)
205
+ end
206
+
207
+ if nested_methods.present?
208
+ nested_methods.each do |method_name|
209
+ next unless given_value_for_check.respond_to?(method_name)
210
+
211
+ given_value_for_check = given_value_for_check.public_send(method_name)
212
+ end
213
+ end
214
+
215
+ return true if !@value_defined && expected_value.nil?
216
+
217
+ matcher = case expected_value
218
+ when Array
219
+ RSpec::Matchers::BuiltIn::ContainExactly.new(expected_value)
220
+ when Hash
221
+ RSpec::Matchers::BuiltIn::Match.new(expected_value)
222
+ when TrueClass, FalseClass
223
+ RSpec::Matchers::BuiltIn::Equal.new(expected_value)
224
+ when NilClass
225
+ RSpec::Matchers::BuiltIn::BeNil.new(expected_value)
226
+ else
227
+ RSpec::Matchers::BuiltIn::Eq.new(expected_value)
228
+ end
229
+
230
+ return true if matcher.matches?(given_value_for_check)
231
+
232
+ matcher.failure_message
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module Result
8
+ # RSpec matcher for validating failed service results.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # Validates that a service result is a failure with expected error type,
13
+ # message, and metadata. Supports custom failure classes configured via
14
+ # Servactory settings.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # ```ruby
19
+ # RSpec.describe MyService, type: :service do
20
+ # it "fails with validation error" do
21
+ # result = described_class.call(invalid: true)
22
+ #
23
+ # expect(result).to be_failure_service
24
+ # .type(:validation_error)
25
+ # .message("Invalid input provided")
26
+ # end
27
+ #
28
+ # it "fails with custom exception and meta" do
29
+ # result = described_class.call(bad_data: true)
30
+ #
31
+ # expect(result).to be_failure_service
32
+ # .with(MyCustomFailure)
33
+ # .type(:processing_error)
34
+ # .meta(field: :data, code: 422)
35
+ # end
36
+ # end
37
+ # ```
38
+ #
39
+ # ## Chain Methods
40
+ #
41
+ # - `.with(Class)` - expected custom failure class
42
+ # - `.type(Symbol)` - expected error type (defaults to `:base`)
43
+ # - `.message(String)` - expected error message
44
+ # - `.meta(Hash)` - expected error metadata
45
+ #
46
+ # ## Validation Steps
47
+ #
48
+ # 1. Checks result is a `Servactory::Result` instance
49
+ # 2. Verifies `result.success?` returns false
50
+ # 3. Verifies `result.failure?` returns true
51
+ # 4. Validates error is a `Servactory::Exceptions::Failure`
52
+ # 5. Validates failure class if specified via `.with`
53
+ # 6. Validates error type (defaults to `:base`)
54
+ # 7. Validates message if specified
55
+ # 8. Validates meta if specified
56
+ class BeFailureServiceMatcher # rubocop:disable Metrics/ClassLength
57
+ include RSpec::Matchers::Composable
58
+
59
+ # Creates a new failure matcher with empty expectations.
60
+ #
61
+ # @return [BeFailureServiceMatcher] New matcher instance
62
+ def initialize
63
+ @expected_failure_class = nil
64
+ @expected_type = nil
65
+ @expected_message = nil
66
+ @expected_meta = nil
67
+ @type_defined = false
68
+ @message_defined = false
69
+ @meta_defined = false
70
+ end
71
+
72
+ # Indicates this matcher does not support block expectations.
73
+ #
74
+ # @return [Boolean] Always false
75
+ def supports_block_expectations?
76
+ false
77
+ end
78
+
79
+ # Performs the match against the actual service result.
80
+ #
81
+ # @param result [Servactory::Result] The service result to validate
82
+ # @return [Boolean] True if result is failure with matching error attributes
83
+ def matches?(result) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
84
+ @result = result
85
+
86
+ failure_class = expected_failure_class || Servactory::Exceptions::Failure
87
+ type = @type_defined ? expected_type : :base
88
+
89
+ matched = result.is_a?(Servactory::Result)
90
+ matched &&= !result.success?
91
+ matched &&= result.failure?
92
+ matched &&= result.error.is_a?(Servactory::Exceptions::Failure)
93
+ matched &&= result.error.is_a?(failure_class)
94
+ matched &&= result.error.type == type
95
+ matched &&= result.error.message == expected_message if @message_defined
96
+ matched &&= result.error.meta == expected_meta if @meta_defined
97
+
98
+ matched
99
+ end
100
+
101
+ # Returns a description of what this matcher validates.
102
+ #
103
+ # @return [String] Human-readable matcher description
104
+ def description
105
+ "service failure"
106
+ end
107
+
108
+ # Returns detailed failure message explaining what check failed.
109
+ #
110
+ # Checks in order: result type, failure status, error class, failure class,
111
+ # error type, message, and meta.
112
+ #
113
+ # @return [String] Detailed failure message with expected vs actual
114
+ def failure_message # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
115
+ unless result.is_a?(Servactory::Result)
116
+ return <<~MESSAGE
117
+ Incorrect service result:
118
+
119
+ expected Servactory::Result
120
+ got #{result.class.name}
121
+ MESSAGE
122
+ end
123
+
124
+ if result.success?
125
+ return <<~MESSAGE
126
+ Incorrect service result:
127
+
128
+ expected failure
129
+ got success
130
+ MESSAGE
131
+ end
132
+
133
+ unless result.error.is_a?(Servactory::Exceptions::Failure)
134
+ return <<~MESSAGE
135
+ Incorrect error object:
136
+
137
+ expected Servactory::Exceptions::Failure
138
+ got #{result.error.class.name}
139
+ MESSAGE
140
+ end
141
+
142
+ if expected_failure_class && !result.error.is_a?(expected_failure_class)
143
+ return <<~MESSAGE
144
+ Incorrect instance error:
145
+
146
+ expected #{expected_failure_class}
147
+ got #{result.error.class.name}
148
+ MESSAGE
149
+ end
150
+
151
+ expected_type_value = @type_defined ? expected_type : :base
152
+ if result.error.type != expected_type_value
153
+ return <<~MESSAGE
154
+ Incorrect error type:
155
+
156
+ expected #{expected_type_value.inspect}
157
+ got #{result.error.type.inspect}
158
+ MESSAGE
159
+ end
160
+
161
+ if @message_defined && result.error.message != expected_message
162
+ return <<~MESSAGE
163
+ Incorrect error message:
164
+
165
+ expected #{expected_message.inspect}
166
+ got #{result.error.message.inspect}
167
+ MESSAGE
168
+ end
169
+
170
+ if @meta_defined && result.error.meta != expected_meta
171
+ return <<~MESSAGE
172
+ Incorrect error meta:
173
+
174
+ expected #{expected_meta.inspect}
175
+ got #{result.error.meta.inspect}
176
+ MESSAGE
177
+ end
178
+
179
+ <<~MESSAGE
180
+ Unexpected case when using `be_failure_service`.
181
+
182
+ Exception: #{result.error.inspect}
183
+ Type: #{result.error.type.inspect}
184
+ Message: #{result.error.message.inspect}
185
+ Meta: #{result.error.meta.inspect}
186
+
187
+ Please try to build an example based on the documentation.
188
+ Or report your problem to us:
189
+
190
+ https://github.com/servactory/servactory/issues
191
+ MESSAGE
192
+ end
193
+
194
+ # Returns the failure message for negated expectations.
195
+ #
196
+ # @return [String] Negated failure message
197
+ def failure_message_when_negated
198
+ "Expected result not to be a failed service"
199
+ end
200
+
201
+ # Chain Methods
202
+ # -------------
203
+
204
+ # Specifies the expected custom failure class.
205
+ #
206
+ # Use when service is configured with a custom failure_class.
207
+ #
208
+ # @param failure_class [Class] Expected failure exception class
209
+ # @return [self] For method chaining
210
+ def with(failure_class)
211
+ @expected_failure_class = failure_class
212
+ self
213
+ end
214
+
215
+ # Specifies the expected error type.
216
+ #
217
+ # @param expected_type [Symbol] Expected type (defaults to :base if not set)
218
+ # @return [self] For method chaining
219
+ def type(expected_type)
220
+ @expected_type = expected_type
221
+ @type_defined = true
222
+ self
223
+ end
224
+
225
+ # Specifies the expected error message.
226
+ #
227
+ # @param expected_message [String] Expected error message text
228
+ # @return [self] For method chaining
229
+ def message(expected_message)
230
+ @expected_message = expected_message
231
+ @message_defined = true
232
+ self
233
+ end
234
+
235
+ # Specifies the expected error metadata.
236
+ #
237
+ # @param expected_meta [Hash] Expected meta hash
238
+ # @return [self] For method chaining
239
+ def meta(expected_meta)
240
+ @expected_meta = expected_meta
241
+ @meta_defined = true
242
+ self
243
+ end
244
+
245
+ private
246
+
247
+ attr_reader :result,
248
+ :expected_failure_class,
249
+ :expected_type,
250
+ :expected_message,
251
+ :expected_meta
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module Result
8
+ # RSpec matcher for validating successful service results.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # Validates that a service result is successful and optionally contains
13
+ # expected output values. Used in integration tests to verify service
14
+ # execution completed without failure.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # ```ruby
19
+ # RSpec.describe MyService, type: :service do
20
+ # it "succeeds with expected outputs" do
21
+ # result = described_class.call(user_id: 123)
22
+ #
23
+ # expect(result).to be_success_service
24
+ # .with_output(:user_name, "John")
25
+ # .with_output(:status, "active")
26
+ # end
27
+ #
28
+ # it "succeeds with multiple outputs" do
29
+ # result = described_class.call(data: input_data)
30
+ #
31
+ # expect(result).to be_success_service
32
+ # .with_outputs(processed: true, count: 5)
33
+ # end
34
+ # end
35
+ # ```
36
+ #
37
+ # ## Chain Methods
38
+ #
39
+ # - `.with_output(key, value)` - validates single output value
40
+ # - `.with_outputs(hash)` - validates multiple output values at once
41
+ #
42
+ # ## Validation Steps
43
+ #
44
+ # 1. Checks result is a `Servactory::Result` instance
45
+ # 2. Verifies `result.success?` returns true
46
+ # 3. Verifies `result.failure?` returns false
47
+ # 4. Validates all expected outputs match actual values
48
+ class BeSuccessServiceMatcher
49
+ include RSpec::Matchers::Composable
50
+
51
+ # Creates a new success matcher with empty output expectations.
52
+ #
53
+ # @return [BeSuccessServiceMatcher] New matcher instance
54
+ def initialize
55
+ @expected_outputs = {}
56
+ end
57
+
58
+ # Indicates this matcher does not support block expectations.
59
+ #
60
+ # @return [Boolean] Always false
61
+ def supports_block_expectations?
62
+ false
63
+ end
64
+
65
+ # Performs the match against the actual service result.
66
+ #
67
+ # @param result [Servactory::Result] The service result to validate
68
+ # @return [Boolean] True if result is successful with matching outputs
69
+ def matches?(result)
70
+ @result = result
71
+
72
+ matched = result.is_a?(Servactory::Result)
73
+ matched &&= result.success?
74
+ matched &&= !result.failure?
75
+
76
+ matched &&= expected_outputs.all? do |key, value|
77
+ result.respond_to?(key) && result.public_send(key) == value
78
+ end
79
+
80
+ matched
81
+ end
82
+
83
+ # Returns a description of what this matcher validates.
84
+ #
85
+ # @return [String] Human-readable matcher description
86
+ def description
87
+ "service success"
88
+ end
89
+
90
+ # Returns detailed failure message explaining what check failed.
91
+ #
92
+ # Checks in order: result type, success status, output existence, output values.
93
+ #
94
+ # @return [String] Detailed failure message
95
+ def failure_message # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
96
+ unless result.is_a?(Servactory::Result)
97
+ return <<~MESSAGE
98
+ Incorrect service result:
99
+
100
+ expected Servactory::Result
101
+ got #{result.class.name}
102
+ MESSAGE
103
+ end
104
+
105
+ if result.failure?
106
+ return <<~MESSAGE
107
+ Incorrect service result:
108
+
109
+ expected success
110
+ got failure
111
+ MESSAGE
112
+ end
113
+
114
+ message = expected_outputs.each do |key, value|
115
+ unless result.respond_to?(key)
116
+ break <<~MESSAGE
117
+ Non-existent value key in result:
118
+
119
+ expected #{result.inspect}
120
+ got #{key}
121
+ MESSAGE
122
+ end
123
+
124
+ received_value = result.public_send(key)
125
+ next if received_value == value
126
+
127
+ break <<~MESSAGE
128
+ Incorrect result value for #{key}:
129
+
130
+ expected #{value.inspect}
131
+ got #{received_value.inspect}
132
+ MESSAGE
133
+ end
134
+
135
+ return message if message.present? && message.is_a?(String)
136
+
137
+ <<~MESSAGE
138
+ Unexpected case when using `be_success_service`.
139
+
140
+ Please try to build an example based on the documentation.
141
+ Or report your problem to us:
142
+
143
+ https://github.com/servactory/servactory/issues
144
+ MESSAGE
145
+ end
146
+
147
+ # Returns the failure message for negated expectations.
148
+ #
149
+ # @return [String] Negated failure message
150
+ def failure_message_when_negated
151
+ "Expected result not to be a successful service"
152
+ end
153
+
154
+ # Chain Methods
155
+ # -------------
156
+
157
+ # Specifies an expected output value on the result.
158
+ #
159
+ # @param key [Symbol] Output attribute name
160
+ # @param value [Object] Expected value
161
+ # @return [self] For method chaining
162
+ def with_output(key, value)
163
+ expected_outputs[key] = value
164
+ self
165
+ end
166
+
167
+ # Specifies multiple expected output values at once.
168
+ #
169
+ # @param attributes [Hash{Symbol => Object}] Expected output key-value pairs
170
+ # @return [self] For method chaining
171
+ def with_outputs(attributes)
172
+ attributes.each { |key, value| expected_outputs[key] = value }
173
+ self
174
+ end
175
+
176
+ private
177
+
178
+ attr_reader :result,
179
+ :expected_outputs
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end