servactory 3.0.0.rc3 → 3.0.0.rc4

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/lib/servactory/context/warehouse/base.rb +20 -4
  3. data/lib/servactory/context/warehouse/crate.rb +39 -0
  4. data/lib/servactory/context/warehouse/inputs.rb +41 -10
  5. data/lib/servactory/context/warehouse/internals.rb +7 -2
  6. data/lib/servactory/context/warehouse/outputs.rb +7 -0
  7. data/lib/servactory/context/warehouse/setup.rb +53 -30
  8. data/lib/servactory/context/workspace/inputs.rb +6 -8
  9. data/lib/servactory/context/workspace/internals.rb +5 -7
  10. data/lib/servactory/context/workspace/outputs.rb +5 -7
  11. data/lib/servactory/inputs/input.rb +28 -18
  12. data/lib/servactory/inputs/tools/validation.rb +9 -9
  13. data/lib/servactory/inputs/validations/required.rb +51 -27
  14. data/lib/servactory/internals/internal.rb +20 -16
  15. data/lib/servactory/maintenance/attributes/tools/validation.rb +9 -10
  16. data/lib/servactory/maintenance/attributes/validations/concerns/error_builder.rb +52 -0
  17. data/lib/servactory/maintenance/attributes/validations/must.rb +164 -57
  18. data/lib/servactory/maintenance/attributes/validations/type.rb +77 -27
  19. data/lib/servactory/maintenance/validations/types.rb +18 -33
  20. data/lib/servactory/outputs/output.rb +18 -13
  21. data/lib/servactory/result.rb +266 -43
  22. data/lib/servactory/version.rb +1 -1
  23. metadata +3 -6
  24. data/lib/servactory/inputs/validations/base.rb +0 -21
  25. data/lib/servactory/inputs/validations/errors.rb +0 -17
  26. data/lib/servactory/maintenance/attributes/tools/check_errors.rb +0 -23
  27. data/lib/servactory/maintenance/attributes/validations/base.rb +0 -23
  28. data/lib/servactory/maintenance/attributes/validations/errors.rb +0 -19
@@ -13,6 +13,7 @@ module Servactory
13
13
  @context = context
14
14
  @attribute = attribute
15
15
  @value = value
16
+ @first_error = nil
16
17
  end
17
18
 
18
19
  def validate!
@@ -26,22 +27,24 @@ module Servactory
26
27
  def process
27
28
  @attribute.options_for_checks.each do |check_key, check_options|
28
29
  process_option(check_key, check_options)
30
+ break if @first_error.present?
29
31
  end
30
32
  end
31
33
 
32
- def process_option(check_key, check_options)
34
+ def process_option(check_key, check_options) # rubocop:disable Metrics/MethodLength
33
35
  return if validation_classes.empty?
34
36
 
35
37
  validation_classes.each do |validation_class|
36
- errors_from_checks = process_validation_class(
38
+ error_message = process_validation_class(
37
39
  validation_class:,
38
40
  check_key:,
39
41
  check_options:
40
42
  )
41
43
 
42
- next if errors_from_checks.nil? || errors_from_checks.empty?
44
+ next if error_message.blank?
43
45
 
44
- errors.merge(errors_from_checks.to_a)
46
+ @first_error = error_message
47
+ break
45
48
  end
46
49
  end
47
50
 
@@ -67,16 +70,12 @@ module Servactory
67
70
 
68
71
  ########################################################################
69
72
 
70
- def errors
71
- @errors ||= Servactory::Maintenance::Attributes::Tools::CheckErrors.new
72
- end
73
-
74
73
  def raise_errors
75
- return if (tmp_errors = errors.not_blank).empty?
74
+ return if @first_error.nil?
76
75
 
77
76
  raise @context.config
78
77
  .public_send(:"#{@attribute.system_name}_exception_class")
79
- .new(context: @context, message: tmp_errors.first)
78
+ .new(context: @context, message: @first_error)
80
79
  end
81
80
  end
82
81
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Maintenance
5
+ module Attributes
6
+ module Validations
7
+ module Concerns
8
+ # Concern providing error message processing for validators.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # ErrorBuilder provides shared logic for processing error messages that
13
+ # can be either static strings or dynamic Procs. This allows validators
14
+ # to support both simple error messages and context-aware messages.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # Extend in validator classes:
19
+ #
20
+ # ```ruby
21
+ # class MyValidator
22
+ # extend Concerns::ErrorBuilder
23
+ #
24
+ # def self.build_error(...)
25
+ # process_message(message, **context)
26
+ # end
27
+ # end
28
+ # ```
29
+ #
30
+ # ## Methods Provided
31
+ #
32
+ # - `process_message` - converts String or Proc message to String
33
+ module ErrorBuilder
34
+ # Processes a message that may be a String or Proc.
35
+ #
36
+ # If message is a Proc, calls it with the provided attributes.
37
+ # If message is a String, returns it unchanged.
38
+ #
39
+ # @param message [String, Proc] The message to process
40
+ # @param attributes [Hash] Attributes to pass to Proc if message is callable
41
+ # @return [String] The processed message string
42
+ def process_message(message, **attributes)
43
+ return message unless message.is_a?(Proc)
44
+
45
+ message.call(**attributes)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -4,87 +4,194 @@ module Servactory
4
4
  module Maintenance
5
5
  module Attributes
6
6
  module Validations
7
- class Must < Base
7
+ # Validates custom conditions defined with `must` option.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Must validator executes user-defined validation lambdas against attribute
12
+ # values. It supports multiple named conditions per attribute and provides
13
+ # rich error context including custom messages, reason codes, and metadata.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Define must conditions on any attribute (input, internal, output):
18
+ #
19
+ # ```ruby
20
+ # class MyService < ApplicationService::Base
21
+ # input :age,
22
+ # type: Integer,
23
+ # must: {
24
+ # be_adult: {
25
+ # is: ->(value:) { value >= 18 },
26
+ # message: "Must be 18 or older"
27
+ # }
28
+ # }
29
+ #
30
+ # internal :discount,
31
+ # type: Float,
32
+ # must: {
33
+ # be_percentage: {
34
+ # is: ->(value:) { value.between?(0, 100) }
35
+ # }
36
+ # }
37
+ # end
38
+ # ```
39
+ class Must
40
+ extend Concerns::ErrorBuilder
41
+
42
+ # Validates all must conditions for an attribute.
43
+ #
44
+ # Iterates through check_options and validates each condition.
45
+ # Returns on first failure (early return pattern).
46
+ #
47
+ # @param context [Object] Service context for error message formatting
48
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute to validate
49
+ # @param value [Object] Value to validate against conditions
50
+ # @param check_key [Symbol] Must be :must to trigger validation
51
+ # @param check_options [Hash{Symbol => Hash}] Named conditions with :is and :message
52
+ # @return [String, nil] Error message on first failure, nil if all pass
8
53
  def self.check(context:, attribute:, value:, check_key:, check_options:)
9
54
  return unless should_be_checked_for?(attribute, check_key)
10
55
 
11
- new(context:, attribute:, value:, check_options:).check
56
+ check_options.each do |code, options|
57
+ error_message = validate_condition(context:, attribute:, value:, code:, options:)
58
+ return error_message if error_message.present?
59
+ end
60
+
61
+ nil
12
62
  end
13
63
 
64
+ # Determines if validation should run for given attribute and check key.
65
+ #
66
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute to check
67
+ # @param check_key [Symbol] Current validation check key
68
+ # @return [Boolean] true if this validator should run
14
69
  def self.should_be_checked_for?(attribute, check_key)
15
70
  check_key == :must && attribute.must_present?
16
71
  end
17
-
18
- ##########################################################################
19
-
20
- def initialize(context:, attribute:, value:, check_options:)
21
- super()
22
-
23
- @context = context
24
- @attribute = attribute
25
- @value = value
26
- @check_options = check_options
27
- end
28
-
29
- def check
30
- @check_options.each do |code, options|
31
- message, reason, meta = call_or_fetch_message_from(code, options)
32
-
33
- next if message.blank?
34
-
35
- add_error_with(message, code, reason, meta)
36
- end
37
-
38
- errors
39
- end
40
-
41
- private
42
-
43
- def call_or_fetch_message_from(code, options) # rubocop:disable Metrics/MethodLength
72
+ private_class_method :should_be_checked_for?
73
+
74
+ # Validates a single must condition by executing its check lambda.
75
+ #
76
+ # Executes the check lambda and handles three outcomes:
77
+ # - Lambda returns true: validation passes (returns nil)
78
+ # - Lambda returns [false, reason, meta]: validation fails with context
79
+ # - Lambda raises exception: returns syntax error message
80
+ #
81
+ # @param context [Object] Service context for error formatting
82
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute being validated
83
+ # @param value [Object] Value to pass to check lambda
84
+ # @param code [Symbol] Condition identifier (e.g., :be_adult)
85
+ # @param options [Hash] Condition options with :is (lambda) and :message
86
+ # @return [String, nil] Error message on failure, nil on success
87
+ def self.validate_condition(context:, attribute:, value:, code:, options:) # rubocop:disable Metrics/MethodLength
44
88
  check, message = options.values_at(:is, :message)
45
89
 
46
- check_result, check_result_code, meta =
47
- check.call(value: @value, **Servactory::Utils.fetch_hash_with_desired_attribute(@attribute))
90
+ check_result, check_result_code, meta = call_check(
91
+ context:,
92
+ attribute:,
93
+ value:,
94
+ check:,
95
+ code:
96
+ )
48
97
 
49
- return if check_result
98
+ # check_result is :syntax_error when syntax error occurred
99
+ return check_result_code if check_result == :syntax_error
100
+ return if check_result == true
50
101
 
51
- [
52
- message.presence || Servactory::Maintenance::Attributes::Translator::Must.default_message,
53
- check_result_code,
54
- meta
55
- ]
102
+ build_error_message(
103
+ context:,
104
+ attribute:,
105
+ value:,
106
+ code:,
107
+ reason: check_result_code,
108
+ meta:,
109
+ message:
110
+ )
111
+ end
112
+ private_class_method :validate_condition
113
+
114
+ # Executes the check lambda with exception handling.
115
+ #
116
+ # Catches any StandardError from the lambda and converts it to a
117
+ # formatted syntax error message for better debugging experience.
118
+ #
119
+ # @param context [Object] Service context for error formatting
120
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute being validated
121
+ # @param value [Object] Value to pass to lambda
122
+ # @param check [Proc] The validation lambda to execute
123
+ # @param code [Symbol] Condition identifier for error messages
124
+ # @return [Array] On success: [result, reason_code, meta_hash]
125
+ # @return [Array] On exception: [:syntax_error, error_message_string, nil]
126
+ def self.call_check(context:, attribute:, value:, check:, code:)
127
+ check.call(value:, **Servactory::Utils.fetch_hash_with_desired_attribute(attribute))
56
128
  rescue StandardError => e
57
- add_syntax_error_with(
58
- Servactory::Maintenance::Attributes::Translator::Must.syntax_error_message,
59
- code,
60
- e.message
129
+ # Return formatted syntax error message
130
+ syntax_error_message = build_syntax_error_message(
131
+ context:,
132
+ attribute:,
133
+ value:,
134
+ code:,
135
+ exception_message: e.message
61
136
  )
137
+ [:syntax_error, syntax_error_message, nil]
62
138
  end
63
-
64
- ########################################################################
65
-
66
- def add_error_with(message, code, reason, meta = {})
67
- add_error(
68
- message:,
69
- service: @context.send(:servactory_service_info),
70
- **Servactory::Utils.fetch_hash_with_desired_attribute(@attribute),
71
- value: @value,
139
+ private_class_method :call_check
140
+
141
+ # Builds error message from validation failure.
142
+ #
143
+ # Uses custom message if provided, otherwise falls back to default.
144
+ # Message can be a String or Proc for dynamic formatting.
145
+ #
146
+ # NOTE: Errors from message.call are NOT caught - they propagate to caller.
147
+ #
148
+ # @param context [Object] Service context for message formatting
149
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Failed attribute
150
+ # @param value [Object] The invalid value
151
+ # @param code [Symbol] Condition identifier
152
+ # @param reason [Symbol, nil] Failure reason from check lambda
153
+ # @param meta [Hash, nil] Additional metadata from check lambda
154
+ # @param message [String, Proc, nil] Custom error message
155
+ # @return [String] Processed error message
156
+ def self.build_error_message(context:, attribute:, value:, code:, reason:, meta:, message:)
157
+ message = message.presence || Servactory::Maintenance::Attributes::Translator::Must.default_message
158
+
159
+ process_message(
160
+ message,
161
+ service: context.send(:servactory_service_info),
162
+ **Servactory::Utils.fetch_hash_with_desired_attribute(attribute),
163
+ value:,
72
164
  code:,
73
165
  reason:,
74
166
  meta:
75
167
  )
76
168
  end
77
-
78
- def add_syntax_error_with(message, code, exception_message)
79
- add_error(
80
- message:,
81
- service: @context.send(:servactory_service_info),
82
- **Servactory::Utils.fetch_hash_with_desired_attribute(@attribute),
83
- value: @value,
169
+ private_class_method :build_error_message
170
+
171
+ # Builds error message for exceptions raised in check lambdas.
172
+ #
173
+ # Formats the exception message with context for debugging,
174
+ # including the condition code and original exception text.
175
+ #
176
+ # @param context [Object] Service context for message formatting
177
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute being validated
178
+ # @param value [Object] Value that caused the exception
179
+ # @param code [Symbol] Condition identifier where error occurred
180
+ # @param exception_message [String] Original exception message
181
+ # @return [String] Formatted syntax error message
182
+ def self.build_syntax_error_message(context:, attribute:, value:, code:, exception_message:)
183
+ message = Servactory::Maintenance::Attributes::Translator::Must.syntax_error_message
184
+
185
+ process_message(
186
+ message,
187
+ service: context.send(:servactory_service_info),
188
+ **Servactory::Utils.fetch_hash_with_desired_attribute(attribute),
189
+ value:,
84
190
  code:,
85
191
  exception_message:
86
192
  )
87
193
  end
194
+ private_class_method :build_syntax_error_message
88
195
  end
89
196
  end
90
197
  end
@@ -4,13 +4,68 @@ module Servactory
4
4
  module Maintenance
5
5
  module Attributes
6
6
  module Validations
7
- class Type < Base
7
+ # Validates that attribute values match their declared types.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Type validator ensures attribute values conform to declared type
12
+ # constraints. It supports single types, union types (arrays), and
13
+ # custom type definitions. Works with inputs, internals, and outputs.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Type validation is automatic based on the `type` option:
18
+ #
19
+ # ```ruby
20
+ # class MyService < ApplicationService::Base
21
+ # input :id, type: Integer
22
+ # input :data, type: [Hash, Array] # union type
23
+ # input :status, type: String, required: false
24
+ #
25
+ # internal :result, type: CustomType
26
+ # output :response, type: Hash
27
+ # end
28
+ # ```
29
+ class Type
30
+ extend Concerns::ErrorBuilder
31
+
32
+ # Validates that a value matches the declared attribute types.
33
+ #
34
+ # @param context [Object] Service context for error formatting
35
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute to validate
36
+ # @param value [Object] Value to check against declared types
37
+ # @param check_key [Symbol] Must be :types to trigger validation
38
+ # @return [String, nil] Error message on type mismatch, nil on success
8
39
  def self.check(context:, attribute:, value:, check_key:, **)
9
40
  return unless should_be_checked_for?(attribute, value, check_key)
10
41
 
11
- new(context:, attribute:, value:).check
42
+ prepared_value = compute_prepared_value(attribute, value)
43
+
44
+ error_data = Servactory::Maintenance::Validations::Types.validate(
45
+ context:,
46
+ attribute:,
47
+ types: attribute.types,
48
+ value: prepared_value
49
+ )
50
+
51
+ return if error_data.nil?
52
+
53
+ build_error_message(error_data)
12
54
  end
13
55
 
56
+ # Determines if type validation should run for given attribute.
57
+ #
58
+ # Type validation runs when:
59
+ # - check_key is :types AND one of:
60
+ # - Input is required
61
+ # - Input is optional with non-nil default
62
+ # - Input is optional with non-nil value
63
+ # - Attribute is internal or output (always validated)
64
+ #
65
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute to check
66
+ # @param value [Object] Current value (for optional input check)
67
+ # @param check_key [Symbol] Current validation check key
68
+ # @return [Boolean] true if type validation should run
14
69
  # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
15
70
  def self.should_be_checked_for?(attribute, value, check_key)
16
71
  check_key == :types && (
@@ -26,35 +81,30 @@ module Servactory
26
81
  )
27
82
  end
28
83
  # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
84
+ private_class_method :should_be_checked_for?
29
85
 
30
- def initialize(context:, attribute:, value:)
31
- super()
32
-
33
- @context = context
34
- @attribute = attribute
35
- @value = value
86
+ # Computes prepared value with default substitution for optional inputs.
87
+ #
88
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute
89
+ # @param value [Object] Original value
90
+ # @return [Object] Prepared value (default or original)
91
+ def self.compute_prepared_value(attribute, value)
92
+ if attribute.input? && attribute.optional? && !attribute.default.nil? && value.blank?
93
+ attribute.default
94
+ else
95
+ value
96
+ end
36
97
  end
98
+ private_class_method :compute_prepared_value
37
99
 
38
- def check
39
- Servactory::Maintenance::Validations::Types.validate!(
40
- context: @context,
41
- attribute: @attribute,
42
- types: @attribute.types,
43
- value: prepared_value,
44
- error_callback: ->(**args) { add_error(**args) }
45
- )
46
- end
47
-
48
- private
49
-
50
- def prepared_value
51
- @prepared_value ||=
52
- if @attribute.input? && @attribute.optional? && !@attribute.default.nil? && @value.blank?
53
- @attribute.default
54
- else
55
- @value
56
- end
100
+ # Builds error message from validation error data.
101
+ #
102
+ # @param error_data [Hash] Error data from Types.validate
103
+ # @return [String] Processed error message
104
+ def self.build_error_message(error_data)
105
+ process_message(error_data[:message], **error_data)
57
106
  end
107
+ private_class_method :build_error_message
58
108
  end
59
109
  end
60
110
  end
@@ -4,44 +4,29 @@ module Servactory
4
4
  module Maintenance
5
5
  module Validations
6
6
  class Types
7
- def self.validate!(...)
8
- new(...).validate!
9
- end
10
-
11
- def initialize(context:, attribute:, types:, value:, error_callback:)
12
- @context = context
13
- @attribute = attribute
14
- @types = types
15
- @value = value
16
- @error_callback = error_callback
17
- end
7
+ # Validates value against expected types.
8
+ #
9
+ # Returns nil on success, error data Hash on failure.
10
+ #
11
+ # @param context [Object] Service context for error info
12
+ # @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Attribute being validated
13
+ # @param types [Array<Class, String, Symbol>] Expected type classes
14
+ # @param value [Object] Value to validate
15
+ # @return [Hash, nil] nil on success, error data Hash on failure
16
+ def self.validate(context:, attribute:, types:, value:) # rubocop:disable Metrics/MethodLength
17
+ prepared_types = types.map { |type| Servactory::Utils.constantize_class(type) }
18
18
 
19
- def validate! # rubocop:disable Metrics/MethodLength
20
- return if prepared_types.any? do |type|
21
- @value.is_a?(type)
22
- end
19
+ return if prepared_types.any? { |type| value.is_a?(type) }
23
20
 
24
- @error_callback.call(
21
+ {
25
22
  message: Servactory::Maintenance::Attributes::Translator::Type.default_message,
26
- service: @context.send(:servactory_service_info),
27
- attribute: @attribute,
28
- value: @value,
23
+ service: context.send(:servactory_service_info),
24
+ attribute:,
25
+ value:,
29
26
  key_name: nil,
30
27
  expected_type: prepared_types.join(", "),
31
- given_type: @value.class.name
32
- )
33
- end
34
-
35
- private
36
-
37
- def prepared_types
38
- @prepared_types ||= prepared_types_from(@types)
39
- end
40
-
41
- def prepared_types_from(types)
42
- types.map do |type|
43
- Servactory::Utils.constantize_class(type)
44
- end
28
+ given_type: value.class.name
29
+ }
45
30
  end
46
31
  end
47
32
  end
@@ -12,24 +12,29 @@ module Servactory
12
12
  @name = output.name
13
13
  @types = output.types
14
14
  @options = output.options
15
+ @attribute = output
16
+ end
17
+
18
+ def system_name
19
+ @attribute.system_name
20
+ end
15
21
 
16
- define_identity_methods(output)
22
+ def i18n_name
23
+ @attribute.i18n_name
17
24
  end
18
25
 
19
- private
26
+ # The methods below are required to support the internal work.
20
27
 
21
- def define_identity_methods(output)
22
- methods_map = {
23
- system_name: -> { output.system_name },
24
- i18n_name: -> { output.i18n_name },
25
- input?: -> { false },
26
- internal?: -> { false },
27
- output?: -> { true }
28
- }
28
+ def input?
29
+ false
30
+ end
31
+
32
+ def internal?
33
+ false
34
+ end
29
35
 
30
- methods_map.each do |method_name, implementation|
31
- define_singleton_method(method_name, &implementation)
32
- end
36
+ def output?
37
+ true
33
38
  end
34
39
  end
35
40