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,31 +3,154 @@
|
|
|
3
3
|
module Servactory
|
|
4
4
|
module ToolKit
|
|
5
5
|
module DynamicOptions
|
|
6
|
+
# Validates that numeric value is a multiple of a specified number.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# MultipleOf ensures that numeric values are evenly divisible by
|
|
11
|
+
# a specified divisor. This is useful for validating quantities,
|
|
12
|
+
# prices, or any values that must conform to specific increments.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# This option is **NOT included by default**. Register it for each
|
|
17
|
+
# attribute type where you want to use it:
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# configuration do
|
|
21
|
+
# input_option_helpers([
|
|
22
|
+
# Servactory::ToolKit::DynamicOptions::MultipleOf.use
|
|
23
|
+
# ])
|
|
24
|
+
#
|
|
25
|
+
# internal_option_helpers([
|
|
26
|
+
# Servactory::ToolKit::DynamicOptions::MultipleOf.use
|
|
27
|
+
# ])
|
|
28
|
+
#
|
|
29
|
+
# output_option_helpers([
|
|
30
|
+
# Servactory::ToolKit::DynamicOptions::MultipleOf.use
|
|
31
|
+
# ])
|
|
32
|
+
# end
|
|
33
|
+
# ```
|
|
34
|
+
#
|
|
35
|
+
# Use in your service definition:
|
|
36
|
+
#
|
|
37
|
+
# ```ruby
|
|
38
|
+
# class ProcessOrderService < ApplicationService::Base
|
|
39
|
+
# input :quantity, type: Integer, multiple_of: 5
|
|
40
|
+
# input :price, type: Float, multiple_of: 0.25
|
|
41
|
+
# input :batch_size, type: Integer, multiple_of: 100
|
|
42
|
+
# end
|
|
43
|
+
# ```
|
|
44
|
+
#
|
|
45
|
+
# ## Simple Mode
|
|
46
|
+
#
|
|
47
|
+
# Specify divisor directly:
|
|
48
|
+
#
|
|
49
|
+
# ```ruby
|
|
50
|
+
# class ProcessOrderService < ApplicationService::Base
|
|
51
|
+
# input :quantity, type: Integer, multiple_of: 5
|
|
52
|
+
# input :price, type: Float, multiple_of: 0.25
|
|
53
|
+
# input :batch_size, type: Integer, multiple_of: 100
|
|
54
|
+
# end
|
|
55
|
+
# ```
|
|
56
|
+
#
|
|
57
|
+
# ## Advanced Mode
|
|
58
|
+
#
|
|
59
|
+
# Specify divisor with custom error message using a hash:
|
|
60
|
+
#
|
|
61
|
+
# With static message:
|
|
62
|
+
#
|
|
63
|
+
# ```ruby
|
|
64
|
+
# input :quantity, type: Integer, multiple_of: {
|
|
65
|
+
# is: 5,
|
|
66
|
+
# message: "Input `quantity` must be a multiple of 5"
|
|
67
|
+
# }
|
|
68
|
+
# ```
|
|
69
|
+
#
|
|
70
|
+
# With dynamic lambda message:
|
|
71
|
+
#
|
|
72
|
+
# ```ruby
|
|
73
|
+
# input :quantity, type: Integer, multiple_of: {
|
|
74
|
+
# is: 5,
|
|
75
|
+
# message: lambda do |input:, value:, option_value:, **|
|
|
76
|
+
# "Input `#{input.name}` must be divisible by #{option_value}, got #{value}"
|
|
77
|
+
# end
|
|
78
|
+
# }
|
|
79
|
+
# ```
|
|
80
|
+
#
|
|
81
|
+
# Lambda receives the following parameters:
|
|
82
|
+
# - For inputs: `input:, option_value:, value:, **`
|
|
83
|
+
# - For internals: `internal:, option_value:, value:, **`
|
|
84
|
+
# - For outputs: `output:, option_value:, value:, **`
|
|
85
|
+
#
|
|
86
|
+
# ## Supported Types
|
|
87
|
+
#
|
|
88
|
+
# - Integer
|
|
89
|
+
# - Float
|
|
90
|
+
# - Rational
|
|
91
|
+
# - BigDecimal
|
|
92
|
+
#
|
|
93
|
+
# ## Important Notes
|
|
94
|
+
#
|
|
95
|
+
# - Divisor must be a non-zero Numeric
|
|
96
|
+
# - Uses epsilon comparison for floating point precision
|
|
97
|
+
# - Returns false for non-numeric values
|
|
98
|
+
# - Provides specific error messages for blank and zero divisors
|
|
6
99
|
class MultipleOf < Must
|
|
100
|
+
# Creates a MultipleOf validator instance.
|
|
101
|
+
#
|
|
102
|
+
# @param option_name [Symbol] The option name (default: :multiple_of)
|
|
103
|
+
# @return [Servactory::Maintenance::Attributes::OptionHelper]
|
|
7
104
|
def self.use(option_name = :multiple_of)
|
|
8
105
|
new(option_name).must(:be_multiple_of)
|
|
9
106
|
end
|
|
10
107
|
|
|
108
|
+
# Validates multiple_of condition for input attribute.
|
|
109
|
+
#
|
|
110
|
+
# @param input [Object] Input attribute object
|
|
111
|
+
# @param value [Object] Value to validate
|
|
112
|
+
# @param option [WorkOption] Multiple of configuration
|
|
113
|
+
# @return [Boolean] true if valid
|
|
11
114
|
def condition_for_input_with(...)
|
|
12
115
|
common_condition_with(...)
|
|
13
116
|
end
|
|
14
117
|
|
|
118
|
+
# Validates multiple_of condition for internal attribute.
|
|
119
|
+
#
|
|
120
|
+
# @param internal [Object] Internal attribute object
|
|
121
|
+
# @param value [Object] Value to validate
|
|
122
|
+
# @param option [WorkOption] Multiple of configuration
|
|
123
|
+
# @return [Boolean] true if valid
|
|
15
124
|
def condition_for_internal_with(...)
|
|
16
125
|
common_condition_with(...)
|
|
17
126
|
end
|
|
18
127
|
|
|
128
|
+
# Validates multiple_of condition for output attribute.
|
|
129
|
+
#
|
|
130
|
+
# @param output [Object] Output attribute object
|
|
131
|
+
# @param value [Object] Value to validate
|
|
132
|
+
# @param option [WorkOption] Multiple of configuration
|
|
133
|
+
# @return [Boolean] true if valid
|
|
19
134
|
def condition_for_output_with(...)
|
|
20
135
|
common_condition_with(...)
|
|
21
136
|
end
|
|
22
137
|
|
|
138
|
+
# Common validation logic for all attribute types.
|
|
139
|
+
#
|
|
140
|
+
# @param value [Object] Value to validate
|
|
141
|
+
# @param option [WorkOption] Multiple of configuration
|
|
142
|
+
# @return [Boolean] true if value is multiple of divisor
|
|
23
143
|
def common_condition_with(value:, option:, **) # rubocop:disable Naming/PredicateMethod
|
|
24
144
|
case value
|
|
25
145
|
when Integer, Float, Rational, BigDecimal
|
|
146
|
+
# Validate divisor is present and valid.
|
|
26
147
|
return false if option.value.blank?
|
|
27
|
-
return false unless
|
|
148
|
+
return false unless option.value.is_a?(Numeric)
|
|
28
149
|
return false if option.value.zero?
|
|
29
150
|
|
|
30
|
-
|
|
151
|
+
# Calculate remainder with epsilon tolerance for floats.
|
|
152
|
+
remainder = value % option.value
|
|
153
|
+
remainder.zero? || remainder.abs < Float::EPSILON * [value.abs, option.value.abs].max
|
|
31
154
|
else
|
|
32
155
|
false
|
|
33
156
|
end
|
|
@@ -35,13 +158,25 @@ module Servactory
|
|
|
35
158
|
|
|
36
159
|
########################################################################
|
|
37
160
|
|
|
161
|
+
# Generates error message for input validation failure.
|
|
162
|
+
#
|
|
163
|
+
# Selects appropriate message based on divisor state:
|
|
164
|
+
# - blank: divisor is nil or empty
|
|
165
|
+
# - divided_by_0: divisor is zero
|
|
166
|
+
# - default: standard not-multiple-of message
|
|
167
|
+
#
|
|
168
|
+
# @param service [Object] Service context
|
|
169
|
+
# @param input [Object] Input attribute
|
|
170
|
+
# @param value [Object] Failed value
|
|
171
|
+
# @param option_name [Symbol] Option name
|
|
172
|
+
# @param option_value [Object] Divisor value
|
|
173
|
+
# @return [String] Localized error message
|
|
38
174
|
def message_for_input_with(service:, input:, value:, option_name:, option_value:, **) # rubocop:disable Metrics/MethodLength
|
|
39
175
|
i18n_key = "inputs.validations.must.dynamic_options.multiple_of"
|
|
40
176
|
|
|
41
177
|
i18n_key += if option_value.blank?
|
|
42
178
|
".blank"
|
|
43
|
-
elsif
|
|
44
|
-
option_value.zero?
|
|
179
|
+
elsif option_value.is_a?(Numeric) && option_value.zero?
|
|
45
180
|
".divided_by_0"
|
|
46
181
|
else
|
|
47
182
|
".default"
|
|
@@ -56,13 +191,20 @@ module Servactory
|
|
|
56
191
|
)
|
|
57
192
|
end
|
|
58
193
|
|
|
194
|
+
# Generates error message for internal validation failure.
|
|
195
|
+
#
|
|
196
|
+
# @param service [Object] Service context
|
|
197
|
+
# @param internal [Object] Internal attribute
|
|
198
|
+
# @param value [Object] Failed value
|
|
199
|
+
# @param option_name [Symbol] Option name
|
|
200
|
+
# @param option_value [Object] Divisor value
|
|
201
|
+
# @return [String] Localized error message
|
|
59
202
|
def message_for_internal_with(service:, internal:, value:, option_name:, option_value:, **) # rubocop:disable Metrics/MethodLength
|
|
60
203
|
i18n_key = "internals.validations.must.dynamic_options.multiple_of"
|
|
61
204
|
|
|
62
205
|
i18n_key += if option_value.blank?
|
|
63
206
|
".blank"
|
|
64
|
-
elsif
|
|
65
|
-
option_value.zero?
|
|
207
|
+
elsif option_value.is_a?(Numeric) && option_value.zero?
|
|
66
208
|
".divided_by_0"
|
|
67
209
|
else
|
|
68
210
|
".default"
|
|
@@ -77,13 +219,20 @@ module Servactory
|
|
|
77
219
|
)
|
|
78
220
|
end
|
|
79
221
|
|
|
222
|
+
# Generates error message for output validation failure.
|
|
223
|
+
#
|
|
224
|
+
# @param service [Object] Service context
|
|
225
|
+
# @param output [Object] Output attribute
|
|
226
|
+
# @param value [Object] Failed value
|
|
227
|
+
# @param option_name [Symbol] Option name
|
|
228
|
+
# @param option_value [Object] Divisor value
|
|
229
|
+
# @return [String] Localized error message
|
|
80
230
|
def message_for_output_with(service:, output:, value:, option_name:, option_value:, **) # rubocop:disable Metrics/MethodLength
|
|
81
231
|
i18n_key = "outputs.validations.must.dynamic_options.multiple_of"
|
|
82
232
|
|
|
83
233
|
i18n_key += if option_value.blank?
|
|
84
234
|
".blank"
|
|
85
|
-
elsif
|
|
86
|
-
option_value.zero?
|
|
235
|
+
elsif option_value.is_a?(Numeric) && option_value.zero?
|
|
87
236
|
".divided_by_0"
|
|
88
237
|
else
|
|
89
238
|
".default"
|
|
@@ -3,16 +3,129 @@
|
|
|
3
3
|
module Servactory
|
|
4
4
|
module ToolKit
|
|
5
5
|
module DynamicOptions
|
|
6
|
+
# Base class for creating custom dynamic validation options.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Must provides a foundation for implementing custom validation rules
|
|
11
|
+
# that can be applied to service inputs, internals, and outputs.
|
|
12
|
+
# It handles the complexity of option parsing, condition evaluation,
|
|
13
|
+
# and error message generation, allowing subclasses to focus on
|
|
14
|
+
# validation logic.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Create a custom validator by inheriting from Must:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# class MyValidator < Servactory::ToolKit::DynamicOptions::Must
|
|
22
|
+
# def self.use(option_name = :my_option)
|
|
23
|
+
# new(option_name).must(:my_validation)
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# def condition_for_input_with(input:, value:, option:)
|
|
27
|
+
# # Return true if valid, false otherwise
|
|
28
|
+
# value.meets_criteria?(option.value)
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# def message_for_input_with(input:, value:, option_name:, option_value:, **)
|
|
32
|
+
# "Input `#{input.name}` must satisfy #{option_name}"
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # Implement similar methods for internal and output...
|
|
36
|
+
# end
|
|
37
|
+
# ```
|
|
38
|
+
#
|
|
39
|
+
# Register in service configuration:
|
|
40
|
+
#
|
|
41
|
+
# ```ruby
|
|
42
|
+
# configuration do
|
|
43
|
+
# input_option_helpers([
|
|
44
|
+
# MyValidator.use
|
|
45
|
+
# ])
|
|
46
|
+
# end
|
|
47
|
+
# ```
|
|
48
|
+
#
|
|
49
|
+
# ## Simple Mode
|
|
50
|
+
#
|
|
51
|
+
# Custom validators support simple mode with direct value:
|
|
52
|
+
#
|
|
53
|
+
# ```ruby
|
|
54
|
+
# input :value, type: Integer, my_option: 10
|
|
55
|
+
# ```
|
|
56
|
+
#
|
|
57
|
+
# ## Advanced Mode
|
|
58
|
+
#
|
|
59
|
+
# Custom validators support advanced mode with custom messages:
|
|
60
|
+
#
|
|
61
|
+
# With static message:
|
|
62
|
+
#
|
|
63
|
+
# ```ruby
|
|
64
|
+
# input :value, type: Integer, my_option: {
|
|
65
|
+
# is: 10,
|
|
66
|
+
# message: "Custom validation failed"
|
|
67
|
+
# }
|
|
68
|
+
# ```
|
|
69
|
+
#
|
|
70
|
+
# With dynamic lambda message:
|
|
71
|
+
#
|
|
72
|
+
# ```ruby
|
|
73
|
+
# input :value, type: Integer, my_option: {
|
|
74
|
+
# is: 10,
|
|
75
|
+
# message: lambda do |input:, value:, option_value:, **|
|
|
76
|
+
# "Input `#{input.name}` failed validation with value `#{value}`"
|
|
77
|
+
# end
|
|
78
|
+
# }
|
|
79
|
+
# ```
|
|
80
|
+
#
|
|
81
|
+
# Lambda receives the following parameters:
|
|
82
|
+
# - For inputs: `input:, option_value:, value:, **`
|
|
83
|
+
# - For internals: `internal:, option_value:, value:, **`
|
|
84
|
+
# - For outputs: `output:, option_value:, value:, **`
|
|
85
|
+
#
|
|
86
|
+
# ## Architecture
|
|
87
|
+
#
|
|
88
|
+
# The class uses a two-phase validation approach:
|
|
89
|
+
# 1. **Condition phase**: `condition_for_*` methods return boolean
|
|
90
|
+
# 2. **Message phase**: `message_for_*` methods generate error text
|
|
91
|
+
#
|
|
92
|
+
# ## Important Notes
|
|
93
|
+
#
|
|
94
|
+
# - Subclasses must implement all six abstract methods
|
|
95
|
+
# - Option values support both simple mode (`option: value`) and
|
|
96
|
+
# advanced mode (`option: { is: value, message: "..." }`)
|
|
97
|
+
# - Custom messages can be strings or Procs for dynamic generation
|
|
6
98
|
class Must # rubocop:disable Metrics/ClassLength
|
|
99
|
+
# Value object representing a parsed validation option.
|
|
100
|
+
#
|
|
101
|
+
# ## Purpose
|
|
102
|
+
#
|
|
103
|
+
# WorkOption normalizes the various ways an option can be specified
|
|
104
|
+
# (simple value, hash with `:is` key, hash with custom message)
|
|
105
|
+
# into a consistent interface for validators.
|
|
106
|
+
#
|
|
107
|
+
# ## Attributes
|
|
108
|
+
#
|
|
109
|
+
# - `name` - The option name symbol (e.g., `:min`, `:max`)
|
|
110
|
+
# - `value` - The actual validation value (e.g., `10` for `min: 10`)
|
|
111
|
+
# - `message` - Custom error message (String or Proc), or nil
|
|
112
|
+
# - `properties` - Additional hash properties passed with the option
|
|
7
113
|
class WorkOption
|
|
8
114
|
attr_reader :name,
|
|
9
115
|
:value,
|
|
10
116
|
:message,
|
|
11
117
|
:properties
|
|
12
118
|
|
|
119
|
+
# Creates a new WorkOption by parsing option data.
|
|
120
|
+
#
|
|
121
|
+
# @param name [Symbol] The option name
|
|
122
|
+
# @param data [Object] Raw option value (can be scalar, Hash, etc.)
|
|
123
|
+
# @param body_key [Symbol] Key to extract value from Hash (default: :is)
|
|
124
|
+
# @param body_fallback [Object] Default value if data is empty
|
|
13
125
|
def initialize(name, data, body_key:, body_fallback:)
|
|
14
126
|
@name = name
|
|
15
127
|
|
|
128
|
+
# Extract the primary value from data.
|
|
16
129
|
@value =
|
|
17
130
|
if data.is_a?(Hash) && data.key?(body_key)
|
|
18
131
|
data.delete(body_key)
|
|
@@ -20,17 +133,29 @@ module Servactory
|
|
|
20
133
|
data.presence || body_fallback
|
|
21
134
|
end
|
|
22
135
|
|
|
136
|
+
# Extract custom message if provided.
|
|
23
137
|
@message = (data.is_a?(Hash) && data.key?(:message) ? data.delete(:message) : nil)
|
|
138
|
+
|
|
139
|
+
# Remaining hash properties become additional configuration.
|
|
24
140
|
@properties = data.is_a?(Hash) ? data : {}
|
|
25
141
|
end
|
|
26
142
|
end
|
|
27
143
|
|
|
144
|
+
# Creates a new Must instance.
|
|
145
|
+
#
|
|
146
|
+
# @param option_name [Symbol] Name of the option (e.g., :min, :max)
|
|
147
|
+
# @param body_key [Symbol] Key for extracting value in advanced mode
|
|
148
|
+
# @param body_fallback [Object] Default value when none provided
|
|
28
149
|
def initialize(option_name, body_key = :is, body_fallback = nil)
|
|
29
150
|
@option_name = option_name
|
|
30
151
|
@body_key = body_key
|
|
31
152
|
@body_fallback = body_fallback
|
|
32
153
|
end
|
|
33
154
|
|
|
155
|
+
# Creates an OptionHelper for registration with Servactory.
|
|
156
|
+
#
|
|
157
|
+
# @param name [Symbol] Internal validation name
|
|
158
|
+
# @return [Servactory::Maintenance::Attributes::OptionHelper]
|
|
34
159
|
def must(name)
|
|
35
160
|
Servactory::Maintenance::Attributes::OptionHelper.new(
|
|
36
161
|
name: @option_name,
|
|
@@ -42,6 +167,10 @@ module Servactory
|
|
|
42
167
|
)
|
|
43
168
|
end
|
|
44
169
|
|
|
170
|
+
# Builds the equivalence lambda for option transformation.
|
|
171
|
+
#
|
|
172
|
+
# @param name [Symbol] Validation name
|
|
173
|
+
# @return [Proc] Lambda that transforms option data to must format
|
|
45
174
|
def equivalent_with(name)
|
|
46
175
|
lambda do |data|
|
|
47
176
|
option = WorkOption.new(@option_name, data, body_key: @body_key, body_fallback: @body_fallback)
|
|
@@ -54,6 +183,10 @@ module Servactory
|
|
|
54
183
|
end
|
|
55
184
|
end
|
|
56
185
|
|
|
186
|
+
# Constructs the must content hash with value and message lambdas.
|
|
187
|
+
#
|
|
188
|
+
# @param option [WorkOption] Parsed option data
|
|
189
|
+
# @return [Hash] Hash with :is and :message keys
|
|
57
190
|
def must_content_with(option)
|
|
58
191
|
{
|
|
59
192
|
is: must_content_value_with(option),
|
|
@@ -63,6 +196,10 @@ module Servactory
|
|
|
63
196
|
|
|
64
197
|
########################################################################
|
|
65
198
|
|
|
199
|
+
# Builds the validation condition lambda.
|
|
200
|
+
#
|
|
201
|
+
# @param option [WorkOption] Parsed option data
|
|
202
|
+
# @return [Proc] Lambda that evaluates validation condition
|
|
66
203
|
def must_content_value_with(option)
|
|
67
204
|
lambda do |value:, input: nil, internal: nil, output: nil|
|
|
68
205
|
if input.present? && input.input?
|
|
@@ -75,6 +212,15 @@ module Servactory
|
|
|
75
212
|
end
|
|
76
213
|
end
|
|
77
214
|
|
|
215
|
+
# Builds the error message lambda.
|
|
216
|
+
#
|
|
217
|
+
# Handles three message sources:
|
|
218
|
+
# 1. Custom Proc message (called with context)
|
|
219
|
+
# 2. Custom String message (returned as-is)
|
|
220
|
+
# 3. Default message from subclass implementation
|
|
221
|
+
#
|
|
222
|
+
# @param option [WorkOption] Parsed option data
|
|
223
|
+
# @return [Proc] Lambda that generates error message
|
|
78
224
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
79
225
|
def must_content_message_with(option)
|
|
80
226
|
is_option_message_present = option.message.present?
|
|
@@ -132,26 +278,74 @@ module Servactory
|
|
|
132
278
|
|
|
133
279
|
########################################################################
|
|
134
280
|
|
|
281
|
+
# Validates condition for input attribute.
|
|
282
|
+
#
|
|
283
|
+
# @abstract Subclasses must implement this method
|
|
284
|
+
# @param input [Object] Input attribute object
|
|
285
|
+
# @param value [Object] Current value being validated
|
|
286
|
+
# @param option [WorkOption] Validation option configuration
|
|
287
|
+
# @return [Boolean] true if valid, false otherwise
|
|
288
|
+
# @raise [RuntimeError] If not implemented in subclass
|
|
135
289
|
def condition_for_input_with(**)
|
|
136
290
|
raise "Need to implement `condition_for_input_with(**attributes)` method"
|
|
137
291
|
end
|
|
138
292
|
|
|
293
|
+
# Validates condition for internal attribute.
|
|
294
|
+
#
|
|
295
|
+
# @abstract Subclasses must implement this method
|
|
296
|
+
# @param internal [Object] Internal attribute object
|
|
297
|
+
# @param value [Object] Current value being validated
|
|
298
|
+
# @param option [WorkOption] Validation option configuration
|
|
299
|
+
# @return [Boolean] true if valid, false otherwise
|
|
300
|
+
# @raise [RuntimeError] If not implemented in subclass
|
|
139
301
|
def condition_for_internal_with(**)
|
|
140
302
|
raise "Need to implement `condition_for_internal_with(**attributes)` method"
|
|
141
303
|
end
|
|
142
304
|
|
|
305
|
+
# Validates condition for output attribute.
|
|
306
|
+
#
|
|
307
|
+
# @abstract Subclasses must implement this method
|
|
308
|
+
# @param output [Object] Output attribute object
|
|
309
|
+
# @param value [Object] Current value being validated
|
|
310
|
+
# @param option [WorkOption] Validation option configuration
|
|
311
|
+
# @return [Boolean] true if valid, false otherwise
|
|
312
|
+
# @raise [RuntimeError] If not implemented in subclass
|
|
143
313
|
def condition_for_output_with(**)
|
|
144
314
|
raise "Need to implement `condition_for_output_with(**attributes)` method"
|
|
145
315
|
end
|
|
146
316
|
|
|
317
|
+
# Generates error message for input validation failure.
|
|
318
|
+
#
|
|
319
|
+
# @abstract Subclasses must implement this method
|
|
320
|
+
# @param input [Object] Input attribute object
|
|
321
|
+
# @param option_name [Symbol] Name of the failed option
|
|
322
|
+
# @param option_value [Object] Expected value
|
|
323
|
+
# @return [String] Human-readable error message
|
|
324
|
+
# @raise [RuntimeError] If not implemented in subclass
|
|
147
325
|
def message_for_input_with(**)
|
|
148
326
|
raise "Need to implement `message_for_input_with(**attributes)` method"
|
|
149
327
|
end
|
|
150
328
|
|
|
329
|
+
# Generates error message for internal validation failure.
|
|
330
|
+
#
|
|
331
|
+
# @abstract Subclasses must implement this method
|
|
332
|
+
# @param internal [Object] Internal attribute object
|
|
333
|
+
# @param option_name [Symbol] Name of the failed option
|
|
334
|
+
# @param option_value [Object] Expected value
|
|
335
|
+
# @return [String] Human-readable error message
|
|
336
|
+
# @raise [RuntimeError] If not implemented in subclass
|
|
151
337
|
def message_for_internal_with(**)
|
|
152
338
|
raise "Need to implement `message_for_internal_with(**attributes)` method"
|
|
153
339
|
end
|
|
154
340
|
|
|
341
|
+
# Generates error message for output validation failure.
|
|
342
|
+
#
|
|
343
|
+
# @abstract Subclasses must implement this method
|
|
344
|
+
# @param output [Object] Output attribute object
|
|
345
|
+
# @param option_name [Symbol] Name of the failed option
|
|
346
|
+
# @param option_value [Object] Expected value
|
|
347
|
+
# @return [String] Human-readable error message
|
|
348
|
+
# @raise [RuntimeError] If not implemented in subclass
|
|
155
349
|
def message_for_output_with(**)
|
|
156
350
|
raise "Need to implement `message_for_output_with(**attributes)` method"
|
|
157
351
|
end
|