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,45 +3,172 @@
|
|
|
3
3
|
module Servactory
|
|
4
4
|
module ToolKit
|
|
5
5
|
module DynamicOptions
|
|
6
|
+
# Validates that collection elements are of specified types.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# ConsistsOf ensures that all elements within an Array or collection
|
|
11
|
+
# attribute match one of the specified types. This is essential for
|
|
12
|
+
# validating homogeneous collections where each element must conform
|
|
13
|
+
# to expected types.
|
|
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 collection types (e.g., add `ActiveRecord::Relation`),
|
|
21
|
+
# use the `collection_mode_class_names` configuration:
|
|
22
|
+
#
|
|
23
|
+
# ```ruby
|
|
24
|
+
# configuration do
|
|
25
|
+
# collection_mode_class_names([ActiveRecord::Relation])
|
|
26
|
+
# end
|
|
27
|
+
# ```
|
|
28
|
+
#
|
|
29
|
+
# ## Simple Mode
|
|
30
|
+
#
|
|
31
|
+
# Specify type directly as the option value:
|
|
32
|
+
#
|
|
33
|
+
# ```ruby
|
|
34
|
+
# class ProcessUsersService < ApplicationService::Base
|
|
35
|
+
# input :user_ids, type: Array, consists_of: Integer
|
|
36
|
+
# input :tags, type: Array, consists_of: [String, Symbol]
|
|
37
|
+
# input :scores, type: Array, consists_of: Float
|
|
38
|
+
# end
|
|
39
|
+
# ```
|
|
40
|
+
#
|
|
41
|
+
# ## Advanced Mode
|
|
42
|
+
#
|
|
43
|
+
# Specify type with custom error message using a hash.
|
|
44
|
+
# Note: Advanced mode uses `:type` key (not `:is` like other options).
|
|
45
|
+
#
|
|
46
|
+
# With static message:
|
|
47
|
+
#
|
|
48
|
+
# ```ruby
|
|
49
|
+
# input :ids, type: Array, consists_of: {
|
|
50
|
+
# type: String,
|
|
51
|
+
# message: "Input `ids` must be an array of `String`"
|
|
52
|
+
# }
|
|
53
|
+
# ```
|
|
54
|
+
#
|
|
55
|
+
# With dynamic lambda message:
|
|
56
|
+
#
|
|
57
|
+
# ```ruby
|
|
58
|
+
# input :ids, type: Array, consists_of: {
|
|
59
|
+
# type: String,
|
|
60
|
+
# message: lambda do |input:, option_value:, **|
|
|
61
|
+
# "Input `#{input.name}` must be an array of `#{Array(option_value).join(', ')}`"
|
|
62
|
+
# end
|
|
63
|
+
# }
|
|
64
|
+
# ```
|
|
65
|
+
#
|
|
66
|
+
# Lambda receives the following parameters:
|
|
67
|
+
# - For inputs: `input:, value:, option_value:, reason:, **`
|
|
68
|
+
# - For internals: `internal:, value:, option_value:, reason:, **`
|
|
69
|
+
# - For outputs: `output:, value:, option_value:, reason:, **`
|
|
70
|
+
#
|
|
71
|
+
# ## Validation Rules
|
|
72
|
+
#
|
|
73
|
+
# - Collection must be of a registered collection type (Array, Set, etc.)
|
|
74
|
+
# - All elements are flattened before validation (nested arrays supported)
|
|
75
|
+
# - Empty collections pass validation for optional input attributes only
|
|
76
|
+
# - For internal/output attributes, presence check is always performed
|
|
77
|
+
# - For optional inputs with non-empty collections, presence check is performed
|
|
78
|
+
# - Multiple types can be specified as an array
|
|
79
|
+
#
|
|
80
|
+
# ## Important Notes
|
|
81
|
+
#
|
|
82
|
+
# - Use `consists_of: false` to disable validation
|
|
83
|
+
# - NilClass in types allows nil elements in the collection
|
|
84
|
+
# - Nested arrays are automatically flattened for validation
|
|
85
|
+
# - Advanced mode uses `:type` key (not `:is` like other options)
|
|
86
|
+
# - Numeric types use exact class matching (Integer != Float)
|
|
6
87
|
class ConsistsOf < Must
|
|
88
|
+
# Creates a ConsistsOf validator instance.
|
|
89
|
+
#
|
|
90
|
+
# @param option_name [Symbol] The option name (default: :consists_of)
|
|
91
|
+
# @param collection_mode_class_names [Array<Class>] Valid collection types
|
|
92
|
+
# @return [Servactory::Maintenance::Attributes::OptionHelper]
|
|
7
93
|
def self.use(option_name = :consists_of, collection_mode_class_names:)
|
|
8
94
|
instance = new(option_name, :type, false)
|
|
9
95
|
instance.assign(collection_mode_class_names)
|
|
10
96
|
instance.must(:consists_of)
|
|
11
97
|
end
|
|
12
98
|
|
|
99
|
+
# Assigns the list of valid collection class names.
|
|
100
|
+
#
|
|
101
|
+
# @param collection_mode_class_names [Array<Class>] Collection types to accept
|
|
102
|
+
# @return [void]
|
|
13
103
|
def assign(collection_mode_class_names)
|
|
14
104
|
@collection_mode_class_names = collection_mode_class_names
|
|
15
105
|
end
|
|
16
106
|
|
|
107
|
+
# Validates element types for input attribute.
|
|
108
|
+
#
|
|
109
|
+
# @param input [Object] Input attribute object
|
|
110
|
+
# @param value [Object] Collection value to validate
|
|
111
|
+
# @param option [WorkOption] Type configuration
|
|
112
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
17
113
|
def condition_for_input_with(input:, value:, option:)
|
|
18
114
|
common_condition_with(attribute: input, value:, option:)
|
|
19
115
|
end
|
|
20
116
|
|
|
117
|
+
# Validates element types for internal attribute.
|
|
118
|
+
#
|
|
119
|
+
# @param internal [Object] Internal attribute object
|
|
120
|
+
# @param value [Object] Collection value to validate
|
|
121
|
+
# @param option [WorkOption] Type configuration
|
|
122
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
21
123
|
def condition_for_internal_with(internal:, value:, option:)
|
|
22
124
|
common_condition_with(attribute: internal, value:, option:)
|
|
23
125
|
end
|
|
24
126
|
|
|
127
|
+
# Validates element types for output attribute.
|
|
128
|
+
#
|
|
129
|
+
# @param output [Object] Output attribute object
|
|
130
|
+
# @param value [Object] Collection value to validate
|
|
131
|
+
# @param option [WorkOption] Type configuration
|
|
132
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
25
133
|
def condition_for_output_with(output:, value:, option:)
|
|
26
134
|
common_condition_with(attribute: output, value:, option:)
|
|
27
135
|
end
|
|
28
136
|
|
|
137
|
+
# Common validation logic for all attribute types.
|
|
138
|
+
#
|
|
139
|
+
# @param attribute [Object] The attribute being validated
|
|
140
|
+
# @param value [Object] Collection value to validate
|
|
141
|
+
# @param option [WorkOption] Type configuration
|
|
142
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
29
143
|
def common_condition_with(attribute:, value:, option:)
|
|
144
|
+
# Validation disabled.
|
|
30
145
|
return true if option.value == false
|
|
146
|
+
|
|
147
|
+
# Attribute must be a collection type.
|
|
31
148
|
return [false, :wrong_type] unless @collection_mode_class_names.intersect?(attribute.types)
|
|
32
149
|
|
|
150
|
+
# Flatten nested arrays for uniform validation.
|
|
33
151
|
values = value.respond_to?(:flatten) ? value&.flatten : value
|
|
34
152
|
|
|
35
153
|
validate_for!(attribute:, values:, option:)
|
|
36
154
|
end
|
|
37
155
|
|
|
156
|
+
# Validates all elements against allowed types.
|
|
157
|
+
#
|
|
158
|
+
# @param attribute [Object] The attribute being validated
|
|
159
|
+
# @param values [Array] Flattened collection elements
|
|
160
|
+
# @param option [WorkOption] Type configuration
|
|
161
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
38
162
|
def validate_for!(attribute:, values:, option:)
|
|
39
163
|
consists_of_types = Array(option.value).uniq
|
|
40
164
|
|
|
165
|
+
# Check presence requirements.
|
|
41
166
|
return [false, :required] if fails_presence_validation?(attribute:, values:, consists_of_types:)
|
|
42
167
|
|
|
168
|
+
# Empty optional collections are valid.
|
|
43
169
|
return true if values.blank? && attribute.input? && attribute.optional?
|
|
44
170
|
|
|
171
|
+
# Verify each element matches allowed types.
|
|
45
172
|
return true if values.all? do |value|
|
|
46
173
|
consists_of_types.include?(value.class)
|
|
47
174
|
end
|
|
@@ -49,7 +176,14 @@ module Servactory
|
|
|
49
176
|
[false, :wrong_element_type]
|
|
50
177
|
end
|
|
51
178
|
|
|
179
|
+
# Checks if collection fails presence validation.
|
|
180
|
+
#
|
|
181
|
+
# @param attribute [Object] The attribute being validated
|
|
182
|
+
# @param values [Array] Collection elements
|
|
183
|
+
# @param consists_of_types [Array<Class>] Allowed types
|
|
184
|
+
# @return [Boolean] true if validation fails
|
|
52
185
|
def fails_presence_validation?(attribute:, values:, consists_of_types:)
|
|
186
|
+
# NilClass in types allows nil elements.
|
|
53
187
|
return false if consists_of_types.include?(NilClass)
|
|
54
188
|
|
|
55
189
|
check_present = proc { _1 && !values.all?(&:present?) }
|
|
@@ -63,6 +197,15 @@ module Servactory
|
|
|
63
197
|
|
|
64
198
|
########################################################################
|
|
65
199
|
|
|
200
|
+
# Generates error message for input validation failure.
|
|
201
|
+
#
|
|
202
|
+
# @param service [Object] Service context
|
|
203
|
+
# @param input [Object] Input attribute
|
|
204
|
+
# @param value [Object] Failed value
|
|
205
|
+
# @param option_name [Symbol] Option name
|
|
206
|
+
# @param option_value [Object] Expected types
|
|
207
|
+
# @param reason [Symbol] Failure reason
|
|
208
|
+
# @return [String] Localized error message
|
|
66
209
|
def message_for_input_with(service:, input:, value:, option_name:, option_value:, reason:, **)
|
|
67
210
|
i18n_key = "inputs.validations.must.dynamic_options.consists_of"
|
|
68
211
|
i18n_key += reason.present? ? ".#{reason}" : ".default"
|
|
@@ -77,6 +220,15 @@ module Servactory
|
|
|
77
220
|
)
|
|
78
221
|
end
|
|
79
222
|
|
|
223
|
+
# Generates error message for internal validation failure.
|
|
224
|
+
#
|
|
225
|
+
# @param service [Object] Service context
|
|
226
|
+
# @param internal [Object] Internal attribute
|
|
227
|
+
# @param value [Object] Failed value
|
|
228
|
+
# @param option_name [Symbol] Option name
|
|
229
|
+
# @param option_value [Object] Expected types
|
|
230
|
+
# @param reason [Symbol] Failure reason
|
|
231
|
+
# @return [String] Localized error message
|
|
80
232
|
def message_for_internal_with(service:, internal:, value:, option_name:, option_value:, reason:, **)
|
|
81
233
|
i18n_key = "internals.validations.must.dynamic_options.consists_of"
|
|
82
234
|
i18n_key += reason.present? ? ".#{reason}" : ".default"
|
|
@@ -91,6 +243,15 @@ module Servactory
|
|
|
91
243
|
)
|
|
92
244
|
end
|
|
93
245
|
|
|
246
|
+
# Generates error message for output validation failure.
|
|
247
|
+
#
|
|
248
|
+
# @param service [Object] Service context
|
|
249
|
+
# @param output [Object] Output attribute
|
|
250
|
+
# @param value [Object] Failed value
|
|
251
|
+
# @param option_name [Symbol] Option name
|
|
252
|
+
# @param option_value [Object] Expected types
|
|
253
|
+
# @param reason [Symbol] Failure reason
|
|
254
|
+
# @return [String] Localized error message
|
|
94
255
|
def message_for_output_with(service:, output:, value:, option_name:, option_value:, reason:, **)
|
|
95
256
|
i18n_key = "outputs.validations.must.dynamic_options.consists_of"
|
|
96
257
|
i18n_key += reason.present? ? ".#{reason}" : ".default"
|
|
@@ -105,6 +266,11 @@ module Servactory
|
|
|
105
266
|
)
|
|
106
267
|
end
|
|
107
268
|
|
|
269
|
+
# Extracts type names of elements that don't match expected types.
|
|
270
|
+
#
|
|
271
|
+
# @param values [Array, nil] Collection elements
|
|
272
|
+
# @param option_value [Object] Expected types
|
|
273
|
+
# @return [String] Comma-separated list of unexpected type names
|
|
108
274
|
def given_type_for(values:, option_value:)
|
|
109
275
|
return "NilClass" if values.nil?
|
|
110
276
|
|
|
@@ -3,10 +3,125 @@
|
|
|
3
3
|
module Servactory
|
|
4
4
|
module ToolKit
|
|
5
5
|
module DynamicOptions
|
|
6
|
+
# Validates string values against predefined or custom format patterns.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Format provides pattern-based validation for string attributes using
|
|
11
|
+
# regular expressions and custom validators. It includes built-in formats
|
|
12
|
+
# for common use cases (UUID, email, date, etc.) and supports custom
|
|
13
|
+
# format definitions.
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# This option is **NOT included by default**. Register it for each
|
|
18
|
+
# attribute type where you want to use it:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# configuration do
|
|
22
|
+
# input_option_helpers([
|
|
23
|
+
# Servactory::ToolKit::DynamicOptions::Format.use
|
|
24
|
+
# ])
|
|
25
|
+
#
|
|
26
|
+
# internal_option_helpers([
|
|
27
|
+
# Servactory::ToolKit::DynamicOptions::Format.use
|
|
28
|
+
# ])
|
|
29
|
+
#
|
|
30
|
+
# output_option_helpers([
|
|
31
|
+
# Servactory::ToolKit::DynamicOptions::Format.use
|
|
32
|
+
# ])
|
|
33
|
+
# end
|
|
34
|
+
# ```
|
|
35
|
+
#
|
|
36
|
+
# Use built-in formats in your service:
|
|
37
|
+
#
|
|
38
|
+
# ```ruby
|
|
39
|
+
# class MyService < ApplicationService::Base
|
|
40
|
+
# input :user_id, type: String, format: :uuid
|
|
41
|
+
# input :email, type: String, format: :email
|
|
42
|
+
# input :birth_date, type: String, format: :date
|
|
43
|
+
# end
|
|
44
|
+
# ```
|
|
45
|
+
#
|
|
46
|
+
# Add custom formats:
|
|
47
|
+
#
|
|
48
|
+
# ```ruby
|
|
49
|
+
# Format.use(formats: {
|
|
50
|
+
# phone: {
|
|
51
|
+
# pattern: /\A\+?[1-9]\d{6,14}\z/,
|
|
52
|
+
# validator: ->(value:) { value.present? }
|
|
53
|
+
# }
|
|
54
|
+
# })
|
|
55
|
+
# ```
|
|
56
|
+
#
|
|
57
|
+
# ## Supported Formats
|
|
58
|
+
#
|
|
59
|
+
# | Format | Description |
|
|
60
|
+
# |--------|-------------|
|
|
61
|
+
# | `:uuid` | Standard UUID format (8-4-4-4-12 hex digits) |
|
|
62
|
+
# | `:email` | Email address per RFC 2822 |
|
|
63
|
+
# | `:password` | 8-16 chars with digit, lowercase, uppercase |
|
|
64
|
+
# | `:duration` | ISO 8601 duration (e.g., "PT1H30M") |
|
|
65
|
+
# | `:date` | Parseable date string |
|
|
66
|
+
# | `:datetime` | Parseable datetime string |
|
|
67
|
+
# | `:time` | Parseable time string |
|
|
68
|
+
# | `:boolean` | Truthy boolean string ("true" or "1") |
|
|
69
|
+
#
|
|
70
|
+
# ## Simple Mode
|
|
71
|
+
#
|
|
72
|
+
# Specify format directly as the option value:
|
|
73
|
+
#
|
|
74
|
+
# ```ruby
|
|
75
|
+
# class ValidateUserService < ApplicationService::Base
|
|
76
|
+
# input :uuid, type: String, format: :uuid
|
|
77
|
+
# input :email, type: String, format: :email
|
|
78
|
+
# input :password, type: String, format: :password
|
|
79
|
+
# end
|
|
80
|
+
# ```
|
|
81
|
+
#
|
|
82
|
+
# ## Advanced Mode
|
|
83
|
+
#
|
|
84
|
+
# Specify format with custom error message using a hash:
|
|
85
|
+
#
|
|
86
|
+
# With static message:
|
|
87
|
+
#
|
|
88
|
+
# ```ruby
|
|
89
|
+
# input :email, type: String, format: {
|
|
90
|
+
# is: :email,
|
|
91
|
+
# message: "Input `email` must be a valid email address"
|
|
92
|
+
# }
|
|
93
|
+
# ```
|
|
94
|
+
#
|
|
95
|
+
# With dynamic lambda message:
|
|
96
|
+
#
|
|
97
|
+
# ```ruby
|
|
98
|
+
# input :email, type: String, format: {
|
|
99
|
+
# is: :email,
|
|
100
|
+
# message: lambda do |input:, value:, option_value:, **|
|
|
101
|
+
# "Input `#{input.name}` with value `#{value}` does not match `#{option_value}` format"
|
|
102
|
+
# end
|
|
103
|
+
# }
|
|
104
|
+
# ```
|
|
105
|
+
#
|
|
106
|
+
# Lambda receives the following parameters:
|
|
107
|
+
# - For inputs: `input:, value:, option_value:, reason:, **`
|
|
108
|
+
# - For internals: `internal:, value:, option_value:, reason:, **`
|
|
109
|
+
# - For outputs: `output:, value:, option_value:, reason:, **`
|
|
110
|
+
#
|
|
111
|
+
# ## Important Notes
|
|
112
|
+
#
|
|
113
|
+
# - Optional inputs with blank values skip validation
|
|
114
|
+
# - Custom patterns can be strings or Regexp objects
|
|
115
|
+
# - Validators receive `value:` keyword argument
|
|
116
|
+
# - Unknown format names return `:unknown` error
|
|
117
|
+
# - Format validation is two-phase: pattern check (if defined), then validator callback
|
|
118
|
+
# - The `:boolean` format pattern matches "true", "false", "0", "1", but validator
|
|
119
|
+
# only passes for truthy values ("true", "1"); "false" and "0" will fail validation
|
|
6
120
|
class Format < Must # rubocop:disable Metrics/ClassLength
|
|
121
|
+
# Built-in format definitions with patterns and validators.
|
|
7
122
|
DEFAULT_FORMATS = {
|
|
8
123
|
uuid: {
|
|
9
|
-
pattern:
|
|
124
|
+
pattern: /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/,
|
|
10
125
|
validator: ->(value:) { value.present? }
|
|
11
126
|
},
|
|
12
127
|
email: {
|
|
@@ -14,11 +129,11 @@ module Servactory
|
|
|
14
129
|
validator: ->(value:) { value.present? }
|
|
15
130
|
},
|
|
16
131
|
password: {
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
pattern:
|
|
132
|
+
# Password must contain one digit from 1 to 9, one lowercase letter, one
|
|
133
|
+
# uppercase letter, and it must be 8-16 characters long.
|
|
134
|
+
# Usage of any other special character and space is optional.
|
|
135
|
+
# Reference: https://dev.to/rasaf_ibrahim/write-regex-password-validation-like-a-pro-5175
|
|
136
|
+
pattern: /\A(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,16}\z/,
|
|
22
137
|
validator: ->(value:) { value.present? }
|
|
23
138
|
},
|
|
24
139
|
duration: {
|
|
@@ -54,42 +169,79 @@ module Servactory
|
|
|
54
169
|
end
|
|
55
170
|
},
|
|
56
171
|
boolean: {
|
|
57
|
-
pattern:
|
|
172
|
+
pattern: /\A(true|false|0|1)\z/i,
|
|
58
173
|
validator: ->(value:) { %w[true 1].include?(value&.downcase) }
|
|
59
174
|
}
|
|
60
175
|
}.freeze
|
|
61
176
|
private_constant :DEFAULT_FORMATS
|
|
62
177
|
|
|
178
|
+
# Creates a Format validator instance.
|
|
179
|
+
#
|
|
180
|
+
# @param option_name [Symbol] The option name (default: :format)
|
|
181
|
+
# @param formats [Hash] Custom format definitions to merge with defaults
|
|
182
|
+
# @return [Servactory::Maintenance::Attributes::OptionHelper]
|
|
63
183
|
def self.use(option_name = :format, formats: {})
|
|
64
184
|
instance = new(option_name)
|
|
65
185
|
instance.assign(formats)
|
|
66
186
|
instance.must(:be_in_format)
|
|
67
187
|
end
|
|
68
188
|
|
|
189
|
+
# Assigns format definitions, merging with defaults.
|
|
190
|
+
#
|
|
191
|
+
# @param formats [Hash] Custom formats to add
|
|
192
|
+
# @return [void]
|
|
69
193
|
def assign(formats = {})
|
|
70
194
|
@formats = formats.is_a?(Hash) ? DEFAULT_FORMATS.merge(formats) : DEFAULT_FORMATS
|
|
71
195
|
end
|
|
72
196
|
|
|
197
|
+
# Validates format condition for input attribute.
|
|
198
|
+
#
|
|
199
|
+
# @param input [Object] Input attribute object
|
|
200
|
+
# @param value [Object] String value to validate
|
|
201
|
+
# @param option [WorkOption] Format configuration
|
|
202
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
73
203
|
def condition_for_input_with(...)
|
|
74
204
|
common_condition_with(...)
|
|
75
205
|
end
|
|
76
206
|
|
|
207
|
+
# Validates format condition for internal attribute.
|
|
208
|
+
#
|
|
209
|
+
# @param internal [Object] Internal attribute object
|
|
210
|
+
# @param value [Object] String value to validate
|
|
211
|
+
# @param option [WorkOption] Format configuration
|
|
212
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
77
213
|
def condition_for_internal_with(...)
|
|
78
214
|
common_condition_with(...)
|
|
79
215
|
end
|
|
80
216
|
|
|
217
|
+
# Validates format condition for output attribute.
|
|
218
|
+
#
|
|
219
|
+
# @param output [Object] Output attribute object
|
|
220
|
+
# @param value [Object] String value to validate
|
|
221
|
+
# @param option [WorkOption] Format configuration
|
|
222
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
81
223
|
def condition_for_output_with(...)
|
|
82
224
|
common_condition_with(...)
|
|
83
225
|
end
|
|
84
226
|
|
|
227
|
+
# Common format validation logic.
|
|
228
|
+
#
|
|
229
|
+
# @param value [Object] Value to validate
|
|
230
|
+
# @param option [WorkOption] Format configuration
|
|
231
|
+
# @param input [Object, nil] Input attribute if applicable
|
|
232
|
+
# @param internal [Object, nil] Internal attribute if applicable
|
|
233
|
+
# @param output [Object, nil] Output attribute if applicable
|
|
234
|
+
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
85
235
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
86
236
|
def common_condition_with(value:, option:, input: nil, internal: nil, output: nil)
|
|
87
237
|
option_value = option.value&.to_sym
|
|
88
238
|
|
|
239
|
+
# Check if format exists.
|
|
89
240
|
return [false, :unknown] unless @formats.key?(option_value)
|
|
90
241
|
|
|
91
242
|
attribute = Utils.define_attribute_with(input:, internal:, output:)
|
|
92
243
|
|
|
244
|
+
# Skip validation for blank optional values.
|
|
93
245
|
if value.blank? &&
|
|
94
246
|
(
|
|
95
247
|
(attribute.input? && attribute.optional?) ||
|
|
@@ -103,16 +255,33 @@ module Servactory
|
|
|
103
255
|
|
|
104
256
|
format_options = @formats.fetch(option_value)
|
|
105
257
|
|
|
258
|
+
# Get pattern from option properties or format definition.
|
|
106
259
|
format_pattern = option.properties.fetch(:pattern, format_options.fetch(:pattern))
|
|
107
260
|
|
|
108
|
-
|
|
261
|
+
# Validate against pattern if defined.
|
|
262
|
+
if format_pattern.present?
|
|
263
|
+
return [false, :wrong_type] unless value.respond_to?(:match?)
|
|
109
264
|
|
|
265
|
+
compiled_pattern = format_pattern.is_a?(Regexp) ? format_pattern : Regexp.compile(format_pattern)
|
|
266
|
+
return [false, :wrong_pattern] unless value.match?(compiled_pattern)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Run validator callback.
|
|
110
270
|
option.properties.fetch(:validator, format_options.fetch(:validator)).call(value:)
|
|
111
271
|
end
|
|
112
272
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
113
273
|
|
|
114
274
|
########################################################################
|
|
115
275
|
|
|
276
|
+
# Generates error message for input validation failure.
|
|
277
|
+
#
|
|
278
|
+
# @param service [Object] Service context
|
|
279
|
+
# @param input [Object] Input attribute
|
|
280
|
+
# @param value [Object] Failed value
|
|
281
|
+
# @param option_name [Symbol] Option name
|
|
282
|
+
# @param option_value [Symbol] Format name
|
|
283
|
+
# @param reason [Symbol] Failure reason
|
|
284
|
+
# @return [String] Localized error message
|
|
116
285
|
def message_for_input_with(service:, input:, value:, option_name:, option_value:, reason:, **)
|
|
117
286
|
i18n_key = "inputs.validations.must.dynamic_options.format"
|
|
118
287
|
i18n_key += reason.present? ? ".#{reason}" : ".default"
|
|
@@ -126,6 +295,15 @@ module Servactory
|
|
|
126
295
|
)
|
|
127
296
|
end
|
|
128
297
|
|
|
298
|
+
# Generates error message for internal validation failure.
|
|
299
|
+
#
|
|
300
|
+
# @param service [Object] Service context
|
|
301
|
+
# @param internal [Object] Internal attribute
|
|
302
|
+
# @param value [Object] Failed value
|
|
303
|
+
# @param option_name [Symbol] Option name
|
|
304
|
+
# @param option_value [Symbol] Format name
|
|
305
|
+
# @param reason [Symbol] Failure reason
|
|
306
|
+
# @return [String] Localized error message
|
|
129
307
|
def message_for_internal_with(service:, internal:, value:, option_name:, option_value:, reason:, **)
|
|
130
308
|
i18n_key = "internals.validations.must.dynamic_options.format"
|
|
131
309
|
i18n_key += reason.present? ? ".#{reason}" : ".default"
|
|
@@ -139,6 +317,15 @@ module Servactory
|
|
|
139
317
|
)
|
|
140
318
|
end
|
|
141
319
|
|
|
320
|
+
# Generates error message for output validation failure.
|
|
321
|
+
#
|
|
322
|
+
# @param service [Object] Service context
|
|
323
|
+
# @param output [Object] Output attribute
|
|
324
|
+
# @param value [Object] Failed value
|
|
325
|
+
# @param option_name [Symbol] Option name
|
|
326
|
+
# @param option_value [Symbol] Format name
|
|
327
|
+
# @param reason [Symbol] Failure reason
|
|
328
|
+
# @return [String] Localized error message
|
|
142
329
|
def message_for_output_with(service:, output:, value:, option_name:, option_value:, reason:, **)
|
|
143
330
|
i18n_key = "outputs.validations.must.dynamic_options.format"
|
|
144
331
|
i18n_key += reason.present? ? ".#{reason}" : ".default"
|