servactory 2.16.1 → 3.0.0.rc2

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 (111) 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/test_kit/fake_type.rb +23 -0
  49. data/lib/servactory/test_kit/result.rb +45 -0
  50. data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
  51. data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
  52. data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
  53. data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
  54. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
  55. data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
  56. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
  57. data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
  58. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
  59. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
  60. data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
  61. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
  62. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
  63. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
  64. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
  65. data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
  66. data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
  67. data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
  68. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
  69. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
  70. data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
  71. data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
  72. data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
  73. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
  74. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
  75. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
  76. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
  77. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
  78. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
  79. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
  80. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
  81. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
  82. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
  83. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
  84. data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
  85. data/lib/servactory/test_kit/utils/faker.rb +62 -2
  86. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
  87. data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
  88. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
  89. data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
  90. data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
  91. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
  92. data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
  93. data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
  94. data/lib/servactory/tool_kit/dynamic_options/target.rb +248 -0
  95. data/lib/servactory/version.rb +4 -4
  96. data/lib/servactory.rb +6 -0
  97. metadata +57 -48
  98. data/lib/generators/servactory/install_generator.rb +0 -21
  99. data/lib/generators/servactory/rspec_generator.rb +0 -88
  100. data/lib/generators/servactory/service_generator.rb +0 -49
  101. data/lib/servactory/configuration/setup.rb +0 -97
  102. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
  103. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
  104. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
  105. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
  106. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
  107. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
  108. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
  109. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
  110. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
  111. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +0 -199
@@ -1,228 +1,98 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "have_service_input_matchers/default_matcher"
4
- require_relative "have_service_input_matchers/optional_matcher"
5
- require_relative "have_service_input_matchers/required_matcher"
6
- require_relative "have_service_input_matchers/valid_with_matcher"
7
-
8
3
  module Servactory
9
4
  module TestKit
10
5
  module Rspec
11
6
  module Matchers
12
- class HaveServiceInputMatcher # rubocop:disable Metrics/ClassLength
13
- attr_reader :described_class, :input_name, :options
14
-
15
- def initialize(described_class, input_name)
16
- @described_class = described_class
17
- @input_name = input_name
18
-
19
- @options = {}
20
- @submatchers = []
21
-
22
- @missing = ""
23
- end
24
-
25
- def supports_block_expectations?
26
- true
27
- end
28
-
29
- def type(type)
30
- @option_types = Array(type)
31
- add_submatcher(
32
- HaveServiceAttributeMatchers::TypesMatcher,
33
- described_class,
34
- :input,
35
- input_name,
36
- @option_types
37
- )
38
- self
39
- end
40
-
41
- def types(*types)
42
- @option_types = types
43
- add_submatcher(
44
- HaveServiceAttributeMatchers::TypesMatcher,
45
- described_class,
46
- :input,
47
- input_name,
48
- @option_types
49
- )
50
- self
51
- end
52
-
53
- def required(custom_message = nil)
54
- remove_submatcher(HaveServiceInputMatchers::OptionalMatcher)
55
- add_submatcher(
56
- HaveServiceInputMatchers::RequiredMatcher,
57
- described_class,
58
- :input,
59
- input_name,
60
- custom_message
61
- )
62
- self
63
- end
64
-
65
- def optional
66
- remove_submatcher(HaveServiceInputMatchers::RequiredMatcher)
67
- add_submatcher(
68
- HaveServiceInputMatchers::OptionalMatcher,
69
- described_class,
70
- :input,
71
- input_name
72
- )
73
- self
74
- end
75
-
76
- def default(value)
77
- add_submatcher(
78
- HaveServiceInputMatchers::DefaultMatcher,
79
- described_class,
80
- :input,
81
- input_name,
82
- value
83
- )
84
- self
85
- end
86
-
87
- def consists_of(*types)
88
- add_submatcher(
89
- HaveServiceAttributeMatchers::ConsistsOfMatcher,
90
- described_class,
91
- :input,
92
- input_name,
93
- @option_types,
94
- Array(types)
95
- )
96
- self
97
- end
98
-
99
- def schema(data = {})
100
- add_submatcher(
101
- HaveServiceAttributeMatchers::SchemaMatcher,
102
- described_class,
103
- :input,
104
- input_name,
105
- @option_types,
106
- data
107
- )
108
- self
109
- end
110
-
111
- def inclusion(values)
112
- add_submatcher(
113
- HaveServiceAttributeMatchers::InclusionMatcher,
114
- described_class,
115
- :input,
116
- input_name,
117
- Array(values)
118
- )
119
- self
120
- end
121
-
122
- def must(*must_names)
123
- add_submatcher(
124
- HaveServiceAttributeMatchers::MustMatcher,
125
- described_class,
126
- :input,
127
- input_name,
128
- Array(must_names)
129
- )
130
- self
131
- end
132
-
133
- def valid_with(attributes)
134
- add_submatcher(
135
- HaveServiceInputMatchers::ValidWithMatcher,
136
- described_class,
137
- :input,
138
- input_name,
139
- attributes
140
- )
141
- self
142
- end
143
-
144
- def message(message)
145
- add_submatcher(
146
- HaveServiceAttributeMatchers::MessageMatcher,
147
- described_class,
148
- :input,
149
- input_name,
150
- @last_submatcher,
151
- message
152
- )
153
- self
154
- end
155
-
156
- # NOTE: Used for delayed chain implementation
157
- # def not_implemented_chain(*description)
158
- # Kernel.warn <<-MESSAGE.squish
159
- # This chain has not yet been implemented.
160
- # This message is for informational purposes only.
161
- # Description: #{description}
162
- # MESSAGE
163
- # self
164
- # end
165
-
166
- def description
167
- "#{input_name} with #{submatchers.map(&:description).join(', ')}"
168
- end
169
-
170
- def failure_message
171
- "Expected #{expectation}, which #{missing_options}"
172
- end
173
-
174
- def failure_message_when_negated
175
- "Did not expect #{expectation} with specified options"
176
- end
177
-
178
- def matches?(subject)
179
- @subject = subject
180
-
181
- submatchers_match?
182
- end
183
-
184
- protected
185
-
186
- attr_reader :last_submatcher, :submatchers, :missing, :subject
187
-
188
- def add_submatcher(matcher_class, *args)
189
- remove_submatcher(matcher_class)
190
- @last_submatcher = matcher_class.new(*args)
191
- submatchers << @last_submatcher
192
- end
193
-
194
- def remove_submatcher(matcher_class)
195
- submatchers.delete_if do |submatcher|
196
- submatcher.is_a?(matcher_class)
197
- end
198
- end
199
-
200
- def expectation
201
- "#{described_class.name} to have a service input attribute named #{input_name}"
202
- end
203
-
204
- def missing_options
205
- missing_options = [missing, missing_options_for_failing_submatchers]
206
- missing_options.flatten.select(&:present?).join(", ")
207
- end
208
-
209
- def failing_submatchers
210
- @failing_submatchers ||= submatchers.reject do |matcher|
211
- matcher.matches?(subject)
212
- end
213
- end
214
-
215
- def missing_options_for_failing_submatchers
216
- if defined?(failing_submatchers)
217
- failing_submatchers.map(&:missing_option)
218
- else
219
- []
220
- end
221
- end
222
-
223
- def submatchers_match?
224
- failing_submatchers.empty?
225
- end
7
+ # RSpec matcher for validating Servactory service input definitions.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Validates that a service class has the expected input attribute with
12
+ # specified type, required status, defaults, and other options.
13
+ #
14
+ # ## Usage
15
+ #
16
+ # ```ruby
17
+ # RSpec.describe MyService, type: :service do
18
+ # it { is_expected.to have_service_input(:user_id).type(Integer).required }
19
+ # it { is_expected.to have_service_input(:name).type(String).optional }
20
+ # it { is_expected.to have_service_input(:age).type(Integer).default(18) }
21
+ # it { is_expected.to have_service_input(:status).inclusion(%w[active inactive]) }
22
+ # end
23
+ # ```
24
+ #
25
+ # ## Chain Methods
26
+ #
27
+ # - `.type(Class)` / `.types(Class, ...)` - expected type(s)
28
+ # - `.required` / `.optional` - required status
29
+ # - `.default(value)` - expected default value
30
+ # - `.consists_of(Class)` - for Array/Hash element types
31
+ # - `.schema(Hash)` - expected schema definition
32
+ # - `.inclusion(Array)` - expected inclusion values
33
+ # - `.must(Array)` - custom validation rules
34
+ # - `.target(value, name:)` - target validation
35
+ # - `.message(String)` - expected error message (after other chain)
36
+ class HaveServiceInputMatcher < Base::AttributeMatcher
37
+ for_attribute_type :input
38
+
39
+ # Shared submatchers
40
+ register_submatcher :types,
41
+ class_name: "Shared::TypesSubmatcher",
42
+ chain_method: :type,
43
+ chain_aliases: [:types],
44
+ transform_args: ->(args, _kwargs = {}) { [Array(args).flatten] },
45
+ stores_option_types: true
46
+
47
+ register_submatcher :consists_of,
48
+ class_name: "Shared::ConsistsOfSubmatcher",
49
+ transform_args: ->(args, _kwargs = {}) { [Array(args).flatten] },
50
+ requires_option_types: true
51
+
52
+ register_submatcher :schema,
53
+ class_name: "Shared::SchemaSubmatcher",
54
+ transform_args: ->(args, _kwargs = {}) { [args.first || {}] },
55
+ requires_option_types: true
56
+
57
+ register_submatcher :inclusion,
58
+ class_name: "Shared::InclusionSubmatcher",
59
+ transform_args: (lambda do |args, _kwargs = {}|
60
+ [args.first.is_a?(Range) ? args.first : Array(args.first)]
61
+ end)
62
+
63
+ register_submatcher :target,
64
+ class_name: "Shared::TargetSubmatcher",
65
+ transform_args: (lambda do |args, kwargs = {}|
66
+ [kwargs.fetch(:name, :target), Array(args.first)]
67
+ end),
68
+ accepts_trailing_options: true
69
+
70
+ register_submatcher :must,
71
+ class_name: "Shared::MustSubmatcher",
72
+ transform_args: ->(args, _kwargs = {}) { [Array(args).flatten] }
73
+
74
+ register_submatcher :message,
75
+ class_name: "Shared::MessageSubmatcher",
76
+ transform_args: ->(args, _kwargs = {}) { [args.first] },
77
+ requires_last_submatcher: true
78
+
79
+ # Input-specific submatchers
80
+ register_submatcher :required,
81
+ class_name: "Input::RequiredSubmatcher",
82
+ transform_args: ->(args, _kwargs = {}) { [args.first] },
83
+ mutually_exclusive_with: [:optional]
84
+
85
+ register_submatcher :optional,
86
+ class_name: "Input::OptionalSubmatcher",
87
+ mutually_exclusive_with: [:required]
88
+
89
+ register_submatcher :default,
90
+ class_name: "Input::DefaultSubmatcher",
91
+ transform_args: ->(args, _kwargs = {}) { [args.first] }
92
+
93
+ register_submatcher :valid_with,
94
+ class_name: "Input::ValidWithSubmatcher",
95
+ transform_args: ->(args, _kwargs = {}) { [args.first] }
226
96
  end
227
97
  end
228
98
  end
@@ -1,175 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "have_service_attribute_matchers/types_matcher"
4
- require_relative "have_service_attribute_matchers/consists_of_matcher"
5
- require_relative "have_service_attribute_matchers/schema_matcher"
6
- require_relative "have_service_attribute_matchers/inclusion_matcher"
7
- require_relative "have_service_attribute_matchers/must_matcher"
8
- require_relative "have_service_attribute_matchers/message_matcher"
9
-
10
3
  module Servactory
11
4
  module TestKit
12
5
  module Rspec
13
6
  module Matchers
14
- class HaveServiceInternalMatcher # rubocop:disable Metrics/ClassLength
15
- attr_reader :described_class, :internal_name, :options
16
-
17
- def initialize(described_class, internal_name)
18
- @described_class = described_class
19
- @internal_name = internal_name
20
-
21
- @options = {}
22
- @submatchers = []
23
-
24
- @missing = ""
25
- end
26
-
27
- def supports_block_expectations?
28
- true
29
- end
30
-
31
- def type(type)
32
- @option_types = Array(type)
33
- add_submatcher(
34
- HaveServiceAttributeMatchers::TypesMatcher,
35
- described_class,
36
- :internal,
37
- internal_name,
38
- @option_types
39
- )
40
- self
41
- end
42
-
43
- def types(*types)
44
- @option_types = types
45
- add_submatcher(
46
- HaveServiceAttributeMatchers::TypesMatcher,
47
- described_class,
48
- :internal,
49
- internal_name,
50
- @option_types
51
- )
52
- self
53
- end
54
-
55
- def consists_of(*types)
56
- add_submatcher(
57
- HaveServiceAttributeMatchers::ConsistsOfMatcher,
58
- described_class,
59
- :internal,
60
- internal_name,
61
- @option_types,
62
- Array(types)
63
- )
64
- self
65
- end
66
-
67
- def schema(data = {})
68
- add_submatcher(
69
- HaveServiceAttributeMatchers::SchemaMatcher,
70
- described_class,
71
- :internal,
72
- internal_name,
73
- @option_types,
74
- data
75
- )
76
- self
77
- end
78
-
79
- def inclusion(values)
80
- add_submatcher(
81
- HaveServiceAttributeMatchers::InclusionMatcher,
82
- described_class,
83
- :internal,
84
- internal_name,
85
- Array(values)
86
- )
87
- self
88
- end
89
-
90
- def must(*must_names)
91
- add_submatcher(
92
- HaveServiceAttributeMatchers::MustMatcher,
93
- described_class,
94
- :internal,
95
- internal_name,
96
- Array(must_names)
97
- )
98
- self
99
- end
100
-
101
- def message(message)
102
- add_submatcher(
103
- HaveServiceAttributeMatchers::MessageMatcher,
104
- described_class,
105
- :internal,
106
- internal_name,
107
- @last_submatcher,
108
- message
109
- )
110
- self
111
- end
112
-
113
- def description
114
- "#{internal_name} with #{submatchers.map(&:description).join(', ')}"
115
- end
116
-
117
- def failure_message
118
- "Expected #{expectation}, which #{missing_options}"
119
- end
120
-
121
- def failure_message_when_negated
122
- "Did not expect #{expectation} with specified options"
123
- end
124
-
125
- def matches?(subject)
126
- @subject = subject
127
-
128
- submatchers_match?
129
- end
130
-
131
- protected
132
-
133
- attr_reader :last_submatcher, :submatchers, :missing, :subject
134
-
135
- def add_submatcher(matcher_class, *args)
136
- remove_submatcher(matcher_class)
137
- @last_submatcher = matcher_class.new(*args)
138
- submatchers << @last_submatcher
139
- end
140
-
141
- def remove_submatcher(matcher_class)
142
- submatchers.delete_if do |submatcher|
143
- submatcher.is_a?(matcher_class)
144
- end
145
- end
146
-
147
- def expectation
148
- "#{described_class.name} to have a service internal attribute named #{internal_name}"
149
- end
150
-
151
- def missing_options
152
- missing_options = [missing, missing_options_for_failing_submatchers]
153
- missing_options.flatten.select(&:present?).join(", ")
154
- end
155
-
156
- def failing_submatchers
157
- @failing_submatchers ||= submatchers.reject do |matcher|
158
- matcher.matches?(subject)
159
- end
160
- end
161
-
162
- def missing_options_for_failing_submatchers
163
- if defined?(failing_submatchers)
164
- failing_submatchers.map(&:missing_option)
165
- else
166
- []
167
- end
168
- end
169
-
170
- def submatchers_match?
171
- failing_submatchers.empty?
172
- end
7
+ # RSpec matcher for validating Servactory service internal definitions.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Validates that a service class has the expected internal attribute with
12
+ # specified type, schema, and other options. Internal attributes are used
13
+ # for intermediate values during service execution.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # ```ruby
18
+ # RSpec.describe MyService, type: :service do
19
+ # it { is_expected.to have_service_internal(:processed_data).type(Hash) }
20
+ # it { is_expected.to have_service_internal(:items).type(Array).consists_of(Item) }
21
+ # it { is_expected.to have_service_internal(:config).schema({ key: String }) }
22
+ # end
23
+ # ```
24
+ #
25
+ # ## Chain Methods
26
+ #
27
+ # - `.type(Class)` / `.types(Class, ...)` - expected type(s)
28
+ # - `.consists_of(Class)` - for Array/Hash element types
29
+ # - `.schema(Hash)` - expected schema definition
30
+ # - `.inclusion(Array)` - expected inclusion values
31
+ # - `.must(Array)` - custom validation rules
32
+ # - `.target(value, name:)` - target validation
33
+ # - `.message(String)` - expected error message (after other chain)
34
+ #
35
+ # ## Architecture
36
+ #
37
+ # Inherits from Base::AttributeMatcher with `for_attribute_type :internal`.
38
+ # Uses shared submatchers for common validations.
39
+ class HaveServiceInternalMatcher < Base::AttributeMatcher
40
+ for_attribute_type :internal
41
+
42
+ # Shared submatchers
43
+ register_submatcher :types,
44
+ class_name: "Shared::TypesSubmatcher",
45
+ chain_method: :type,
46
+ chain_aliases: [:types],
47
+ transform_args: ->(args, _kwargs = {}) { [Array(args).flatten] },
48
+ stores_option_types: true
49
+
50
+ register_submatcher :consists_of,
51
+ class_name: "Shared::ConsistsOfSubmatcher",
52
+ transform_args: ->(args, _kwargs = {}) { [Array(args).flatten] },
53
+ requires_option_types: true
54
+
55
+ register_submatcher :schema,
56
+ class_name: "Shared::SchemaSubmatcher",
57
+ transform_args: ->(args, _kwargs = {}) { [args.first || {}] },
58
+ requires_option_types: true
59
+
60
+ register_submatcher :inclusion,
61
+ class_name: "Shared::InclusionSubmatcher",
62
+ transform_args: (lambda do |args, _kwargs = {}|
63
+ [args.first.is_a?(Range) ? args.first : Array(args.first)]
64
+ end)
65
+
66
+ register_submatcher :target,
67
+ class_name: "Shared::TargetSubmatcher",
68
+ transform_args: (lambda do |args, kwargs = {}|
69
+ [kwargs.fetch(:name, :target), Array(args.first)]
70
+ end),
71
+ accepts_trailing_options: true
72
+
73
+ register_submatcher :must,
74
+ class_name: "Shared::MustSubmatcher",
75
+ transform_args: ->(args, _kwargs = {}) { [Array(args).flatten] }
76
+
77
+ register_submatcher :message,
78
+ class_name: "Shared::MessageSubmatcher",
79
+ transform_args: ->(args, _kwargs = {}) { [args.first] },
80
+ requires_last_submatcher: true
173
81
  end
174
82
  end
175
83
  end