servactory 2.16.0 → 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 +3 -3
- data/lib/servactory.rb +4 -0
- metadata +73 -25
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/servactory/version.rb
CHANGED
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)
|