servactory 2.16.1 → 3.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -9
  3. data/config/locales/de.yml +134 -0
  4. data/config/locales/en.yml +15 -0
  5. data/config/locales/es.yml +134 -0
  6. data/config/locales/fr.yml +134 -0
  7. data/config/locales/it.yml +134 -0
  8. data/config/locales/ru.yml +15 -0
  9. data/lib/generators/README.md +45 -0
  10. data/lib/generators/servactory/base.rb +82 -0
  11. data/lib/generators/servactory/extension/USAGE +54 -0
  12. data/lib/generators/servactory/extension/extension_generator.rb +41 -0
  13. data/lib/generators/servactory/extension/templates/extension.rb.tt +62 -0
  14. data/lib/generators/servactory/install/USAGE +27 -0
  15. data/lib/generators/servactory/install/install_generator.rb +94 -0
  16. data/lib/generators/servactory/{templates/services/application_service/base.rb → install/templates/application_service/base.rb.tt} +29 -19
  17. data/lib/generators/servactory/{templates/services/application_service/exceptions.rb → install/templates/application_service/exceptions.rb.tt} +1 -1
  18. data/lib/generators/servactory/{templates/services/application_service/result.rb → install/templates/application_service/result.rb.tt} +1 -1
  19. data/lib/generators/servactory/rspec/USAGE +46 -0
  20. data/lib/generators/servactory/rspec/rspec_generator.rb +95 -0
  21. data/lib/generators/servactory/rspec/templates/service_spec.rb.tt +58 -0
  22. data/lib/generators/servactory/service/USAGE +51 -0
  23. data/lib/generators/servactory/service/service_generator.rb +56 -0
  24. data/lib/generators/servactory/service/templates/service.rb.tt +22 -0
  25. data/lib/servactory/actions/collection.rb +56 -1
  26. data/lib/servactory/actions/dsl.rb +11 -11
  27. data/lib/servactory/actions/tools/rules.rb +1 -1
  28. data/lib/servactory/actions/tools/runner.rb +111 -28
  29. data/lib/servactory/base.rb +1 -7
  30. data/lib/servactory/configuration/actions/aliases/collection.rb +5 -0
  31. data/lib/servactory/configuration/actions/rescue_handlers/collection.rb +5 -0
  32. data/lib/servactory/configuration/actions/shortcuts/collection.rb +5 -0
  33. data/lib/servactory/configuration/collection_mode/class_names_collection.rb +5 -0
  34. data/lib/servactory/configuration/config.rb +36 -0
  35. data/lib/servactory/configuration/configurable.rb +95 -0
  36. data/lib/servactory/configuration/dsl.rb +3 -27
  37. data/lib/servactory/configuration/factory.rb +20 -20
  38. data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -0
  39. data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +5 -0
  40. data/lib/servactory/context/warehouse/inputs.rb +2 -2
  41. data/lib/servactory/context/workspace/inputs.rb +2 -2
  42. data/lib/servactory/context/workspace/internals.rb +2 -2
  43. data/lib/servactory/context/workspace/outputs.rb +2 -2
  44. data/lib/servactory/context/workspace.rb +11 -7
  45. data/lib/servactory/dsl.rb +10 -8
  46. data/lib/servactory/maintenance/attributes/tools/validation.rb +1 -1
  47. data/lib/servactory/result.rb +2 -2
  48. data/lib/servactory/test_kit/fake_type.rb +23 -0
  49. data/lib/servactory/test_kit/result.rb +45 -0
  50. data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
  51. data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
  52. data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
  53. data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
  54. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
  55. data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
  56. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
  57. data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
  58. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
  59. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
  60. data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
  61. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
  62. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
  63. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
  64. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
  65. data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
  66. data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
  67. data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
  68. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
  69. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
  70. data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
  71. data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
  72. data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
  73. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
  74. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
  75. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
  76. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
  77. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
  78. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
  79. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
  80. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
  81. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
  82. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
  83. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
  84. data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
  85. data/lib/servactory/test_kit/utils/faker.rb +62 -2
  86. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
  87. data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
  88. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
  89. data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
  90. data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
  91. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
  92. data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
  93. data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
  94. data/lib/servactory/tool_kit/dynamic_options/target.rb +248 -0
  95. data/lib/servactory/version.rb +4 -4
  96. data/lib/servactory.rb +6 -0
  97. metadata +57 -48
  98. data/lib/generators/servactory/install_generator.rb +0 -21
  99. data/lib/generators/servactory/rspec_generator.rb +0 -88
  100. data/lib/generators/servactory/service_generator.rb +0 -49
  101. data/lib/servactory/configuration/setup.rb +0 -97
  102. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
  103. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
  104. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
  105. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
  106. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
  107. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
  108. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
  109. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
  110. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
  111. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +0 -199
@@ -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: /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
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
- # NOTE: Pattern 4 » https://dev.to/rasaf_ibrahim/write-regex-password-validation-like-a-pro-5175
18
- # Password must contain one digit from 1 to 9, one lowercase letter, one
19
- # uppercase letter, and one underscore, and it must be 8-16 characters long.
20
- # Usage of any other special character and usage of space is optional.
21
- pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,16}$/,
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: /^(true|false|0|1)$/i,
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
- return [false, :wrong_pattern] if format_pattern.present? && !value.match?(Regexp.compile(format_pattern))
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"