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
@@ -3,56 +3,219 @@
3
3
  module Servactory
4
4
  module ToolKit
5
5
  module DynamicOptions
6
+ # Validates Hash structures against a defined schema.
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Schema provides deep validation for Hash-type attributes by checking
11
+ # that nested keys exist with correct types. It supports required/optional
12
+ # fields, default values, and nested object validation. This is essential
13
+ # for validating complex data structures like API payloads or configurations.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # This option is **included by default** for inputs, internals, and outputs.
18
+ # No registration required for basic usage.
19
+ #
20
+ # To extend supported Hash-compatible types, use the
21
+ # `hash_mode_class_names` configuration:
22
+ #
23
+ # ```ruby
24
+ # configuration do
25
+ # hash_mode_class_names([CustomHashClass])
26
+ # end
27
+ # ```
28
+ #
29
+ # Define schema in your service:
30
+ #
31
+ # ```ruby
32
+ # class CreateUserService < ApplicationService::Base
33
+ # input :user_data,
34
+ # type: Hash,
35
+ # schema: {
36
+ # name: { type: String },
37
+ # age: { type: Integer, required: false, default: 18 },
38
+ # address: {
39
+ # type: Hash,
40
+ # street: { type: String },
41
+ # city: { type: String, required: false }
42
+ # }
43
+ # }
44
+ # end
45
+ # ```
46
+ #
47
+ # ## Simple Mode
48
+ #
49
+ # Specify schema definition directly:
50
+ #
51
+ # ```ruby
52
+ # class CreateUserService < ApplicationService::Base
53
+ # input :user_data,
54
+ # type: Hash,
55
+ # schema: {
56
+ # name: { type: String },
57
+ # age: { type: Integer, required: false },
58
+ # email: { type: String }
59
+ # }
60
+ # end
61
+ # ```
62
+ #
63
+ # ## Advanced Mode
64
+ #
65
+ # Specify schema with custom error message using a hash:
66
+ #
67
+ # With static message:
68
+ #
69
+ # ```ruby
70
+ # input :user_data, type: Hash, schema: {
71
+ # is: {
72
+ # name: { type: String },
73
+ # email: { type: String }
74
+ # },
75
+ # message: "Input `user_data` has invalid structure"
76
+ # }
77
+ # ```
78
+ #
79
+ # With dynamic lambda message:
80
+ #
81
+ # ```ruby
82
+ # input :user_data, type: Hash, schema: {
83
+ # is: {
84
+ # name: { type: String },
85
+ # email: { type: String }
86
+ # },
87
+ # message: lambda do |input:, reason:, key_name:, expected_type:, given_type:, **|
88
+ # "Schema error in `#{input.name}`: " \
89
+ # "key `#{key_name}` expected #{expected_type}, got #{given_type}"
90
+ # end
91
+ # }
92
+ # ```
93
+ #
94
+ # Lambda receives the following parameters:
95
+ # - For inputs: `input:, reason:, key_name:, expected_type:, given_type:, **`
96
+ # - For internals: `internal:, reason:, key_name:, expected_type:, given_type:, **`
97
+ # - For outputs: `output:, reason:, key_name:, expected_type:, given_type:, **`
98
+ #
99
+ # Use `schema: false` to disable schema validation.
100
+ #
101
+ # ## Schema Options
102
+ #
103
+ # Each field in the schema supports:
104
+ # - `type` - Expected type (String, Integer, Array, Hash, etc.)
105
+ # - `required` - Whether field is required (default: true)
106
+ # - `default` - Default value when field is missing
107
+ # - `prepare` - Proc to transform the value (inputs only)
108
+ #
109
+ # ## Processing Flow
110
+ #
111
+ # 1. **Type check**: Verify attribute is Hash-compatible
112
+ # 2. **Schema validation**: Recursively check all nested keys and types
113
+ # 3. **Default application**: Apply defaults to missing optional fields
114
+ # 4. **Preparation**: Execute prepare callbacks (inputs only)
115
+ #
116
+ # ## Important Notes
117
+ #
118
+ # - Empty values skip validation for: optional inputs, all internal/output attributes
119
+ # - Nested Hash types are validated recursively
120
+ # - The `prepare` option is stripped for internals and outputs
121
+ # - Reserved options: :type, :required, :default, :prepare
6
122
  class Schema < Must # rubocop:disable Metrics/ClassLength
7
- RESERVED_OPTIONS = %i[type required default payload].freeze
123
+ # Reserved keys that are not treated as nested schema definitions.
124
+ RESERVED_OPTIONS = %i[type required default prepare].freeze
8
125
  private_constant :RESERVED_OPTIONS
9
126
 
127
+ # Creates a Schema validator instance.
128
+ #
129
+ # @param option_name [Symbol] The option name (default: :schema)
130
+ # @param default_hash_mode_class_names [Array<Class>] Valid Hash-like types
131
+ # @return [Servactory::Maintenance::Attributes::OptionHelper]
10
132
  def self.use(option_name = :schema, default_hash_mode_class_names:)
11
133
  instance = new(option_name, :is, false)
12
134
  instance.assign(default_hash_mode_class_names)
13
135
  instance.must(:schema)
14
136
  end
15
137
 
138
+ # Assigns the list of valid Hash-compatible class names.
139
+ #
140
+ # @param default_hash_mode_class_names [Array<Class>] Hash-like types to accept
141
+ # @return [void]
16
142
  def assign(default_hash_mode_class_names)
17
143
  @default_hash_mode_class_names = default_hash_mode_class_names
18
144
  end
19
145
 
146
+ # Validates schema condition for input attribute.
147
+ #
148
+ # @param input [Object] Input attribute object
149
+ # @param value [Object] Hash value to validate
150
+ # @param option [WorkOption] Schema configuration
151
+ # @return [Boolean, Array] true if valid, or [false, reason, meta]
20
152
  def condition_for_input_with(input:, value:, option:)
21
153
  common_condition_with(attribute: input, value:, option:)
22
154
  end
23
155
 
156
+ # Validates schema condition for internal attribute.
157
+ #
158
+ # @param internal [Object] Internal attribute object
159
+ # @param value [Object] Hash value to validate
160
+ # @param option [WorkOption] Schema configuration
161
+ # @return [Boolean, Array] true if valid, or [false, reason, meta]
24
162
  def condition_for_internal_with(internal:, value:, option:)
25
163
  common_condition_with(attribute: internal, value:, option:)
26
164
  end
27
165
 
166
+ # Validates schema condition for output attribute.
167
+ #
168
+ # @param output [Object] Output attribute object
169
+ # @param value [Object] Hash value to validate
170
+ # @param option [WorkOption] Schema configuration
171
+ # @return [Boolean, Array] true if valid, or [false, reason, meta]
28
172
  def condition_for_output_with(output:, value:, option:)
29
173
  common_condition_with(attribute: output, value:, option:)
30
174
  end
31
175
 
176
+ # Common validation logic for all attribute types.
177
+ #
178
+ # @param attribute [Object] The attribute being validated
179
+ # @param value [Object] Hash value to validate
180
+ # @param option [WorkOption] Schema configuration
181
+ # @return [Boolean, Array] true if valid, or [false, reason, meta]
32
182
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
33
183
  def common_condition_with(attribute:, value:, option:)
184
+ # Schema disabled, skip validation.
34
185
  return true if option.value == false
186
+
187
+ # Attribute type must be Hash-compatible.
35
188
  return [false, :wrong_type] unless @default_hash_mode_class_names.intersect?(attribute.types)
36
189
 
190
+ # Skip validation for blank optional values.
37
191
  if value.blank? && ((attribute.input? && attribute.optional?) || attribute.internal? || attribute.output?)
38
192
  return true
39
193
  end
40
194
 
41
195
  schema = option.value.fetch(:is, option.value)
42
196
 
197
+ # Remove :prepare option for internals and outputs.
43
198
  if attribute.internal? || attribute.output?
44
199
  schema = schema.transform_values { |options| options.except(:prepare) }
45
200
  end
46
201
 
47
202
  is_success, reason, meta = validate_for!(object: value, schema:)
48
203
 
204
+ # Apply defaults and preparations if validation passed.
49
205
  prepare_object_with!(object: value, schema:) if is_success
50
206
 
51
207
  [is_success, reason, meta]
52
208
  end
53
209
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
54
210
 
211
+ # Recursively validates object against schema definition.
212
+ #
213
+ # @param object [Hash] The object to validate
214
+ # @param schema [Hash] Schema definition
215
+ # @param root_schema_key [Symbol, nil] Parent key for nested validation
216
+ # @return [Boolean, Array] true if valid, or [false, reason, meta]
55
217
  def validate_for!(object:, schema:, root_schema_key: nil) # rubocop:disable Metrics/MethodLength
218
+ # Object must be Hash-like (respond to :fetch).
56
219
  unless object.respond_to?(:fetch)
57
220
  return [
58
221
  false,
@@ -65,10 +228,12 @@ module Servactory
65
228
  ]
66
229
  end
67
230
 
231
+ # Validate each schema field.
68
232
  errors = schema.map do |schema_key, schema_value|
69
233
  attribute_type = schema_value.fetch(:type, String)
70
234
 
71
235
  if attribute_type == Hash
236
+ # Recursively validate nested Hash.
72
237
  validate_for!(
73
238
  object: object.fetch(schema_key, {}),
74
239
  schema: schema_value.except(*RESERVED_OPTIONS),
@@ -89,10 +254,20 @@ module Servactory
89
254
  end
90
255
  end
91
256
 
257
+ # Return first error or true.
92
258
  errors.compact.first || true
93
259
  end
94
260
 
261
+ # Validates a single field against its type specification.
262
+ #
263
+ # @param object [Hash] Parent object containing the field
264
+ # @param schema_key [Symbol] Field key to validate
265
+ # @param schema_value [Hash] Field schema definition
266
+ # @param attribute_type [Class, Array<Class>] Expected type(s)
267
+ # @param attribute_required [Boolean] Whether field is required
268
+ # @return [Array<Boolean, String>] [success, given_type_name]
95
269
  def validate_with(object:, schema_key:, schema_value:, attribute_type:, attribute_required:) # rubocop:disable Metrics/MethodLength
270
+ # Skip validation if not required and no value present.
96
271
  unless should_be_checked_for?(
97
272
  object:,
98
273
  schema_key:,
@@ -111,6 +286,13 @@ module Servactory
111
286
  ]
112
287
  end
113
288
 
289
+ # Determines if a field should be validated.
290
+ #
291
+ # @param object [Hash] Parent object
292
+ # @param schema_key [Symbol] Field key
293
+ # @param schema_value [Hash] Field schema
294
+ # @param required [Boolean] Whether required
295
+ # @return [Boolean] true if validation needed
114
296
  def should_be_checked_for?(object:, schema_key:, schema_value:, required:)
115
297
  required || (
116
298
  !required && !fetch_default_from(schema_value).nil?
@@ -119,6 +301,12 @@ module Servactory
119
301
  )
120
302
  end
121
303
 
304
+ # Prepares value for validation, applying defaults if needed.
305
+ #
306
+ # @param schema_value [Hash] Field schema
307
+ # @param value [Object] Current value
308
+ # @param required [Boolean] Whether required
309
+ # @return [Object] Value to validate
122
310
  def prepare_value_from(schema_value:, value:, required:)
123
311
  if !required && !fetch_default_from(schema_value).nil? && value.blank?
124
312
  fetch_default_from(schema_value)
@@ -127,36 +315,50 @@ module Servactory
127
315
  end
128
316
  end
129
317
 
318
+ # Extracts default value from schema definition.
319
+ #
320
+ # @param value [Hash] Schema definition
321
+ # @return [Object, nil] Default value or nil
130
322
  def fetch_default_from(value)
131
323
  value.fetch(:default, nil)
132
324
  end
133
325
 
134
326
  ########################################################################
135
327
 
136
- def prepare_object_with!(object:, schema:) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
328
+ # Applies defaults and preparations to the validated object.
329
+ #
330
+ # @param object [Hash] Object to modify
331
+ # @param schema [Hash] Schema definition
332
+ # @return [void]
333
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
334
+ def prepare_object_with!(object:, schema:)
137
335
  schema.map do |schema_key, schema_value|
138
336
  attribute_type = schema_value.fetch(:type, String)
139
337
  required = schema_value.fetch(:required, true)
140
338
  object_value = object[schema_key]
141
339
 
142
340
  if attribute_type == Hash
341
+ # Apply nested Hash defaults.
143
342
  default_value = schema_value.fetch(:default, {})
144
343
 
145
344
  if !required && !default_value.nil? && !Servactory::Utils.value_present?(object_value)
146
345
  object[schema_key] = default_value
147
346
  end
148
347
 
348
+ # Recursively prepare nested objects.
149
349
  prepare_object_with!(
150
350
  object: object.fetch(schema_key, {}),
151
351
  schema: schema_value.except(*RESERVED_OPTIONS)
152
352
  )
153
353
  else
354
+ # Apply scalar defaults.
154
355
  default_value = schema_value.fetch(:default, nil)
155
356
 
156
357
  if !required && !default_value.nil? && !Servactory::Utils.value_present?(object_value)
157
358
  object[schema_key] = default_value
158
359
  end
159
360
 
361
+ # Execute prepare callback if defined.
160
362
  unless (input_prepare = schema_value.fetch(:prepare, nil)).nil?
161
363
  object[schema_key] = input_prepare.call(value: object[schema_key])
162
364
  end
@@ -165,11 +367,19 @@ module Servactory
165
367
  end
166
368
  end
167
369
  end
370
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
168
371
 
169
372
  ########################################################################
170
373
  ########################################################################
171
374
  ########################################################################
172
375
 
376
+ # Generates error message for input validation failure.
377
+ #
378
+ # @param service [Object] Service context
379
+ # @param input [Object] Input attribute
380
+ # @param reason [Symbol] Failure reason
381
+ # @param meta [Hash] Additional metadata
382
+ # @return [String] Localized error message
173
383
  def message_for_input_with(service:, input:, reason:, meta:, **)
174
384
  i18n_key = "inputs.validations.must.dynamic_options.schema"
175
385
  i18n_key += reason.present? ? ".#{reason}" : ".default"
@@ -184,6 +394,13 @@ module Servactory
184
394
  )
185
395
  end
186
396
 
397
+ # Generates error message for internal validation failure.
398
+ #
399
+ # @param service [Object] Service context
400
+ # @param internal [Object] Internal attribute
401
+ # @param reason [Symbol] Failure reason
402
+ # @param meta [Hash] Additional metadata
403
+ # @return [String] Localized error message
187
404
  def message_for_internal_with(service:, internal:, reason:, meta:, **)
188
405
  i18n_key = "internals.validations.must.dynamic_options.schema"
189
406
  i18n_key += reason.present? ? ".#{reason}" : ".default"
@@ -198,6 +415,13 @@ module Servactory
198
415
  )
199
416
  end
200
417
 
418
+ # Generates error message for output validation failure.
419
+ #
420
+ # @param service [Object] Service context
421
+ # @param output [Object] Output attribute
422
+ # @param reason [Symbol] Failure reason
423
+ # @param meta [Hash] Additional metadata
424
+ # @return [String] Localized error message
201
425
  def message_for_output_with(service:, output:, reason:, meta:, **)
202
426
  i18n_key = "outputs.validations.must.dynamic_options.schema"
203
427
  i18n_key += reason.present? ? ".#{reason}" : ".default"
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module ToolKit
5
+ module DynamicOptions
6
+ # Validates that attribute value matches one of the target values.
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Target provides exact value matching validation for attributes.
11
+ # It ensures that the value is one of the specified target values,
12
+ # supporting both single values and arrays of acceptable values.
13
+ # This is useful for enum-like validations where only specific
14
+ # values are allowed.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # This option is **NOT included by default**. Register it for each
19
+ # attribute type where you want to use it:
20
+ #
21
+ # ```ruby
22
+ # configuration do
23
+ # input_option_helpers([
24
+ # Servactory::ToolKit::DynamicOptions::Target.use
25
+ # ])
26
+ #
27
+ # internal_option_helpers([
28
+ # Servactory::ToolKit::DynamicOptions::Target.use
29
+ # ])
30
+ #
31
+ # output_option_helpers([
32
+ # Servactory::ToolKit::DynamicOptions::Target.use
33
+ # ])
34
+ # end
35
+ # ```
36
+ #
37
+ # Use in your service definition:
38
+ #
39
+ # ```ruby
40
+ # class ProcessOrderService < ApplicationService::Base
41
+ # input :status, type: Symbol, target: { in: [:pending, :processing, :complete] }
42
+ # input :priority, type: Integer, target: { in: [1, 2, 3] }
43
+ # input :model_class, type: Class, target: { in: [User, Admin] }
44
+ # end
45
+ # ```
46
+ #
47
+ # ## Simple Mode
48
+ #
49
+ # Specify target values directly:
50
+ #
51
+ # ```ruby
52
+ # class ProcessOrderService < ApplicationService::Base
53
+ # input :status, type: Symbol, target: :pending
54
+ # input :priority, type: Integer, target: [1, 2, 3]
55
+ # input :model_class, type: Class, target: [User, Admin]
56
+ # end
57
+ # ```
58
+ #
59
+ # ## Advanced Mode
60
+ #
61
+ # Specify target with custom error message using a hash.
62
+ # Note: Advanced mode uses `:in` key (not `:is`).
63
+ #
64
+ # With static message:
65
+ #
66
+ # ```ruby
67
+ # input :status, type: Symbol, target: {
68
+ # in: [:pending, :processing, :complete],
69
+ # message: "Input `status` must be one of: pending, processing, complete"
70
+ # }
71
+ # ```
72
+ #
73
+ # With dynamic lambda message:
74
+ #
75
+ # ```ruby
76
+ # input :status, type: Symbol, target: {
77
+ # in: [:pending, :processing, :complete],
78
+ # message: lambda do |input:, value:, option_value:, **|
79
+ # "Input `#{input.name}` has invalid value `#{value}`, expected: #{option_value.inspect}"
80
+ # end
81
+ # }
82
+ # ```
83
+ #
84
+ # Lambda receives the following parameters:
85
+ # - For inputs: `input:, value:, option_value:, reason:, **`
86
+ # - For internals: `internal:, value:, option_value:, reason:, **`
87
+ # - For outputs: `output:, value:, option_value:, reason:, **`
88
+ #
89
+ # ## Validation Rules
90
+ #
91
+ # - Value must exactly match one of the target values
92
+ # - Supports single value or array of values
93
+ # - Optional inputs with nil value validate against default
94
+ #
95
+ # ## Important Notes
96
+ #
97
+ # - Use `target: { in: [...] }` syntax for specifying allowed values
98
+ # - Returns `:invalid_option` error if target is nil
99
+ # - For optional inputs with nil value and default, validates the default
100
+ # - Internal/output attributes do NOT have default value handling (unlike inputs)
101
+ # - For Class-typed attributes, arrays of classes are preserved as-is rather
102
+ # than being wrapped (e.g., `[User, Admin]` stays as array, not `[[User, Admin]]`)
103
+ class Target < Must
104
+ # Creates a Target validator instance.
105
+ #
106
+ # @param option_name [Symbol] The option name (default: :target)
107
+ # @return [Servactory::Maintenance::Attributes::OptionHelper]
108
+ def self.use(option_name = :target)
109
+ instance = new(option_name, :in)
110
+ instance.must(:"be_#{option_name}")
111
+ end
112
+
113
+ # Validates target condition for input attribute.
114
+ #
115
+ # @param input [Object] Input attribute object
116
+ # @param value [Object] Value to validate
117
+ # @param option [WorkOption] Target configuration
118
+ # @return [Boolean, Array] true if valid, or [false, reason]
119
+ def condition_for_input_with(input:, value:, option:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
120
+ return [false, :invalid_option] if option.value.nil?
121
+
122
+ target_values = normalize_target_values(option.value, input.types)
123
+
124
+ # Required inputs or optional with non-nil value.
125
+ return target_values.include?(value) if input.required? || (input.optional? && !value.nil?)
126
+
127
+ # Optional with nil value but has default.
128
+ return target_values.include?(input.default) if input.optional? && value.nil? && !input.default.nil?
129
+
130
+ true
131
+ end
132
+
133
+ # Validates target condition for internal attribute.
134
+ #
135
+ # @param internal [Object] Internal attribute object
136
+ # @param value [Object] Value to validate
137
+ # @param option [WorkOption] Target configuration
138
+ # @return [Boolean, Array] true if valid, or [false, reason]
139
+ def condition_for_internal_with(value:, option:, internal: nil, **)
140
+ return [false, :invalid_option] if option.value.nil?
141
+
142
+ target_values = normalize_target_values(option.value, internal.types)
143
+
144
+ target_values.include?(value)
145
+ end
146
+
147
+ # Validates target condition for output attribute.
148
+ #
149
+ # @param output [Object] Output attribute object
150
+ # @param value [Object] Value to validate
151
+ # @param option [WorkOption] Target configuration
152
+ # @return [Boolean, Array] true if valid, or [false, reason]
153
+ def condition_for_output_with(value:, option:, output: nil, **)
154
+ return [false, :invalid_option] if option.value.nil?
155
+
156
+ target_values = normalize_target_values(option.value, output.types)
157
+
158
+ target_values.include?(value)
159
+ end
160
+
161
+ ########################################################################
162
+
163
+ # Generates error message for input validation failure.
164
+ #
165
+ # @param service [Object] Service context
166
+ # @param input [Object] Input attribute
167
+ # @param value [Object] Failed value
168
+ # @param option_name [Symbol] Option name
169
+ # @param option_value [Object] Expected target values
170
+ # @param reason [Symbol] Failure reason
171
+ # @return [String] Localized error message
172
+ def message_for_input_with(service:, input:, value:, option_name:, option_value:, reason:, **)
173
+ i18n_key = "inputs.validations.must.dynamic_options.target"
174
+ i18n_key += reason.present? ? ".#{reason}" : ".default"
175
+
176
+ service.translate(
177
+ i18n_key,
178
+ input_name: input.name,
179
+ value: value.inspect,
180
+ expected_target: option_value.inspect,
181
+ option_name:
182
+ )
183
+ end
184
+
185
+ # Generates error message for internal validation failure.
186
+ #
187
+ # @param service [Object] Service context
188
+ # @param internal [Object] Internal attribute
189
+ # @param value [Object] Failed value
190
+ # @param option_name [Symbol] Option name
191
+ # @param option_value [Object] Expected target values
192
+ # @param reason [Symbol] Failure reason
193
+ # @return [String] Localized error message
194
+ def message_for_internal_with(service:, internal:, value:, option_name:, option_value:, reason:, **)
195
+ i18n_key = "internals.validations.must.dynamic_options.target"
196
+ i18n_key += reason.present? ? ".#{reason}" : ".default"
197
+
198
+ service.translate(
199
+ i18n_key,
200
+ internal_name: internal.name,
201
+ value: value.inspect,
202
+ expected_target: option_value.inspect,
203
+ option_name:
204
+ )
205
+ end
206
+
207
+ # Generates error message for output validation failure.
208
+ #
209
+ # @param service [Object] Service context
210
+ # @param output [Object] Output attribute
211
+ # @param value [Object] Failed value
212
+ # @param option_name [Symbol] Option name
213
+ # @param option_value [Object] Expected target values
214
+ # @param reason [Symbol] Failure reason
215
+ # @return [String] Localized error message
216
+ def message_for_output_with(service:, output:, value:, option_name:, option_value:, reason:, **)
217
+ i18n_key = "outputs.validations.must.dynamic_options.target"
218
+ i18n_key += reason.present? ? ".#{reason}" : ".default"
219
+
220
+ service.translate(
221
+ i18n_key,
222
+ output_name: output.name,
223
+ value: value.inspect,
224
+ expected_target: option_value.inspect,
225
+ option_name:
226
+ )
227
+ end
228
+
229
+ private
230
+
231
+ # Normalizes target values into array format.
232
+ #
233
+ # Handles special case for Class types where arrays should
234
+ # be preserved as-is rather than wrapped.
235
+ #
236
+ # @param option_value [Object] Target value(s)
237
+ # @param types [Array<Class>] Attribute types
238
+ # @return [Array] Normalized array of target values
239
+ def normalize_target_values(option_value, types)
240
+ # Special handling for Class type attributes.
241
+ if types.size == 1 && types.first == Class
242
+ return [option_value] unless option_value.is_a?(Array)
243
+
244
+ return option_value
245
+ end
246
+
247
+ option_value.is_a?(Array) ? option_value : [option_value]
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Servactory
4
4
  module VERSION
5
- MAJOR = 2
6
- MINOR = 16
7
- PATCH = 1
8
- PRE = nil
5
+ MAJOR = 3
6
+ MINOR = 0
7
+ PATCH = 0
8
+ PRE = "rc1"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
data/lib/servactory.rb CHANGED
@@ -14,6 +14,10 @@ loader.inflector.inflect(
14
14
  )
15
15
  loader.setup
16
16
 
17
+ # Eager load DSL to initialize Stroma::Registry.
18
+ # Registry must be populated before any service class is defined.
19
+ require_relative "servactory/dsl"
20
+
17
21
  module Servactory; end
18
22
 
19
23
  require "servactory/engine" if defined?(Rails::Engine)