servactory 3.0.4 → 3.1.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/servactory/actions/action.rb +2 -2
  3. data/lib/servactory/actions/collection.rb +1 -1
  4. data/lib/servactory/actions/stages/collection.rb +1 -1
  5. data/lib/servactory/configuration/config.rb +0 -16
  6. data/lib/servactory/configuration/configurable.rb +3 -6
  7. data/lib/servactory/configuration/hash_mode/class_names_collection.rb +1 -4
  8. data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +57 -8
  9. data/lib/servactory/context/warehouse/inputs.rb +28 -15
  10. data/lib/servactory/context/workspace/inputs.rb +27 -18
  11. data/lib/servactory/context/workspace/internals.rb +44 -27
  12. data/lib/servactory/context/workspace/outputs.rb +31 -23
  13. data/lib/servactory/dsl.rb +1 -1
  14. data/lib/servactory/info/builder.rb +2 -3
  15. data/lib/servactory/inputs/collection.rb +14 -21
  16. data/lib/servactory/inputs/input.rb +6 -103
  17. data/lib/servactory/inputs/tools/validation.rb +32 -57
  18. data/lib/servactory/inputs/validations/required.rb +3 -2
  19. data/lib/servactory/internals/collection.rb +5 -24
  20. data/lib/servactory/internals/internal.rb +4 -112
  21. data/lib/servactory/maintenance/attributes/base.rb +135 -0
  22. data/lib/servactory/maintenance/attributes/collection.rb +109 -0
  23. data/lib/servactory/maintenance/attributes/option_helper.rb +33 -12
  24. data/lib/servactory/maintenance/{attributes/options_collection.rb → options/collection.rb} +6 -7
  25. data/lib/servactory/maintenance/{attributes → options}/define_conflict.rb +1 -1
  26. data/lib/servactory/maintenance/{attributes → options}/define_method.rb +1 -1
  27. data/lib/servactory/maintenance/options/helper.rb +23 -0
  28. data/lib/servactory/maintenance/{attributes → options}/option.rb +50 -27
  29. data/lib/servactory/maintenance/options/registrar.rb +200 -0
  30. data/lib/servactory/maintenance/{attributes/validations → validations/checkers}/must.rb +25 -8
  31. data/lib/servactory/maintenance/{attributes/validations → validations/checkers}/type.rb +6 -5
  32. data/lib/servactory/maintenance/validations/concerns/error_builder.rb +50 -0
  33. data/lib/servactory/maintenance/validations/performer.rb +57 -0
  34. data/lib/servactory/maintenance/validations/support/type_validator.rb +36 -0
  35. data/lib/servactory/maintenance/{attributes → validations}/translator/must.rb +1 -1
  36. data/lib/servactory/maintenance/{attributes → validations}/translator/type.rb +1 -1
  37. data/lib/servactory/outputs/collection.rb +5 -24
  38. data/lib/servactory/outputs/output.rb +4 -112
  39. data/lib/servactory/result.rb +48 -39
  40. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +2 -4
  41. data/lib/servactory/tool_kit/dynamic_options/must.rb +2 -2
  42. data/lib/servactory/tool_kit/dynamic_options/schema.rb +7 -9
  43. data/lib/servactory/utils.rb +0 -8
  44. data/lib/servactory/version.rb +3 -3
  45. data/lib/servactory.rb +4 -0
  46. metadata +19 -22
  47. data/lib/servactory/maintenance/attributes/options/registrar.rb +0 -183
  48. data/lib/servactory/maintenance/attributes/tools/validation.rb +0 -84
  49. data/lib/servactory/maintenance/attributes/validations/concerns/error_builder.rb +0 -52
  50. data/lib/servactory/maintenance/validations/types.rb +0 -34
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Maintenance
5
+ module Attributes
6
+ # Collection wrapper for managing attribute objects (inputs, internals, outputs).
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Collection provides a polymorphic base for storing and querying attribute
11
+ # instances. It wraps a Set to ensure uniqueness and delegates common
12
+ # enumeration methods. Subclasses override `lookup_name` to customize
13
+ # how attributes are indexed and filtered (e.g., inputs use `internal_name`
14
+ # for the `as:` parameter).
15
+ #
16
+ # ## Usage
17
+ #
18
+ # Subclasses are used internally by Input, Internal, and Output DSL modules
19
+ # to manage their registered attributes:
20
+ #
21
+ # ```ruby
22
+ # collection = Collection.new
23
+ # collection << attribute
24
+ #
25
+ # collection.names # => [:first_name, :last_name]
26
+ # collection.find_by(name: :first_name) # => attribute instance
27
+ # collection.only(:first_name) # => filtered Collection
28
+ # collection.except(:last_name) # => filtered Collection
29
+ # ```
30
+ #
31
+ # ## Performance
32
+ #
33
+ # The collection uses memoization for frequently accessed data:
34
+ # - `attributes_index` - cached hash for O(1) lookups by name
35
+ #
36
+ class Collection
37
+ extend Forwardable
38
+
39
+ def_delegators :@collection,
40
+ :<<,
41
+ :filter,
42
+ :each, :each_with_object,
43
+ :map, :to_h,
44
+ :merge
45
+
46
+ # Initializes the collection with an optional pre-built Set.
47
+ #
48
+ # @param collection [Set] initial set of attributes
49
+ # @return [Collection]
50
+ def initialize(collection = Set.new)
51
+ @collection = collection
52
+ end
53
+
54
+ # Returns names of all attributes in the collection.
55
+ #
56
+ # @return [Array<Symbol>] list of attribute names
57
+ def names
58
+ map(&:name)
59
+ end
60
+
61
+ # Returns a new collection containing only the named attributes.
62
+ #
63
+ # @param names [Array<Symbol>] attribute names to include
64
+ # @return [Collection] filtered collection
65
+ def only(*names)
66
+ self.class.new(filter { |attribute| names.include?(lookup_name(attribute)) })
67
+ end
68
+
69
+ # Returns a new collection excluding the named attributes.
70
+ #
71
+ # @param names [Array<Symbol>] attribute names to exclude
72
+ # @return [Collection] filtered collection
73
+ def except(*names)
74
+ self.class.new(filter { |attribute| names.exclude?(lookup_name(attribute)) })
75
+ end
76
+
77
+ # Finds an attribute by its name using indexed lookup.
78
+ #
79
+ # @param name [Symbol] the attribute name to find
80
+ # @return [Object, nil] the found attribute or nil
81
+ def find_by(name:)
82
+ attributes_index[name]
83
+ end
84
+
85
+ private
86
+
87
+ # Builds and caches a hash index for O(1) attribute lookups.
88
+ #
89
+ # @return [Hash{Symbol => Object}] attribute names mapped to attribute instances
90
+ def attributes_index
91
+ @attributes_index ||= each_with_object({}) do |attribute, index|
92
+ index[lookup_name(attribute)] = attribute
93
+ end
94
+ end
95
+
96
+ # Returns the name used for indexing and filtering a given attribute.
97
+ #
98
+ # Subclasses override this to use alternative name fields
99
+ # (e.g., `internal_name` for inputs with the `as:` parameter).
100
+ #
101
+ # @param attribute [Object] the attribute to extract a name from
102
+ # @return [Symbol] the lookup name
103
+ def lookup_name(attribute)
104
+ attribute.name
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -3,19 +3,40 @@
3
3
  module Servactory
4
4
  module Maintenance
5
5
  module Attributes
6
- class OptionHelper
7
- attr_reader :name,
8
- :equivalent,
9
- :meta
10
-
6
+ # @deprecated Use {Servactory::Maintenance::Options::Helper} instead.
7
+ # This class is maintained for backward compatibility only and will be
8
+ # removed in a future major version.
9
+ #
10
+ # OptionHelper provides a simple way to define custom option helpers
11
+ # for service inputs, internals, and outputs configuration.
12
+ #
13
+ # @example Creating a custom option helper
14
+ # Servactory::Maintenance::Attributes::OptionHelper.new(
15
+ # name: :must_be_positive,
16
+ # equivalent: {
17
+ # must: {
18
+ # be_positive: {
19
+ # is: ->(value:, **) { value.positive? },
20
+ # message: "must be positive"
21
+ # }
22
+ # }
23
+ # }
24
+ # )
25
+ #
26
+ # @see Servactory::Maintenance::Options::Helper The new preferred class
27
+ class OptionHelper < Servactory::Maintenance::Options::Helper
28
+ # @deprecated This class is deprecated. Use Options::Helper instead.
29
+ #
30
+ # Creates a new OptionHelper instance.
31
+ #
32
+ # @param name [Symbol] The name of the option helper
33
+ # @param equivalent [Hash, Proc] The equivalent option configuration
34
+ # @param meta [Hash] Additional metadata for the helper
35
+ # @return [OptionHelper] A new instance
11
36
  def initialize(name:, equivalent:, meta: {})
12
- @name = name
13
- @equivalent = equivalent
14
- @meta = meta
15
- end
16
-
17
- def dynamic_option?
18
- meta[:type] == :dynamic_option
37
+ warn "[DEPRECATION] Servactory::Maintenance::Attributes::OptionHelper is deprecated. " \
38
+ "Use Servactory::Maintenance::Options::Helper instead."
39
+ super
19
40
  end
20
41
  end
21
42
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  module Servactory
4
4
  module Maintenance
5
- module Attributes
5
+ module Options
6
6
  # Collection wrapper for managing Option objects.
7
7
  #
8
8
  # ## Purpose
9
9
  #
10
- # OptionsCollection provides a unified interface for storing and querying
10
+ # Collection provides a unified interface for storing and querying
11
11
  # Option instances associated with service attributes (inputs, internals, outputs).
12
12
  # It wraps a Set to ensure uniqueness and delegates common enumeration methods.
13
13
  #
@@ -17,7 +17,7 @@ module Servactory
17
17
  # to manage their registered options:
18
18
  #
19
19
  # ```ruby
20
- # collection = OptionsCollection.new
20
+ # collection = Collection.new
21
21
  # collection << Option.new(name: :required, ...)
22
22
  # collection << Option.new(name: :types, ...)
23
23
  #
@@ -32,21 +32,20 @@ module Servactory
32
32
  # - `options_for_checks` - cached hash for validation pipeline
33
33
  # - `options_index` - cached hash for O(1) lookups by name
34
34
  #
35
- class OptionsCollection
35
+ class Collection
36
36
  extend Forwardable
37
37
 
38
38
  def_delegators :@collection,
39
39
  :<<,
40
40
  :filter,
41
- :each, :each_with_object,
41
+ :each_with_object,
42
42
  :map, :flat_map,
43
- :find,
44
43
  :size,
45
44
  :empty?
46
45
 
47
46
  # Initializes an empty collection.
48
47
  #
49
- # @return [OptionsCollection]
48
+ # @return [Collection]
50
49
  def initialize
51
50
  @collection = Set.new
52
51
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Servactory
4
4
  module Maintenance
5
- module Attributes
5
+ module Options
6
6
  class DefineConflict
7
7
  attr_reader :content
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Servactory
4
4
  module Maintenance
5
- module Attributes
5
+ module Options
6
6
  class DefineMethod
7
7
  attr_reader :name,
8
8
  :content
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Maintenance
5
+ module Options
6
+ class Helper
7
+ attr_reader :name,
8
+ :equivalent,
9
+ :meta
10
+
11
+ def initialize(name:, equivalent:, meta: {})
12
+ @name = name
13
+ @equivalent = equivalent
14
+ @meta = meta
15
+ end
16
+
17
+ def dynamic_option?
18
+ meta[:type] == :dynamic_option
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Servactory
4
4
  module Maintenance
5
- module Attributes
5
+ module Options
6
6
  class Option # rubocop:disable Metrics/ClassLength
7
7
  DEFAULT_BODY = ->(key:, body:, message: nil) { { key => body, message: } }
8
8
 
@@ -14,9 +14,10 @@ module Servactory
14
14
  :define_conflicts,
15
15
  :need_for_checks,
16
16
  :body,
17
- :body_key
17
+ :body_key,
18
+ :return_value_on_access
18
19
 
19
- # rubocop:disable Metrics/MethodLength
20
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
20
21
  def initialize(
21
22
  name:,
22
23
  attribute:,
@@ -28,11 +29,15 @@ module Servactory
28
29
  body_value: true,
29
30
  define_methods: nil,
30
31
  define_conflicts: nil,
31
- with_advanced_mode: true,
32
+ detect_advanced_mode: true,
33
+ return_value_on_access: false,
34
+ normalizer: nil,
32
35
  **options
33
36
  )
34
37
  @name = name.to_sym
35
38
  @body_key = body_key
39
+ @return_value_on_access = return_value_on_access
40
+ @normalizer = normalizer
36
41
  @validation_class = validation_class
37
42
  @define_methods = define_methods
38
43
  @define_conflicts = define_conflicts
@@ -44,12 +49,12 @@ module Servactory
44
49
  body_key:,
45
50
  body_value:,
46
51
  body_fallback:,
47
- with_advanced_mode:
52
+ detect_advanced_mode:
48
53
  )
49
54
 
50
55
  apply_dynamic_methods_to(attribute:)
51
56
  end
52
- # rubocop:enable Metrics/MethodLength
57
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
53
58
 
54
59
  def value
55
60
  return body unless body.is_a?(Hash)
@@ -57,51 +62,69 @@ module Servactory
57
62
  body.fetch(@body_key, body)
58
63
  end
59
64
 
65
+ def body_for_access
66
+ @return_value_on_access ? value : body
67
+ end
68
+
60
69
  def need_for_checks?
61
70
  need_for_checks
62
71
  end
63
72
 
64
73
  private
65
74
 
75
+ # rubocop:disable Metrics/MethodLength
66
76
  def construct_body(
67
77
  original_value:,
68
78
  options:,
69
79
  body_key:,
70
80
  body_value:,
71
81
  body_fallback:,
72
- with_advanced_mode:
82
+ detect_advanced_mode:
73
83
  )
74
- return original_value if original_value.present?
75
- return options.fetch(@name, body_fallback) unless with_advanced_mode
84
+ return wrap_and_normalize(original_value) if original_value.present?
85
+
86
+ raw_value = options.fetch(@name, body_fallback)
87
+
88
+ result = if detect_advanced_mode
89
+ use_advanced_mode(
90
+ raw_value:,
91
+ body_key:,
92
+ body_value:,
93
+ body_fallback:
94
+ )
95
+ else
96
+ wrap_value(raw_value)
97
+ end
98
+
99
+ apply_normalizer(result)
100
+ end
101
+ # rubocop:enable Metrics/MethodLength
76
102
 
77
- use_advanced_mode(
78
- options:,
79
- body_key:,
80
- body_value:,
81
- body_fallback:
82
- )
103
+ def wrap_and_normalize(value)
104
+ result = wrap_value(value)
105
+ apply_normalizer(result)
83
106
  end
84
107
 
85
- def use_advanced_mode(options:, body_key:, body_value:, body_fallback:)
86
- default_body = create_default_body(body_key:, body_fallback:)
87
- body = extract_body_from_options(options:, default_body:)
108
+ def apply_normalizer(body)
109
+ return body unless @normalizer && body.is_a?(Hash)
110
+
111
+ body[@body_key] = @normalizer.call(body[@body_key])
112
+ body
113
+ end
114
+
115
+ def wrap_value(value)
116
+ DEFAULT_BODY.call(key: @body_key, body: value)
117
+ end
88
118
 
119
+ def use_advanced_mode(raw_value:, body_key:, body_value:, body_fallback:)
89
120
  construct_advanced_body(
90
- body:,
121
+ body: raw_value,
91
122
  body_key:,
92
123
  body_value:,
93
124
  body_fallback:
94
125
  )
95
126
  end
96
127
 
97
- def create_default_body(body_key:, body_fallback:)
98
- DEFAULT_BODY.call(key: body_key, body: body_fallback)
99
- end
100
-
101
- def extract_body_from_options(options:, default_body:)
102
- options.fetch(@name, default_body)
103
- end
104
-
105
128
  def construct_advanced_body(body:, body_key:, body_value:, body_fallback:)
106
129
  if body.is_a?(Hash)
107
130
  build_hash_body(body:, body_key:, body_value:, body_fallback:)
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Maintenance
5
+ module Options
6
+ class Registrar # rubocop:disable Metrics/ClassLength
7
+ RESERVED_OPTIONS = %i[
8
+ type
9
+ required
10
+ default
11
+ collection
12
+ must
13
+ prepare
14
+ ].freeze
15
+
16
+ DEFAULT_FEATURES = {
17
+ required: false,
18
+ types: false,
19
+ default: false,
20
+ must: false,
21
+ prepare: false
22
+ }.freeze
23
+
24
+ private_constant :DEFAULT_FEATURES
25
+
26
+ def self.register(...)
27
+ new(...).register
28
+ end
29
+
30
+ def initialize(attribute:, options:, features:)
31
+ @attribute = attribute
32
+ @options = options
33
+ @features = DEFAULT_FEATURES.merge(features)
34
+ end
35
+
36
+ ########################################################################
37
+
38
+ def register
39
+ register_feature(:required, Servactory::Inputs::Validations::Required)
40
+ register_feature(:types, Servactory::Maintenance::Validations::Checkers::Type)
41
+ register_feature(:default, Servactory::Maintenance::Validations::Checkers::Type)
42
+ register_feature(:must, Servactory::Maintenance::Validations::Checkers::Must)
43
+ register_feature(:prepare, nil)
44
+
45
+ self
46
+ end
47
+
48
+ def collection
49
+ @collection ||= Servactory::Maintenance::Options::Collection.new
50
+ end
51
+
52
+ private
53
+
54
+ def register_feature(feature_name, validation_class)
55
+ return unless @features.fetch(feature_name)
56
+
57
+ method_name = "register_#{feature_name}_option"
58
+ send(method_name, validation_class)
59
+ end
60
+
61
+ ########################################################################
62
+
63
+ def register_required_option(validation_class) # rubocop:disable Metrics/MethodLength
64
+ create_option(
65
+ name: :required,
66
+ validation_class:,
67
+ define_methods: required_define_methods,
68
+ define_conflicts: required_define_conflicts,
69
+ need_for_checks: true,
70
+ body_key: :is,
71
+ body_fallback: true,
72
+ detect_advanced_mode: true,
73
+ return_value_on_access: false
74
+ )
75
+ end
76
+
77
+ def register_types_option(validation_class) # rubocop:disable Metrics/MethodLength
78
+ create_option(
79
+ name: :types,
80
+ validation_class:,
81
+ original_value: extract_types_value,
82
+ need_for_checks: true,
83
+ body_key: :is,
84
+ body_fallback: nil,
85
+ detect_advanced_mode: true,
86
+ return_value_on_access: true,
87
+ normalizer: ->(v) { Array(v).uniq }
88
+ )
89
+ end
90
+
91
+ def register_default_option(validation_class) # rubocop:disable Metrics/MethodLength
92
+ create_option(
93
+ name: :default,
94
+ validation_class:,
95
+ define_methods: [
96
+ create_define_method(
97
+ name: :default_value_present?,
98
+ content: ->(option:) { !option[:is].nil? }
99
+ )
100
+ ],
101
+ need_for_checks: true,
102
+ body_key: :is,
103
+ body_fallback: nil,
104
+ detect_advanced_mode: false,
105
+ return_value_on_access: true
106
+ )
107
+ end
108
+
109
+ def register_must_option(validation_class) # rubocop:disable Metrics/MethodLength
110
+ create_option(
111
+ name: :must,
112
+ validation_class:,
113
+ define_methods: [
114
+ create_define_method(
115
+ name: :must_present?,
116
+ content: ->(option:) { option[:rules].present? }
117
+ )
118
+ ],
119
+ need_for_checks: true,
120
+ body_key: :rules,
121
+ body_fallback: nil,
122
+ detect_advanced_mode: false,
123
+ return_value_on_access: true
124
+ )
125
+ end
126
+
127
+ def register_prepare_option(_validation_class) # rubocop:disable Metrics/MethodLength
128
+ create_option(
129
+ name: :prepare,
130
+ validation_class: nil,
131
+ define_methods: [
132
+ create_define_method(
133
+ name: :prepare_present?,
134
+ content: ->(option:) { option[:in].present? }
135
+ )
136
+ ],
137
+ need_for_checks: false,
138
+ body_key: :in,
139
+ body_fallback: false,
140
+ detect_advanced_mode: true,
141
+ return_value_on_access: false
142
+ )
143
+ end
144
+
145
+ def extract_types_value
146
+ type_option = @options[:type]
147
+ return nil if type_option.nil?
148
+
149
+ # Advanced Mode: type: { is: String, message: "..." }
150
+ return nil if type_option.is_a?(Hash) && type_option.key?(:is)
151
+
152
+ # Simple Mode: type: String or type: [String, Integer]
153
+ Array(type_option).uniq
154
+ end
155
+
156
+ ########################################################################
157
+
158
+ def required_define_methods
159
+ [
160
+ create_define_method(
161
+ name: :required?,
162
+ content: ->(option:) { Servactory::Utils.true?(option[:is]) }
163
+ ),
164
+ create_define_method(
165
+ name: :optional?,
166
+ content: ->(option:) { !Servactory::Utils.true?(option[:is]) }
167
+ )
168
+ ]
169
+ end
170
+
171
+ def required_define_conflicts
172
+ [
173
+ Servactory::Maintenance::Options::DefineConflict.new(
174
+ content: -> { :required_vs_default if @attribute.required? && @attribute.default_value_present? }
175
+ )
176
+ ]
177
+ end
178
+
179
+ ########################################################################
180
+
181
+ def create_option(name:, validation_class:, **options)
182
+ collection << Servactory::Maintenance::Options::Option.new(
183
+ name:,
184
+ attribute: @attribute,
185
+ validation_class:,
186
+ **options,
187
+ **@options
188
+ )
189
+ end
190
+
191
+ def create_define_method(name:, content:)
192
+ Servactory::Maintenance::Options::DefineMethod.new(
193
+ name:,
194
+ content:
195
+ )
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Servactory
4
4
  module Maintenance
5
- module Attributes
6
- module Validations
5
+ module Validations
6
+ module Checkers
7
7
  # Validates custom conditions defined with `must` option.
8
8
  #
9
9
  # ## Purpose
@@ -82,9 +82,10 @@ module Servactory
82
82
  # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute being validated
83
83
  # @param value [Object] Value to pass to check lambda
84
84
  # @param code [Symbol] Condition identifier (e.g., :be_adult)
85
- # @param options [Hash] Condition options with :is (lambda) and :message
85
+ # @param options [Hash, Proc] Condition options with :is (lambda) and :message, or just a lambda
86
86
  # @return [String, nil] Error message on failure, nil on success
87
87
  def self.validate_condition(context:, attribute:, value:, code:, options:) # rubocop:disable Metrics/MethodLength
88
+ options = normalize_rule_options(options)
88
89
  check, message = options.values_at(:is, :message)
89
90
 
90
91
  check_result, check_result_code, meta = call_check(
@@ -111,6 +112,22 @@ module Servactory
111
112
  end
112
113
  private_class_method :validate_condition
113
114
 
115
+ # Normalizes rule options to always have :is and :message keys.
116
+ #
117
+ # Supports Simple Mode where only a lambda is provided:
118
+ # must: { be_adult: ->(value:) { value >= 18 } }
119
+ # Converts to Advanced Mode format:
120
+ # must: { be_adult: { is: lambda, message: nil } }
121
+ #
122
+ # @param options [Hash, Proc] Either { is: lambda, message: ... } or just lambda
123
+ # @return [Hash] Normalized options with :is and :message keys
124
+ def self.normalize_rule_options(options)
125
+ return options if options.is_a?(Hash)
126
+
127
+ { is: options, message: nil }
128
+ end
129
+ private_class_method :normalize_rule_options
130
+
114
131
  # Executes the check lambda with exception handling.
115
132
  #
116
133
  # Catches any StandardError from the lambda and converts it to a
@@ -124,7 +141,7 @@ module Servactory
124
141
  # @return [Array] On success: [result, reason_code, meta_hash]
125
142
  # @return [Array] On exception: [:syntax_error, error_message_string, nil]
126
143
  def self.call_check(context:, attribute:, value:, check:, code:)
127
- check.call(value:, **Servactory::Utils.fetch_hash_with_desired_attribute(attribute))
144
+ check.call(value:, **attribute.typed_actor_kwargs)
128
145
  rescue StandardError => e
129
146
  # Return formatted syntax error message
130
147
  syntax_error_message = build_syntax_error_message(
@@ -154,12 +171,12 @@ module Servactory
154
171
  # @param message [String, Proc, nil] Custom error message
155
172
  # @return [String] Processed error message
156
173
  def self.build_error_message(context:, attribute:, value:, code:, reason:, meta:, message:)
157
- message = message.presence || Servactory::Maintenance::Attributes::Translator::Must.default_message
174
+ message = message.presence || Servactory::Maintenance::Validations::Translator::Must.default_message
158
175
 
159
176
  process_message(
160
177
  message,
161
178
  service: context.send(:servactory_service_info),
162
- **Servactory::Utils.fetch_hash_with_desired_attribute(attribute),
179
+ **attribute.typed_actor_kwargs,
163
180
  value:,
164
181
  code:,
165
182
  reason:,
@@ -180,12 +197,12 @@ module Servactory
180
197
  # @param exception_message [String] Original exception message
181
198
  # @return [String] Formatted syntax error message
182
199
  def self.build_syntax_error_message(context:, attribute:, value:, code:, exception_message:)
183
- message = Servactory::Maintenance::Attributes::Translator::Must.syntax_error_message
200
+ message = Servactory::Maintenance::Validations::Translator::Must.syntax_error_message
184
201
 
185
202
  process_message(
186
203
  message,
187
204
  service: context.send(:servactory_service_info),
188
- **Servactory::Utils.fetch_hash_with_desired_attribute(attribute),
205
+ **attribute.typed_actor_kwargs,
189
206
  value:,
190
207
  code:,
191
208
  exception_message: