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,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