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,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Base
|
|
8
|
+
# Registry mixin providing DSL for registering submatchers in attribute matchers.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# SubmatcherRegistry provides a class-level DSL for declaratively registering
|
|
13
|
+
# submatchers in AttributeMatcher subclasses. It handles inheritance of
|
|
14
|
+
# submatcher definitions and configuration options for each submatcher.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Include this module in AttributeMatcher subclasses:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# class HaveServiceInputMatcher < Base::AttributeMatcher
|
|
22
|
+
# include SubmatcherRegistry
|
|
23
|
+
#
|
|
24
|
+
# register_submatcher :required,
|
|
25
|
+
# class_name: "Input::RequiredSubmatcher",
|
|
26
|
+
# mutually_exclusive_with: [:optional]
|
|
27
|
+
#
|
|
28
|
+
# register_submatcher :types,
|
|
29
|
+
# class_name: "Shared::TypesSubmatcher",
|
|
30
|
+
# chain_method: :type,
|
|
31
|
+
# chain_aliases: [:types],
|
|
32
|
+
# stores_option_types: true
|
|
33
|
+
# end
|
|
34
|
+
# ```
|
|
35
|
+
#
|
|
36
|
+
# ## Features
|
|
37
|
+
#
|
|
38
|
+
# - **Declarative Registration** - register submatchers with options hash
|
|
39
|
+
# - **Chain Method Generation** - creates fluent API methods automatically
|
|
40
|
+
# - **Inheritance Support** - subclasses inherit parent's submatcher definitions
|
|
41
|
+
# - **Argument Transformation** - customize how arguments are passed to submatchers
|
|
42
|
+
# - **Mutual Exclusivity** - define conflicting submatchers that replace each other
|
|
43
|
+
#
|
|
44
|
+
# ## Architecture
|
|
45
|
+
#
|
|
46
|
+
# Works with:
|
|
47
|
+
# - AttributeMatcher - uses registry to build chain methods
|
|
48
|
+
# - SubmatcherDefinition - holds configuration for each submatcher
|
|
49
|
+
# - Submatcher - base class for actual validation logic
|
|
50
|
+
module SubmatcherRegistry
|
|
51
|
+
# Extends the including class with ClassMethods.
|
|
52
|
+
#
|
|
53
|
+
# @param base [Class] The class including this module
|
|
54
|
+
# @return [void]
|
|
55
|
+
def self.included(base)
|
|
56
|
+
base.extend(ClassMethods)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Class methods added to the including class.
|
|
60
|
+
module ClassMethods
|
|
61
|
+
# Returns the hash of registered submatcher definitions.
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash{Symbol => SubmatcherDefinition}] Submatcher definitions by name
|
|
64
|
+
def submatcher_definitions
|
|
65
|
+
@submatcher_definitions ||= {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Registers a new submatcher with configuration options.
|
|
69
|
+
#
|
|
70
|
+
# ## Options
|
|
71
|
+
#
|
|
72
|
+
# - `:class_name` - Relative class path (e.g., "Input::RequiredSubmatcher")
|
|
73
|
+
# - `:chain_method` - Method name for fluent API (defaults to name)
|
|
74
|
+
# - `:chain_aliases` - Additional method names that call chain_method
|
|
75
|
+
# - `:transform_args` - Lambda to transform method args before passing to submatcher
|
|
76
|
+
# - `:requires_option_types` - Pass option_types to submatcher context
|
|
77
|
+
# - `:requires_last_submatcher` - Pass previous submatcher to context
|
|
78
|
+
# - `:mutually_exclusive_with` - Array of submatcher names to remove when this is added
|
|
79
|
+
# - `:stores_option_types` - Store transformed args as option_types
|
|
80
|
+
# - `:accepts_trailing_options` - Extract trailing hash as keyword arguments
|
|
81
|
+
#
|
|
82
|
+
# @param name [Symbol] Unique identifier for this submatcher
|
|
83
|
+
# @param options [Hash] Configuration options
|
|
84
|
+
# @return [SubmatcherDefinition] The created definition
|
|
85
|
+
def register_submatcher(name, options = {}) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
86
|
+
submatcher_definitions[name] = SubmatcherDefinition.new(
|
|
87
|
+
name:,
|
|
88
|
+
class_name: options[:class_name],
|
|
89
|
+
chain_method: options[:chain_method] || name,
|
|
90
|
+
chain_aliases: options[:chain_aliases] || [],
|
|
91
|
+
transform_args: options[:transform_args] || ->(args, _kwargs = {}) { args },
|
|
92
|
+
requires_option_types: options[:requires_option_types] || false,
|
|
93
|
+
requires_last_submatcher: options[:requires_last_submatcher] || false,
|
|
94
|
+
mutually_exclusive_with: options[:mutually_exclusive_with] || [],
|
|
95
|
+
stores_option_types: options[:stores_option_types] || false,
|
|
96
|
+
accepts_trailing_options: options[:accepts_trailing_options] || false
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Copies submatcher definitions to subclasses.
|
|
101
|
+
#
|
|
102
|
+
# @param subclass [Class] The inheriting class
|
|
103
|
+
# @return [void]
|
|
104
|
+
def inherited(subclass)
|
|
105
|
+
super
|
|
106
|
+
subclass.instance_variable_set(
|
|
107
|
+
:@submatcher_definitions,
|
|
108
|
+
submatcher_definitions.dup
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Value object holding configuration for a single submatcher registration.
|
|
114
|
+
#
|
|
115
|
+
# ## Purpose
|
|
116
|
+
#
|
|
117
|
+
# Encapsulates all configuration options for a submatcher, including
|
|
118
|
+
# how to generate chain methods, transform arguments, and handle
|
|
119
|
+
# mutual exclusivity with other submatchers.
|
|
120
|
+
#
|
|
121
|
+
# ## Attributes
|
|
122
|
+
#
|
|
123
|
+
# - `name` - Unique identifier for this submatcher
|
|
124
|
+
# - `class_name` - Relative path to submatcher class
|
|
125
|
+
# - `chain_method` - Name of the fluent API method
|
|
126
|
+
# - `chain_aliases` - Alternative method names
|
|
127
|
+
# - `transform_args` - Lambda for argument transformation
|
|
128
|
+
# - `requires_option_types` - Whether submatcher needs type information
|
|
129
|
+
# - `requires_last_submatcher` - Whether submatcher needs previous submatcher
|
|
130
|
+
# - `mutually_exclusive_with` - Conflicting submatcher names
|
|
131
|
+
# - `stores_option_types` - Whether to store args as option_types
|
|
132
|
+
# - `accepts_trailing_options` - Whether to extract trailing hash
|
|
133
|
+
class SubmatcherDefinition
|
|
134
|
+
# @return [Symbol] Unique identifier for this submatcher
|
|
135
|
+
attr_reader :name
|
|
136
|
+
|
|
137
|
+
# @return [String] Relative class path (e.g., "Input::RequiredSubmatcher")
|
|
138
|
+
attr_reader :class_name
|
|
139
|
+
|
|
140
|
+
# @return [Symbol] Method name for the fluent API
|
|
141
|
+
attr_reader :chain_method
|
|
142
|
+
|
|
143
|
+
# @return [Array<Symbol>] Alternative method names
|
|
144
|
+
attr_reader :chain_aliases
|
|
145
|
+
|
|
146
|
+
# @return [Proc] Lambda to transform arguments
|
|
147
|
+
attr_reader :transform_args
|
|
148
|
+
|
|
149
|
+
# @return [Boolean] Whether submatcher needs option_types in context
|
|
150
|
+
attr_reader :requires_option_types
|
|
151
|
+
|
|
152
|
+
# @return [Boolean] Whether submatcher needs last_submatcher in context
|
|
153
|
+
attr_reader :requires_last_submatcher
|
|
154
|
+
|
|
155
|
+
# @return [Array<Symbol>] Names of mutually exclusive submatchers
|
|
156
|
+
attr_reader :mutually_exclusive_with
|
|
157
|
+
|
|
158
|
+
# @return [Boolean] Whether to store transformed args as option_types
|
|
159
|
+
attr_reader :stores_option_types
|
|
160
|
+
|
|
161
|
+
# @return [Boolean] Whether to extract trailing hash as options
|
|
162
|
+
attr_reader :accepts_trailing_options
|
|
163
|
+
|
|
164
|
+
# Creates a new submatcher definition.
|
|
165
|
+
#
|
|
166
|
+
# @param name [Symbol] Unique identifier
|
|
167
|
+
# @param class_name [String] Relative class path
|
|
168
|
+
# @param chain_method [Symbol] Fluent API method name
|
|
169
|
+
# @param chain_aliases [Array<Symbol>] Alternative method names
|
|
170
|
+
# @param transform_args [Proc] Argument transformation lambda
|
|
171
|
+
# @param requires_option_types [Boolean] Pass option_types to context
|
|
172
|
+
# @param requires_last_submatcher [Boolean] Pass last_submatcher to context
|
|
173
|
+
# @param mutually_exclusive_with [Array<Symbol>] Conflicting submatchers
|
|
174
|
+
# @param stores_option_types [Boolean] Store args as option_types
|
|
175
|
+
# @param accepts_trailing_options [Boolean] Extract trailing hash
|
|
176
|
+
def initialize(
|
|
177
|
+
name:,
|
|
178
|
+
class_name:,
|
|
179
|
+
chain_method: nil,
|
|
180
|
+
chain_aliases: nil,
|
|
181
|
+
transform_args: nil,
|
|
182
|
+
requires_option_types: false,
|
|
183
|
+
requires_last_submatcher: false,
|
|
184
|
+
mutually_exclusive_with: nil,
|
|
185
|
+
stores_option_types: false,
|
|
186
|
+
accepts_trailing_options: false
|
|
187
|
+
)
|
|
188
|
+
@name = name
|
|
189
|
+
@class_name = class_name
|
|
190
|
+
@chain_method = chain_method
|
|
191
|
+
@chain_aliases = chain_aliases
|
|
192
|
+
@transform_args = transform_args
|
|
193
|
+
@requires_option_types = requires_option_types
|
|
194
|
+
@requires_last_submatcher = requires_last_submatcher
|
|
195
|
+
@mutually_exclusive_with = mutually_exclusive_with
|
|
196
|
+
@stores_option_types = stores_option_types
|
|
197
|
+
@accepts_trailing_options = accepts_trailing_options
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Concerns
|
|
8
|
+
# Concern providing accessor methods for attribute data in submatchers.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# AttributeDataAccess provides convenient methods for submatchers to access
|
|
13
|
+
# attribute definition data from the context. This includes the service class,
|
|
14
|
+
# attribute metadata, and helper methods for fetching specific options.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Include in submatcher classes:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# class MySubmatcher < Base::Submatcher
|
|
22
|
+
# include Concerns::AttributeDataAccess
|
|
23
|
+
#
|
|
24
|
+
# def passes?
|
|
25
|
+
# # Access attribute data directly
|
|
26
|
+
# fetch_option(:required) == true
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
# ```
|
|
30
|
+
#
|
|
31
|
+
# ## Methods Provided
|
|
32
|
+
#
|
|
33
|
+
# - `described_class` - the service class being tested
|
|
34
|
+
# - `attribute_type` - :input, :internal, or :output
|
|
35
|
+
# - `attribute_name` - name of the attribute
|
|
36
|
+
# - `attribute_data` - full attribute definition hash
|
|
37
|
+
# - `fetch_option` - get specific option from attribute data
|
|
38
|
+
# - `option_present?` - check if option exists and has value
|
|
39
|
+
module AttributeDataAccess
|
|
40
|
+
# Includes InstanceMethods in the including class.
|
|
41
|
+
#
|
|
42
|
+
# @param base [Class] The class including this concern
|
|
43
|
+
# @return [void]
|
|
44
|
+
def self.included(base)
|
|
45
|
+
base.include(InstanceMethods)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Instance methods added by this concern.
|
|
49
|
+
module InstanceMethods
|
|
50
|
+
# @return [Class, nil] The Servactory service class from context
|
|
51
|
+
def described_class = context&.described_class
|
|
52
|
+
|
|
53
|
+
# @return [Symbol, nil] The attribute type from context
|
|
54
|
+
def attribute_type = context&.attribute_type
|
|
55
|
+
|
|
56
|
+
# @return [Symbol, nil] The attribute name from context
|
|
57
|
+
def attribute_name = context&.attribute_name
|
|
58
|
+
|
|
59
|
+
# @return [Symbol, nil] The pluralized attribute type from context
|
|
60
|
+
def attribute_type_plural = context&.attribute_type_plural
|
|
61
|
+
|
|
62
|
+
# @return [String, nil] The i18n root key from context
|
|
63
|
+
def i18n_root_key = context&.i18n_root_key
|
|
64
|
+
|
|
65
|
+
# Returns the attribute definition data hash.
|
|
66
|
+
#
|
|
67
|
+
# @return [Hash] The attribute data from context
|
|
68
|
+
def attribute_data
|
|
69
|
+
context.attribute_data
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Fetches a specific option from attribute data.
|
|
73
|
+
#
|
|
74
|
+
# @param key [Symbol] The option key to fetch
|
|
75
|
+
# @param default [Object, nil] Default value if key not present
|
|
76
|
+
# @return [Object] The option value or default
|
|
77
|
+
def fetch_option(key, default = nil)
|
|
78
|
+
attribute_data.fetch(key, default)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Checks if an option is present and has a meaningful value.
|
|
82
|
+
#
|
|
83
|
+
# Returns false if the key doesn't exist, or if the value is nil
|
|
84
|
+
# or empty (for collections).
|
|
85
|
+
#
|
|
86
|
+
# @param key [Symbol] The option key to check
|
|
87
|
+
# @return [Boolean] True if option exists with non-empty value
|
|
88
|
+
def option_present?(key)
|
|
89
|
+
return false unless attribute_data.key?(key)
|
|
90
|
+
|
|
91
|
+
value = attribute_data[key]
|
|
92
|
+
!value.nil? && (!value.respond_to?(:empty?) || !value.empty?)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Concerns
|
|
8
|
+
# Concern providing helper methods for building error messages in submatchers.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# ErrorMessageBuilder provides consistent formatting methods for constructing
|
|
13
|
+
# readable failure messages. It handles diff-style messages showing expected
|
|
14
|
+
# vs actual values, and list formatting.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Include in submatcher classes:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# class TypesSubmatcher < Base::Submatcher
|
|
22
|
+
# include Concerns::ErrorMessageBuilder
|
|
23
|
+
#
|
|
24
|
+
# def build_failure_message
|
|
25
|
+
# build_diff_message(
|
|
26
|
+
# expected: [String, Integer],
|
|
27
|
+
# actual: [Boolean],
|
|
28
|
+
# prefix: "type mismatch"
|
|
29
|
+
# )
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
# ## Methods Provided
|
|
35
|
+
#
|
|
36
|
+
# - `build_diff_message` - creates expected/got diff output
|
|
37
|
+
# - `format_value` - formats values for display (handles Arrays, Classes, etc.)
|
|
38
|
+
# - `build_list_message` - formats item lists with optional prefix
|
|
39
|
+
module ErrorMessageBuilder
|
|
40
|
+
# Includes InstanceMethods in the including class.
|
|
41
|
+
#
|
|
42
|
+
# @param base [Class] The class including this concern
|
|
43
|
+
# @return [void]
|
|
44
|
+
def self.included(base)
|
|
45
|
+
base.include(InstanceMethods)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Instance methods added by this concern.
|
|
49
|
+
module InstanceMethods
|
|
50
|
+
# Builds a diff-style message showing expected vs actual values.
|
|
51
|
+
#
|
|
52
|
+
# @param expected [Object] The expected value
|
|
53
|
+
# @param actual [Object] The actual value found
|
|
54
|
+
# @param prefix [String] Optional prefix text before the diff
|
|
55
|
+
# @return [String] Formatted diff message
|
|
56
|
+
def build_diff_message(expected:, actual:, prefix: "")
|
|
57
|
+
<<~MESSAGE
|
|
58
|
+
#{prefix}
|
|
59
|
+
expected: #{format_value(expected)}
|
|
60
|
+
got: #{format_value(actual)}
|
|
61
|
+
MESSAGE
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Formats a value for human-readable display.
|
|
65
|
+
#
|
|
66
|
+
# Handles special cases:
|
|
67
|
+
# - Arrays: formats each element recursively
|
|
68
|
+
# - Hashes: uses inspect
|
|
69
|
+
# - Classes: shows class name
|
|
70
|
+
# - nil: shows "nil" string
|
|
71
|
+
# - Others: uses inspect
|
|
72
|
+
#
|
|
73
|
+
# @param value [Object] The value to format
|
|
74
|
+
# @return [String] Formatted string representation
|
|
75
|
+
def format_value(value) # rubocop:disable Metrics/MethodLength
|
|
76
|
+
case value
|
|
77
|
+
when Array
|
|
78
|
+
"[#{value.map { |v| format_value(v) }.join(', ')}]"
|
|
79
|
+
when Hash
|
|
80
|
+
value.inspect
|
|
81
|
+
when Class
|
|
82
|
+
value.name
|
|
83
|
+
when nil
|
|
84
|
+
"nil"
|
|
85
|
+
else # rubocop:disable Lint/DuplicateBranch
|
|
86
|
+
value.inspect
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Builds a comma-separated list message.
|
|
91
|
+
#
|
|
92
|
+
# @param items [Array] Items to list
|
|
93
|
+
# @param prefix [String] Optional prefix before the list
|
|
94
|
+
# @return [String] Formatted list or "(empty)" if no items
|
|
95
|
+
def build_list_message(items, prefix: "")
|
|
96
|
+
return "#{prefix}(empty)" if items.empty?
|
|
97
|
+
|
|
98
|
+
"#{prefix}#{items.join(', ')}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Concerns
|
|
8
|
+
# Concern providing value comparison methods for submatchers.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# ValueComparison provides flexible comparison methods that handle various
|
|
13
|
+
# types of expected values including RSpec matchers, classes (for type checking),
|
|
14
|
+
# arrays, and plain values.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Include in submatcher classes:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# class DefaultSubmatcher < Base::Submatcher
|
|
22
|
+
# include Concerns::ValueComparison
|
|
23
|
+
#
|
|
24
|
+
# def passes?
|
|
25
|
+
# values_match?(@expected_default, fetch_option(:default))
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# ## Comparison Rules
|
|
31
|
+
#
|
|
32
|
+
# - RSpec matchers (respond_to :matches?) - delegates to matcher
|
|
33
|
+
# - Classes - checks if actual is_a?(expected)
|
|
34
|
+
# - Arrays - element-wise recursive comparison
|
|
35
|
+
# - Other values - uses equality (==)
|
|
36
|
+
module ValueComparison
|
|
37
|
+
# Includes InstanceMethods in the including class.
|
|
38
|
+
#
|
|
39
|
+
# @param base [Class] The class including this concern
|
|
40
|
+
# @return [void]
|
|
41
|
+
def self.included(base)
|
|
42
|
+
base.include(InstanceMethods)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Instance methods added by this concern.
|
|
46
|
+
module InstanceMethods
|
|
47
|
+
# Compares expected and actual values with type-aware logic.
|
|
48
|
+
#
|
|
49
|
+
# Supports:
|
|
50
|
+
# - RSpec matchers (anything responding to :matches?)
|
|
51
|
+
# - Class comparison (type checking with is_a?)
|
|
52
|
+
# - Array comparison (recursive element-wise)
|
|
53
|
+
# - Standard equality (==)
|
|
54
|
+
#
|
|
55
|
+
# @param expected [Object] The expected value or matcher
|
|
56
|
+
# @param actual [Object] The actual value to compare
|
|
57
|
+
# @return [Boolean] True if values match
|
|
58
|
+
def values_match?(expected, actual)
|
|
59
|
+
if expected.respond_to?(:matches?)
|
|
60
|
+
expected.matches?(actual)
|
|
61
|
+
elsif expected.is_a?(Class)
|
|
62
|
+
actual.is_a?(expected)
|
|
63
|
+
elsif expected.is_a?(Array) && actual.is_a?(Array)
|
|
64
|
+
arrays_match?(expected, actual)
|
|
65
|
+
else
|
|
66
|
+
expected == actual
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Compares two arrays element-by-element.
|
|
71
|
+
#
|
|
72
|
+
# @param expected [Array] Expected array
|
|
73
|
+
# @param actual [Array] Actual array
|
|
74
|
+
# @return [Boolean] True if arrays match (same size and matching elements)
|
|
75
|
+
def arrays_match?(expected, actual)
|
|
76
|
+
return false unless expected.size == actual.size
|
|
77
|
+
|
|
78
|
+
expected.zip(actual).all? { |e, a| values_match?(e, a) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Compares two type collections for equality (order-independent).
|
|
82
|
+
#
|
|
83
|
+
# @param expected_types [Array<Class>] Expected type classes
|
|
84
|
+
# @param actual_types [Array<Class>] Actual type classes
|
|
85
|
+
# @return [Boolean] True if same set of types
|
|
86
|
+
def types_match?(expected_types, actual_types)
|
|
87
|
+
expected_set = Set.new(expected_types)
|
|
88
|
+
actual_set = Set.new(actual_types)
|
|
89
|
+
expected_set == actual_set
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|