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,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module Base
8
+ # Base class for RSpec matchers that validate Servactory attribute definitions.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # AttributeMatcher provides the foundation for testing service attribute
13
+ # definitions (inputs, internals, outputs) in Servactory services. It implements
14
+ # the RSpec matcher protocol with fluent API for chaining validation rules.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # Subclasses define specific attribute type matchers:
19
+ #
20
+ # ```ruby
21
+ # class HaveServiceInputMatcher < Base::AttributeMatcher
22
+ # for_attribute_type :input
23
+ #
24
+ # register_submatcher :required,
25
+ # class_name: "Input::RequiredSubmatcher",
26
+ # mutually_exclusive_with: [:optional]
27
+ # end
28
+ # ```
29
+ #
30
+ # In RSpec tests:
31
+ #
32
+ # ```ruby
33
+ # expect(MyService).to have_service_input(:name)
34
+ # .type(String)
35
+ # .required
36
+ # .inclusion(%w[admin user])
37
+ # ```
38
+ #
39
+ # ## Architecture
40
+ #
41
+ # Works with:
42
+ # - SubmatcherRegistry - provides DSL for registering submatchers
43
+ # - SubmatcherContext - carries context data to submatchers
44
+ # - Submatcher - base class for individual validation rules
45
+ #
46
+ # ## Features
47
+ #
48
+ # - **Fluent API** - chain methods for readable test assertions
49
+ # - **Dynamic Chain Methods** - generated from submatcher registry
50
+ # - **Mutual Exclusivity** - conflicting options replace each other
51
+ # - **Composable** - works with RSpec's compound matchers
52
+ # - **Block Expectations** - supports `expect { }.to` syntax
53
+ class AttributeMatcher # rubocop:disable Metrics/ClassLength
54
+ include SubmatcherRegistry
55
+ include RSpec::Matchers::Composable
56
+
57
+ class << self
58
+ # @return [Symbol] The attribute type this matcher validates (:input, :internal, :output)
59
+ attr_reader :attribute_type
60
+
61
+ # Sets the attribute type for this matcher class.
62
+ #
63
+ # @param type [Symbol] The attribute type (:input, :internal, :output)
64
+ # @return [void]
65
+ def for_attribute_type(type)
66
+ @attribute_type = type
67
+ end
68
+ end
69
+
70
+ # @return [Class] The Servactory service class being tested
71
+ attr_reader :described_class
72
+
73
+ # @return [Symbol] The name of the attribute being validated
74
+ attr_reader :attribute_name
75
+
76
+ # @return [Array, nil] Type classes passed to the types chain method
77
+ attr_reader :option_types
78
+
79
+ # Creates a new attribute matcher instance.
80
+ #
81
+ # @param described_class [Class] The Servactory service class to test
82
+ # @param attribute_name [Symbol] The name of the attribute to validate
83
+ def initialize(described_class, attribute_name)
84
+ @described_class = described_class
85
+ @attribute_name = attribute_name
86
+ @submatchers = []
87
+ @option_types = nil
88
+ @last_submatcher = nil
89
+
90
+ build_chain_methods_from_registry
91
+ end
92
+
93
+ # Indicates this matcher supports block expectations.
94
+ #
95
+ # Required by RSpec for matchers used with `expect { }.to` syntax.
96
+ #
97
+ # @return [Boolean] Always returns true
98
+ def supports_block_expectations?
99
+ true
100
+ end
101
+
102
+ # Checks if all submatchers pass for the given subject.
103
+ #
104
+ # @param subject [Object] The RSpec subject (typically nil for service class matchers)
105
+ # @return [Boolean] True if all submatchers pass, false otherwise
106
+ def matches?(subject)
107
+ @subject = subject
108
+ failing_submatchers.empty?
109
+ end
110
+
111
+ # Builds a human-readable description of what this matcher validates.
112
+ #
113
+ # @return [String] Description including attribute name and all submatcher descriptions
114
+ def description
115
+ submatcher_descriptions = submatchers.map(&:description).join(", ")
116
+ "#{attribute_name} with #{submatcher_descriptions}"
117
+ end
118
+
119
+ # Builds the failure message when the matcher does not pass.
120
+ #
121
+ # @return [String] Explanation of what was expected and what was missing
122
+ def failure_message
123
+ "Expected #{expectation}, which #{missing_options}"
124
+ end
125
+
126
+ # Builds the failure message for negated expectations.
127
+ #
128
+ # @return [String] Explanation for negated expectation failure
129
+ def failure_message_when_negated
130
+ "Did not expect #{expectation} with specified options"
131
+ end
132
+
133
+ protected
134
+
135
+ # @return [Array<Submatcher>] Collection of registered submatchers
136
+ attr_reader :submatchers
137
+
138
+ # @return [Object] The RSpec subject passed to matches?
139
+ attr_reader :subject
140
+
141
+ # @return [Submatcher, nil] The most recently added submatcher
142
+ attr_reader :last_submatcher
143
+
144
+ # Returns the attribute type from the class-level setting.
145
+ #
146
+ # @return [Symbol] The attribute type (:input, :internal, :output)
147
+ def attribute_type
148
+ self.class.attribute_type
149
+ end
150
+
151
+ # Returns the pluralized attribute type for accessing service info.
152
+ #
153
+ # @return [Symbol] Pluralized attribute type (:inputs, :internals, :outputs)
154
+ def attribute_type_plural
155
+ @attribute_type_plural ||= attribute_type.to_s.pluralize.to_sym
156
+ end
157
+
158
+ # Fetches attribute definition data from the service class.
159
+ #
160
+ # @return [Hash] The attribute definition hash from service info
161
+ def attribute_data
162
+ @attribute_data ||=
163
+ described_class
164
+ .info
165
+ .public_send(attribute_type_plural)
166
+ .fetch(attribute_name)
167
+ end
168
+
169
+ # Returns the i18n root key configured for the service.
170
+ #
171
+ # @return [String, nil] The i18n root key for error messages
172
+ def i18n_root_key
173
+ @i18n_root_key ||= described_class.config.i18n_root_key
174
+ end
175
+
176
+ # Builds the expectation string for failure messages.
177
+ #
178
+ # @return [String] Human-readable expectation description
179
+ def expectation
180
+ "#{described_class.name} to have a service #{attribute_type} attribute named #{attribute_name}"
181
+ end
182
+
183
+ # Finds submatchers that did not pass validation.
184
+ #
185
+ # @return [Array<Submatcher>] Submatchers where matches? returned false
186
+ def failing_submatchers
187
+ @failing_submatchers ||= submatchers.reject { |m| m.matches?(subject) }
188
+ end
189
+
190
+ # Collects missing option messages from failing submatchers.
191
+ #
192
+ # @return [String] Comma-separated list of missing option descriptions
193
+ def missing_options
194
+ failing_submatchers.map(&:missing_option).select(&:present?).join(", ")
195
+ end
196
+
197
+ # Adds a submatcher to the collection, replacing any of the same class.
198
+ #
199
+ # @param submatcher [Submatcher] The submatcher to add
200
+ # @return [void]
201
+ def add_submatcher(submatcher)
202
+ remove_submatcher(submatcher.class)
203
+ @last_submatcher = submatcher
204
+ submatchers << submatcher
205
+ end
206
+
207
+ # Removes all submatchers of the specified class.
208
+ #
209
+ # @param matcher_class [Class] The submatcher class to remove
210
+ # @return [void]
211
+ def remove_submatcher(matcher_class)
212
+ submatchers.delete_if { |sm| sm.is_a?(matcher_class) }
213
+ end
214
+
215
+ private
216
+
217
+ # Dynamically defines chain methods from registered submatcher definitions.
218
+ #
219
+ # Called during initialization to create fluent API methods like
220
+ # `.type()`, `.required()`, `.optional()`, etc.
221
+ #
222
+ # @return [void]
223
+ def build_chain_methods_from_registry
224
+ self.class.submatcher_definitions.each_value do |definition|
225
+ define_chain_method_for(definition)
226
+ end
227
+ end
228
+
229
+ # Defines a chain method and its aliases for a submatcher definition.
230
+ #
231
+ # Creates a singleton method that:
232
+ # 1. Extracts options hash if applicable
233
+ # 2. Removes mutually exclusive submatchers
234
+ # 3. Stores option_types if needed
235
+ # 4. Builds and adds the submatcher
236
+ #
237
+ # @param definition [SubmatcherDefinition] The submatcher definition
238
+ # @return [void]
239
+ def define_chain_method_for(definition) # rubocop:disable Metrics/MethodLength
240
+ define_singleton_method(definition.chain_method) do |*args|
241
+ # For methods that accept trailing options hash (like target(value, name: :option)),
242
+ # we need to extract it. We use a heuristic: if the method expects options AND
243
+ # the last arg is a Hash with Symbol keys that look like option keys.
244
+ options = extract_options_hash(args, definition)
245
+
246
+ handle_mutually_exclusive(definition)
247
+
248
+ @option_types = definition.transform_args.call(args, options) if definition.stores_option_types
249
+
250
+ submatcher = build_submatcher(definition, args, options)
251
+ add_submatcher(submatcher)
252
+ self
253
+ end
254
+
255
+ definition.chain_aliases.each do |alias_name|
256
+ define_singleton_method(alias_name) do |*args|
257
+ public_send(definition.chain_method, *args)
258
+ end
259
+ end
260
+ end
261
+
262
+ # Extracts trailing options hash from arguments if the definition accepts them.
263
+ #
264
+ # @param args [Array] The method arguments
265
+ # @param definition [SubmatcherDefinition] The submatcher definition
266
+ # @return [Hash] Extracted options hash or empty hash
267
+ def extract_options_hash(args, definition)
268
+ # Only extract trailing options hash if the definition explicitly accepts them.
269
+ return {} unless definition.accepts_trailing_options
270
+ return {} unless args.last.is_a?(Hash)
271
+ return {} if args.last.is_a?(Class)
272
+
273
+ args.pop || {}
274
+ end
275
+
276
+ # Removes submatchers that are mutually exclusive with the current one.
277
+ #
278
+ # @param definition [SubmatcherDefinition] The current submatcher definition
279
+ # @return [void]
280
+ def handle_mutually_exclusive(definition)
281
+ definition.mutually_exclusive_with.each do |exclusive_name|
282
+ exclusive_def = self.class.submatcher_definitions[exclusive_name]
283
+ next unless exclusive_def
284
+
285
+ submatcher_class = resolve_submatcher_class(exclusive_def.class_name)
286
+ remove_submatcher(submatcher_class)
287
+ end
288
+ end
289
+
290
+ # Builds a submatcher instance with context and transformed arguments.
291
+ #
292
+ # @param definition [SubmatcherDefinition] The submatcher definition
293
+ # @param args [Array] Original method arguments
294
+ # @param options [Hash] Extracted options hash
295
+ # @return [Submatcher] The constructed submatcher instance
296
+ def build_submatcher(definition, args, options) # rubocop:disable Metrics/MethodLength
297
+ context = SubmatcherContext.new(
298
+ described_class:,
299
+ attribute_type:,
300
+ attribute_name:,
301
+ attribute_data:,
302
+ option_types: definition.requires_option_types ? option_types : nil,
303
+ last_submatcher: definition.requires_last_submatcher ? last_submatcher : nil,
304
+ i18n_root_key:
305
+ )
306
+
307
+ transformed_args = definition.transform_args.call(args, options)
308
+ submatcher_class = resolve_submatcher_class(definition.class_name)
309
+ submatcher_class.new(context, *transformed_args)
310
+ end
311
+
312
+ # Resolves submatcher class name to actual class constant.
313
+ #
314
+ # @param class_name [String] Relative class name (e.g., "Input::RequiredSubmatcher")
315
+ # @return [Class] The submatcher class
316
+ def resolve_submatcher_class(class_name)
317
+ "Servactory::TestKit::Rspec::Matchers::Submatchers::#{class_name}".constantize
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module Base
8
+ # Abstract base class for individual validation rules in attribute matchers.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # Submatcher provides the foundation for specific validation checks like
13
+ # type validation, required/optional status, default values, etc.
14
+ # Each submatcher validates one aspect of a service attribute definition.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # Create a subclass and implement required abstract methods:
19
+ #
20
+ # ```ruby
21
+ # class RequiredSubmatcher < Base::Submatcher
22
+ # def description
23
+ # "required"
24
+ # end
25
+ #
26
+ # protected
27
+ #
28
+ # def passes?
29
+ # fetch_option(:required) == true
30
+ # end
31
+ #
32
+ # def build_failure_message
33
+ # "expected input to be required, but it was optional"
34
+ # end
35
+ # end
36
+ # ```
37
+ #
38
+ # ## Abstract Methods
39
+ #
40
+ # Subclasses must implement:
41
+ # - `description` - human-readable description for test output
42
+ # - `passes?` - validation logic returning boolean
43
+ # - `build_failure_message` - explanation when validation fails
44
+ #
45
+ # ## Architecture
46
+ #
47
+ # Works with:
48
+ # - SubmatcherContext - provides attribute data and metadata
49
+ # - AttributeMatcher - manages collection of submatchers
50
+ # - Concerns - provides helper methods for data access and comparison
51
+ class Submatcher
52
+ include RSpec::Matchers::Composable
53
+ include Concerns::AttributeDataAccess
54
+ include Concerns::ErrorMessageBuilder
55
+ include Concerns::ValueComparison
56
+
57
+ # @return [String] Failure message when validation does not pass
58
+ attr_reader :missing_option
59
+
60
+ # Creates a new submatcher instance.
61
+ #
62
+ # @param context [SubmatcherContext] Context with attribute data and metadata
63
+ def initialize(context)
64
+ @context = context
65
+ @missing_option = ""
66
+ end
67
+
68
+ # Checks if this submatcher's validation passes.
69
+ #
70
+ # @param _subject [Object] RSpec subject (unused, kept for interface compatibility)
71
+ # @return [Boolean] True if validation passes
72
+ def matches?(_subject)
73
+ if passes?
74
+ true
75
+ else
76
+ @missing_option = build_failure_message
77
+ false
78
+ end
79
+ end
80
+
81
+ # Returns the failure message for RSpec output.
82
+ #
83
+ # @return [String] The failure message
84
+ def failure_message
85
+ @missing_option
86
+ end
87
+
88
+ # Returns the failure message for negated expectations.
89
+ #
90
+ # @return [String] The negated failure message
91
+ def failure_message_when_negated
92
+ "expected not to #{description}"
93
+ end
94
+
95
+ # Returns a human-readable description of what this submatcher validates.
96
+ #
97
+ # @abstract Subclasses must implement this method
98
+ # @return [String] Description for test output
99
+ # @raise [NotImplementedError] If not implemented by subclass
100
+ def description
101
+ raise NotImplementedError, "#{self.class} must implement #description"
102
+ end
103
+
104
+ protected
105
+
106
+ # Performs the actual validation logic.
107
+ #
108
+ # @abstract Subclasses must implement this method
109
+ # @return [Boolean] True if validation passes
110
+ # @raise [NotImplementedError] If not implemented by subclass
111
+ def passes?
112
+ raise NotImplementedError, "#{self.class} must implement #passes?"
113
+ end
114
+
115
+ # Builds a descriptive failure message when validation fails.
116
+ #
117
+ # @abstract Subclasses must implement this method
118
+ # @return [String] Explanation of why validation failed
119
+ # @raise [NotImplementedError] If not implemented by subclass
120
+ def build_failure_message
121
+ raise NotImplementedError, "#{self.class} must implement #build_failure_message"
122
+ end
123
+
124
+ private
125
+
126
+ # @return [SubmatcherContext] The context with attribute data
127
+ attr_reader :context
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module Base
8
+ # Data transfer object carrying context for submatcher validation.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # SubmatcherContext encapsulates all the information a submatcher needs
13
+ # to perform its validation. It provides a clean interface for passing
14
+ # attribute metadata, service class info, and dependent data between
15
+ # the parent matcher and individual submatchers.
16
+ #
17
+ # ## Usage
18
+ #
19
+ # Created by AttributeMatcher and passed to each submatcher:
20
+ #
21
+ # ```ruby
22
+ # context = SubmatcherContext.new(
23
+ # described_class: MyService,
24
+ # attribute_type: :input,
25
+ # attribute_name: :user_id,
26
+ # attribute_data: { type: Integer, required: true }
27
+ # )
28
+ #
29
+ # submatcher = RequiredSubmatcher.new(context)
30
+ # ```
31
+ #
32
+ # ## Attributes
33
+ #
34
+ # - `described_class` - The Servactory service class being tested
35
+ # - `attribute_type` - :input, :internal, or :output
36
+ # - `attribute_name` - Name of the attribute being validated
37
+ # - `attribute_data` - Hash with attribute definition (type, required, etc.)
38
+ # - `option_types` - Type classes passed via .type() chain method
39
+ # - `last_submatcher` - Previous submatcher (for dependent validations)
40
+ # - `i18n_root_key` - Root key for i18n error messages
41
+ class SubmatcherContext
42
+ # @return [Class] The Servactory service class being tested
43
+ attr_reader :described_class
44
+
45
+ # @return [Symbol] The attribute type (:input, :internal, :output)
46
+ attr_reader :attribute_type
47
+
48
+ # @return [Symbol] The name of the attribute being validated
49
+ attr_reader :attribute_name
50
+
51
+ # @return [Hash] The attribute definition data from service info
52
+ attr_reader :attribute_data
53
+
54
+ # @return [Array, nil] Type classes from the .type() chain method
55
+ attr_reader :option_types
56
+
57
+ # @return [Submatcher, nil] The previous submatcher for chained validations
58
+ attr_reader :last_submatcher
59
+
60
+ # @return [String, nil] The i18n root key for error messages
61
+ attr_reader :i18n_root_key
62
+
63
+ # Creates a new submatcher context.
64
+ #
65
+ # @param described_class [Class] The Servactory service class
66
+ # @param attribute_type [Symbol] The attribute type (:input, :internal, :output)
67
+ # @param attribute_name [Symbol] The attribute name
68
+ # @param attribute_data [Hash] The attribute definition data
69
+ # @param option_types [Array, nil] Type classes from .type() method
70
+ # @param last_submatcher [Submatcher, nil] Previous submatcher for chaining
71
+ # @param i18n_root_key [String, nil] Root key for i18n messages
72
+ def initialize(
73
+ described_class:,
74
+ attribute_type:,
75
+ attribute_name:,
76
+ attribute_data:,
77
+ option_types: nil,
78
+ last_submatcher: nil,
79
+ i18n_root_key: nil
80
+ )
81
+ @described_class = described_class
82
+ @attribute_type = attribute_type
83
+ @attribute_name = attribute_name
84
+ @attribute_data = attribute_data
85
+ @option_types = option_types
86
+ @last_submatcher = last_submatcher
87
+ @i18n_root_key = i18n_root_key
88
+ end
89
+
90
+ # Returns the pluralized attribute type for accessing service info.
91
+ #
92
+ # @return [Symbol] Pluralized type (:inputs, :internals, :outputs)
93
+ def attribute_type_plural
94
+ @attribute_type_plural ||= attribute_type.to_s.pluralize.to_sym
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end