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.
- checksums.yaml +4 -4
- data/README.md +38 -9
- data/config/locales/de.yml +134 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/es.yml +134 -0
- data/config/locales/fr.yml +134 -0
- data/config/locales/it.yml +134 -0
- data/config/locales/ru.yml +15 -0
- data/lib/generators/README.md +45 -0
- data/lib/generators/servactory/base.rb +82 -0
- data/lib/generators/servactory/extension/USAGE +54 -0
- data/lib/generators/servactory/extension/extension_generator.rb +41 -0
- data/lib/generators/servactory/extension/templates/extension.rb.tt +62 -0
- data/lib/generators/servactory/install/USAGE +27 -0
- data/lib/generators/servactory/install/install_generator.rb +94 -0
- data/lib/generators/servactory/{templates/services/application_service/base.rb → install/templates/application_service/base.rb.tt} +29 -19
- data/lib/generators/servactory/{templates/services/application_service/exceptions.rb → install/templates/application_service/exceptions.rb.tt} +1 -1
- data/lib/generators/servactory/{templates/services/application_service/result.rb → install/templates/application_service/result.rb.tt} +1 -1
- data/lib/generators/servactory/rspec/USAGE +46 -0
- data/lib/generators/servactory/rspec/rspec_generator.rb +95 -0
- data/lib/generators/servactory/rspec/templates/service_spec.rb.tt +58 -0
- data/lib/generators/servactory/service/USAGE +51 -0
- data/lib/generators/servactory/service/service_generator.rb +56 -0
- data/lib/generators/servactory/service/templates/service.rb.tt +22 -0
- data/lib/servactory/actions/collection.rb +56 -1
- data/lib/servactory/actions/dsl.rb +11 -11
- data/lib/servactory/actions/tools/rules.rb +1 -1
- data/lib/servactory/actions/tools/runner.rb +111 -28
- data/lib/servactory/base.rb +1 -7
- data/lib/servactory/configuration/actions/aliases/collection.rb +5 -0
- data/lib/servactory/configuration/actions/rescue_handlers/collection.rb +5 -0
- data/lib/servactory/configuration/actions/shortcuts/collection.rb +5 -0
- data/lib/servactory/configuration/collection_mode/class_names_collection.rb +5 -0
- data/lib/servactory/configuration/config.rb +36 -0
- data/lib/servactory/configuration/configurable.rb +95 -0
- data/lib/servactory/configuration/dsl.rb +3 -27
- data/lib/servactory/configuration/factory.rb +20 -20
- data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -0
- data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +5 -0
- data/lib/servactory/context/warehouse/inputs.rb +2 -2
- data/lib/servactory/context/workspace/inputs.rb +2 -2
- data/lib/servactory/context/workspace/internals.rb +2 -2
- data/lib/servactory/context/workspace/outputs.rb +2 -2
- data/lib/servactory/context/workspace.rb +11 -7
- data/lib/servactory/dsl.rb +10 -8
- data/lib/servactory/maintenance/attributes/tools/validation.rb +1 -1
- data/lib/servactory/result.rb +2 -2
- data/lib/servactory/stroma/dsl.rb +118 -0
- data/lib/servactory/stroma/entry.rb +32 -0
- data/lib/servactory/stroma/exceptions/base.rb +45 -0
- data/lib/servactory/stroma/exceptions/invalid_hook_type.rb +29 -0
- data/lib/servactory/stroma/exceptions/key_already_registered.rb +32 -0
- data/lib/servactory/stroma/exceptions/registry_frozen.rb +33 -0
- data/lib/servactory/stroma/exceptions/registry_not_finalized.rb +33 -0
- data/lib/servactory/stroma/exceptions/unknown_hook_target.rb +39 -0
- data/lib/servactory/stroma/hooks/applier.rb +63 -0
- data/lib/servactory/stroma/hooks/collection.rb +103 -0
- data/lib/servactory/stroma/hooks/factory.rb +80 -0
- data/lib/servactory/stroma/hooks/hook.rb +74 -0
- data/lib/servactory/stroma/registry.rb +94 -0
- data/lib/servactory/stroma/settings/collection.rb +90 -0
- data/lib/servactory/stroma/settings/registry_settings.rb +88 -0
- data/lib/servactory/stroma/settings/setting.rb +113 -0
- data/lib/servactory/stroma/state.rb +59 -0
- data/lib/servactory/test_kit/fake_type.rb +23 -0
- data/lib/servactory/test_kit/result.rb +45 -0
- data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
- data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
- data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
- data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
- data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
- data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
- data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
- data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
- data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
- data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
- data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
- data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
- data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
- data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
- data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
- data/lib/servactory/test_kit/utils/faker.rb +62 -2
- data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
- data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
- data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
- data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
- data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
- data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
- data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
- data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
- data/lib/servactory/tool_kit/dynamic_options/target.rb +252 -0
- data/lib/servactory/version.rb +4 -4
- data/lib/servactory.rb +4 -0
- metadata +73 -19
- data/lib/generators/servactory/install_generator.rb +0 -21
- data/lib/generators/servactory/rspec_generator.rb +0 -88
- data/lib/generators/servactory/service_generator.rb +0 -49
- data/lib/servactory/configuration/setup.rb +0 -97
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
- 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
|