treaty 0.0.1 → 0.1.0
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 +19 -18
- data/Rakefile +4 -2
- data/lib/treaty/attribute/base.rb +172 -0
- data/lib/treaty/attribute/builder/base.rb +142 -0
- data/lib/treaty/attribute/collection.rb +65 -0
- data/lib/treaty/attribute/helper_mapper.rb +72 -0
- data/lib/treaty/attribute/option/base.rb +159 -0
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +87 -0
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +103 -0
- data/lib/treaty/attribute/option/registry.rb +128 -0
- data/lib/treaty/attribute/option/registry_initializer.rb +90 -0
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +80 -0
- data/lib/treaty/attribute/option/validators/required_validator.rb +94 -0
- data/lib/treaty/attribute/option/validators/type_validator.rb +153 -0
- data/lib/treaty/attribute/option_normalizer.rb +150 -0
- data/lib/treaty/attribute/option_orchestrator.rb +186 -0
- data/lib/treaty/attribute/validation/attribute_validator.rb +144 -0
- data/lib/treaty/attribute/validation/base.rb +93 -0
- data/lib/treaty/attribute/validation/nested_array_validator.rb +194 -0
- data/lib/treaty/attribute/validation/nested_object_validator.rb +103 -0
- data/lib/treaty/attribute/validation/nested_transformer.rb +240 -0
- data/lib/treaty/attribute/validation/orchestrator/base.rb +196 -0
- data/lib/treaty/base.rb +9 -0
- data/lib/treaty/configuration.rb +17 -0
- data/lib/treaty/context/callable.rb +24 -0
- data/lib/treaty/context/dsl.rb +12 -0
- data/lib/treaty/context/workspace.rb +28 -0
- data/lib/treaty/controller/dsl.rb +38 -0
- data/lib/treaty/engine.rb +37 -0
- data/lib/treaty/exceptions/base.rb +8 -0
- data/lib/treaty/exceptions/class_name.rb +11 -0
- data/lib/treaty/exceptions/deprecated.rb +8 -0
- data/lib/treaty/exceptions/execution.rb +8 -0
- data/lib/treaty/exceptions/method_name.rb +8 -0
- data/lib/treaty/exceptions/nested_attributes.rb +8 -0
- data/lib/treaty/exceptions/strategy.rb +8 -0
- data/lib/treaty/exceptions/unexpected.rb +8 -0
- data/lib/treaty/exceptions/validation.rb +8 -0
- data/lib/treaty/info/builder.rb +122 -0
- data/lib/treaty/info/dsl.rb +26 -0
- data/lib/treaty/info/result.rb +13 -0
- data/lib/treaty/request/attribute/attribute.rb +24 -0
- data/lib/treaty/request/attribute/builder.rb +22 -0
- data/lib/treaty/request/attribute/validation/orchestrator.rb +27 -0
- data/lib/treaty/request/attribute/validator.rb +50 -0
- data/lib/treaty/request/factory.rb +32 -0
- data/lib/treaty/request/scope/collection.rb +21 -0
- data/lib/treaty/request/scope/factory.rb +42 -0
- data/lib/treaty/response/attribute/attribute.rb +24 -0
- data/lib/treaty/response/attribute/builder.rb +22 -0
- data/lib/treaty/response/attribute/validation/orchestrator.rb +27 -0
- data/lib/treaty/response/attribute/validator.rb +44 -0
- data/lib/treaty/response/factory.rb +38 -0
- data/lib/treaty/response/scope/collection.rb +21 -0
- data/lib/treaty/response/scope/factory.rb +42 -0
- data/lib/treaty/result.rb +22 -0
- data/lib/treaty/strategy.rb +31 -0
- data/lib/treaty/support/loader.rb +24 -0
- data/lib/treaty/version.rb +8 -1
- data/lib/treaty/versions/collection.rb +15 -0
- data/lib/treaty/versions/dsl.rb +30 -0
- data/lib/treaty/versions/execution/request.rb +151 -0
- data/lib/treaty/versions/executor.rb +14 -0
- data/lib/treaty/versions/factory.rb +93 -0
- data/lib/treaty/versions/resolver.rb +72 -0
- data/lib/treaty/versions/semantic.rb +22 -0
- data/lib/treaty/versions/workspace.rb +40 -0
- data/lib/treaty.rb +3 -3
- metadata +184 -27
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -84
- data/LICENSE.txt +0 -21
- data/sig/treaty.rbs +0 -4
- data/treaty.gemspec +0 -35
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Option
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that attribute value matches the declared type.
|
|
8
|
+
#
|
|
9
|
+
# ## Supported Types
|
|
10
|
+
#
|
|
11
|
+
# - `:integer` - Ruby Integer
|
|
12
|
+
# - `:string` - Ruby String
|
|
13
|
+
# - `:object` - Ruby Hash (for nested objects)
|
|
14
|
+
# - `:array` - Ruby Array (for collections)
|
|
15
|
+
# - `:datetime` - Ruby DateTime, Time, or Date
|
|
16
|
+
#
|
|
17
|
+
# ## Usage Examples
|
|
18
|
+
#
|
|
19
|
+
# Simple types:
|
|
20
|
+
# integer :age
|
|
21
|
+
# string :name
|
|
22
|
+
# datetime :created_at
|
|
23
|
+
#
|
|
24
|
+
# Nested structures:
|
|
25
|
+
# object :author do
|
|
26
|
+
# string :name
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# array :tags do
|
|
30
|
+
# string :_self # Simple array of strings
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# ## Validation Rules
|
|
34
|
+
#
|
|
35
|
+
# - Validates only non-nil values (nil handling is done by RequiredValidator)
|
|
36
|
+
# - Type mismatch raises Treaty::Exceptions::Validation
|
|
37
|
+
# - Datetime accepts DateTime, Time, or Date objects
|
|
38
|
+
#
|
|
39
|
+
# ## Note
|
|
40
|
+
#
|
|
41
|
+
# TypeValidator doesn't use option_schema - it validates based on attribute_type.
|
|
42
|
+
# This validator is always active for all attributes.
|
|
43
|
+
class TypeValidator < Treaty::Attribute::Option::Base
|
|
44
|
+
ALLOWED_TYPES = %i[integer string object array datetime].freeze
|
|
45
|
+
|
|
46
|
+
# Validates that the attribute type is one of the allowed types
|
|
47
|
+
#
|
|
48
|
+
# @raise [Treaty::Exceptions::Validation] If type is not allowed
|
|
49
|
+
# @return [void]
|
|
50
|
+
def validate_schema!
|
|
51
|
+
return if ALLOWED_TYPES.include?(@attribute_type)
|
|
52
|
+
|
|
53
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
54
|
+
raise Treaty::Exceptions::Validation,
|
|
55
|
+
"Unknown type '#{@attribute_type}' for attribute '#{@attribute_name}'. " \
|
|
56
|
+
"Allowed types: #{ALLOWED_TYPES.join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Validates that the value matches the declared type
|
|
60
|
+
# Skips validation for nil values (handled by RequiredValidator)
|
|
61
|
+
#
|
|
62
|
+
# @param value [Object] The value to validate
|
|
63
|
+
# @raise [Treaty::Exceptions::Validation] If value type doesn't match
|
|
64
|
+
# @return [void]
|
|
65
|
+
def validate_value!(value) # rubocop:disable Metrics/MethodLength
|
|
66
|
+
return if value.nil? # Type validation doesn't check for nil, required does.
|
|
67
|
+
|
|
68
|
+
case @attribute_type
|
|
69
|
+
when :integer
|
|
70
|
+
validate_integer!(value)
|
|
71
|
+
when :string
|
|
72
|
+
validate_string!(value)
|
|
73
|
+
when :object
|
|
74
|
+
validate_object!(value)
|
|
75
|
+
when :array
|
|
76
|
+
validate_array!(value)
|
|
77
|
+
when :datetime
|
|
78
|
+
validate_datetime!(value)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Validates that value is an Integer
|
|
85
|
+
#
|
|
86
|
+
# @param value [Object] The value to validate
|
|
87
|
+
# @raise [Treaty::Exceptions::Validation] If value is not an Integer
|
|
88
|
+
# @return [void]
|
|
89
|
+
def validate_integer!(value)
|
|
90
|
+
return if value.is_a?(Integer)
|
|
91
|
+
|
|
92
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
93
|
+
raise Treaty::Exceptions::Validation,
|
|
94
|
+
"Attribute '#{@attribute_name}' must be an Integer, got #{value.class}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validates that value is a String
|
|
98
|
+
#
|
|
99
|
+
# @param value [Object] The value to validate
|
|
100
|
+
# @raise [Treaty::Exceptions::Validation] If value is not a String
|
|
101
|
+
# @return [void]
|
|
102
|
+
def validate_string!(value)
|
|
103
|
+
return if value.is_a?(String)
|
|
104
|
+
|
|
105
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
106
|
+
raise Treaty::Exceptions::Validation,
|
|
107
|
+
"Attribute '#{@attribute_name}' must be a String, got #{value.class}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Validates that value is a Hash (object type)
|
|
111
|
+
#
|
|
112
|
+
# @param value [Object] The value to validate
|
|
113
|
+
# @raise [Treaty::Exceptions::Validation] If value is not a Hash
|
|
114
|
+
# @return [void]
|
|
115
|
+
def validate_object!(value)
|
|
116
|
+
return if value.is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
119
|
+
raise Treaty::Exceptions::Validation,
|
|
120
|
+
"Attribute '#{@attribute_name}' must be a Hash (object), got #{value.class}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Validates that value is an Array
|
|
124
|
+
#
|
|
125
|
+
# @param value [Object] The value to validate
|
|
126
|
+
# @raise [Treaty::Exceptions::Validation] If value is not an Array
|
|
127
|
+
# @return [void]
|
|
128
|
+
def validate_array!(value)
|
|
129
|
+
return if value.is_a?(Array)
|
|
130
|
+
|
|
131
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
132
|
+
raise Treaty::Exceptions::Validation,
|
|
133
|
+
"Attribute '#{@attribute_name}' must be an Array, got #{value.class}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Validates that value is a DateTime, Time, or Date
|
|
137
|
+
#
|
|
138
|
+
# @param value [Object] The value to validate
|
|
139
|
+
# @raise [Treaty::Exceptions::Validation] If value is not a datetime type
|
|
140
|
+
# @return [void]
|
|
141
|
+
def validate_datetime!(value)
|
|
142
|
+
# TODO: It is better to divide it into different methods for each class.
|
|
143
|
+
return if value.is_a?(DateTime) || value.is_a?(Time) || value.is_a?(Date)
|
|
144
|
+
|
|
145
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
146
|
+
raise Treaty::Exceptions::Validation,
|
|
147
|
+
"Attribute '#{@attribute_name}' must be a DateTime/Time/Date, got #{value.class}"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
# Normalizes options from simple mode to advanced mode.
|
|
6
|
+
#
|
|
7
|
+
# ## Purpose
|
|
8
|
+
#
|
|
9
|
+
# All options are stored and processed internally in advanced mode.
|
|
10
|
+
# This normalizer converts simple mode to advanced mode automatically.
|
|
11
|
+
#
|
|
12
|
+
# ## Modes Explained
|
|
13
|
+
#
|
|
14
|
+
# ### Simple Mode (Concise syntax)
|
|
15
|
+
# ```ruby
|
|
16
|
+
# {
|
|
17
|
+
# required: true,
|
|
18
|
+
# as: :value,
|
|
19
|
+
# in: %w[twitter linkedin github],
|
|
20
|
+
# default: 12
|
|
21
|
+
# }
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ### Advanced Mode (With messages)
|
|
25
|
+
# ```ruby
|
|
26
|
+
# {
|
|
27
|
+
# required: { is: true, message: nil },
|
|
28
|
+
# as: { is: :value, message: nil },
|
|
29
|
+
# inclusion: { in: %w[twitter linkedin github], message: nil },
|
|
30
|
+
# default: { is: 12, message: nil }
|
|
31
|
+
# }
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
# ## Key Mappings
|
|
35
|
+
#
|
|
36
|
+
# Some simple mode keys are renamed in advanced mode:
|
|
37
|
+
# - `in:` → `inclusion:` (with value key `:in`)
|
|
38
|
+
#
|
|
39
|
+
# Others keep the same name:
|
|
40
|
+
# - `required:` → `required:` (with value key `:is`)
|
|
41
|
+
# - `as:` → `as:` (with value key `:is`)
|
|
42
|
+
# - `default:` → `default:` (with value key `:is`)
|
|
43
|
+
#
|
|
44
|
+
# ## Value Keys
|
|
45
|
+
#
|
|
46
|
+
# Each option has a value key in advanced mode:
|
|
47
|
+
# - Default: `:is` (most options)
|
|
48
|
+
# - Special: `:in` (inclusion validator)
|
|
49
|
+
#
|
|
50
|
+
# ## Message Field
|
|
51
|
+
#
|
|
52
|
+
# The `message` field in advanced mode allows custom error messages:
|
|
53
|
+
# - `nil` - Use default message (most common)
|
|
54
|
+
# - String - Custom error message for validation failures
|
|
55
|
+
#
|
|
56
|
+
# ## Usage in DSL
|
|
57
|
+
#
|
|
58
|
+
# Users can write in either mode:
|
|
59
|
+
#
|
|
60
|
+
# Simple mode:
|
|
61
|
+
# string :provider, in: %w[twitter linkedin]
|
|
62
|
+
#
|
|
63
|
+
# Advanced mode:
|
|
64
|
+
# string :provider, inclusion: { in: %w[twitter linkedin], message: "Invalid provider" }
|
|
65
|
+
#
|
|
66
|
+
# Both are normalized to advanced mode internally.
|
|
67
|
+
class OptionNormalizer
|
|
68
|
+
# Maps simple mode option keys to their advanced mode configuration.
|
|
69
|
+
# Format: simple_key => { advanced_key:, value_key: }
|
|
70
|
+
OPTION_KEY_MAPPING = {
|
|
71
|
+
in: { advanced_key: :inclusion, value_key: :in },
|
|
72
|
+
as: { advanced_key: :as, value_key: :is },
|
|
73
|
+
default: { advanced_key: :default, value_key: :is }
|
|
74
|
+
}.freeze
|
|
75
|
+
private_constant :OPTION_KEY_MAPPING
|
|
76
|
+
|
|
77
|
+
# Reverse mapping: advanced_key => value_key
|
|
78
|
+
# Used to determine value key when option is already in advanced mode.
|
|
79
|
+
ADVANCED_KEY_TO_VALUE_KEY = OPTION_KEY_MAPPING.each_with_object({}) do |(_, config), result|
|
|
80
|
+
result[config.fetch(:advanced_key)] = config.fetch(:value_key)
|
|
81
|
+
end.freeze
|
|
82
|
+
private_constant :ADVANCED_KEY_TO_VALUE_KEY
|
|
83
|
+
|
|
84
|
+
DEFAULT_VALUE_KEY = :is
|
|
85
|
+
private_constant :DEFAULT_VALUE_KEY
|
|
86
|
+
|
|
87
|
+
class << self
|
|
88
|
+
# Normalizes all options from simple mode to advanced mode
|
|
89
|
+
#
|
|
90
|
+
# @param options [Hash] Options hash in simple or advanced mode
|
|
91
|
+
# @return [Hash] Normalized options in advanced mode
|
|
92
|
+
def normalize(options)
|
|
93
|
+
options.each_with_object({}) do |(key, value), result|
|
|
94
|
+
advanced_key, normalized_value = normalize_option(key, value)
|
|
95
|
+
result[advanced_key] = normalized_value
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Normalizes a single option to advanced mode
|
|
102
|
+
#
|
|
103
|
+
# @param key [Symbol] Option key
|
|
104
|
+
# @param value [Object] Option value
|
|
105
|
+
# @return [Array<Symbol, Hash>] Tuple of [advanced_key, normalized_value]
|
|
106
|
+
def normalize_option(key, value) # rubocop:disable Metrics/MethodLength
|
|
107
|
+
mapping = OPTION_KEY_MAPPING.fetch(key, nil)
|
|
108
|
+
|
|
109
|
+
if mapping.present?
|
|
110
|
+
# Special handling for mapped options (e.g., in -> inclusion).
|
|
111
|
+
advanced_key = mapping.fetch(:advanced_key)
|
|
112
|
+
value_key = mapping.fetch(:value_key)
|
|
113
|
+
normalized_value = normalize_value(value, value_key)
|
|
114
|
+
[advanced_key, normalized_value]
|
|
115
|
+
else
|
|
116
|
+
# Check if this key is already an advanced mode key.
|
|
117
|
+
value_key = ADVANCED_KEY_TO_VALUE_KEY.fetch(key, nil) || DEFAULT_VALUE_KEY
|
|
118
|
+
normalized_value = normalize_value(value, value_key)
|
|
119
|
+
[key, normalized_value]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Normalizes option value to advanced mode format
|
|
124
|
+
#
|
|
125
|
+
# @param value [Object] The option value (simple or advanced mode)
|
|
126
|
+
# @param value_key [Symbol] The key to use for the value (:is or :in)
|
|
127
|
+
# @return [Hash] Normalized hash with value_key and :message
|
|
128
|
+
def normalize_value(value, value_key)
|
|
129
|
+
if advanced_mode?(value, value_key)
|
|
130
|
+
# Already in advanced mode, ensure it has both keys.
|
|
131
|
+
{ value_key => value.fetch(value_key), message: value.fetch(:message, nil) }
|
|
132
|
+
else
|
|
133
|
+
# Simple mode, convert to advanced.
|
|
134
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
135
|
+
{ value_key => value, message: nil }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Checks if value is already in advanced mode
|
|
140
|
+
#
|
|
141
|
+
# @param value [Object] The value to check
|
|
142
|
+
# @param value_key [Symbol] The expected value key
|
|
143
|
+
# @return [Boolean] True if value is a hash with the value key
|
|
144
|
+
def advanced_mode?(value, value_key)
|
|
145
|
+
value.is_a?(Hash) && value.key?(value_key)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
# Orchestrates all option processors for a single attribute.
|
|
6
|
+
#
|
|
7
|
+
# ## Purpose
|
|
8
|
+
#
|
|
9
|
+
# Coordinates the execution of all option processors (validators and modifiers)
|
|
10
|
+
# for an attribute through three distinct processing phases.
|
|
11
|
+
#
|
|
12
|
+
# ## Responsibilities
|
|
13
|
+
#
|
|
14
|
+
# 1. **Processor Building** - Creates instances of all relevant option processors
|
|
15
|
+
# 2. **Schema Validation** - Validates DSL definition correctness (phase 1)
|
|
16
|
+
# 3. **Value Validation** - Validates runtime data values (phase 2)
|
|
17
|
+
# 4. **Value Transformation** - Transforms values through modifiers (phase 3)
|
|
18
|
+
# 5. **Name Transformation** - Provides target name if `as:` option is used
|
|
19
|
+
#
|
|
20
|
+
# ## Processing Phases
|
|
21
|
+
#
|
|
22
|
+
# ### Phase 1: Schema Validation
|
|
23
|
+
# Validates that the attribute definition in the DSL is correct.
|
|
24
|
+
# Called once during treaty definition loading.
|
|
25
|
+
#
|
|
26
|
+
# ```ruby
|
|
27
|
+
# orchestrator.validate_schema!
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# ### Phase 2: Value Validation
|
|
31
|
+
# Validates that runtime data matches the constraints.
|
|
32
|
+
# Called for each request/response.
|
|
33
|
+
#
|
|
34
|
+
# ```ruby
|
|
35
|
+
# orchestrator.validate_value!(value)
|
|
36
|
+
# ```
|
|
37
|
+
#
|
|
38
|
+
# ### Phase 3: Value Transformation
|
|
39
|
+
# Transforms the value (applies defaults, renaming, etc.).
|
|
40
|
+
# Called for each request/response after validation.
|
|
41
|
+
#
|
|
42
|
+
# ```ruby
|
|
43
|
+
# transformed = orchestrator.transform_value(value)
|
|
44
|
+
# ```
|
|
45
|
+
#
|
|
46
|
+
# ## Usage
|
|
47
|
+
#
|
|
48
|
+
# Used by AttributeValidator to coordinate all option processing:
|
|
49
|
+
#
|
|
50
|
+
# orchestrator = OptionOrchestrator.new(attribute)
|
|
51
|
+
# orchestrator.validate_schema!
|
|
52
|
+
# orchestrator.validate_value!(value)
|
|
53
|
+
# transformed = orchestrator.transform_value(value)
|
|
54
|
+
# target_name = orchestrator.target_name
|
|
55
|
+
#
|
|
56
|
+
# ## Processor Building
|
|
57
|
+
#
|
|
58
|
+
# Automatically:
|
|
59
|
+
# - Builds processor instances for all defined options
|
|
60
|
+
# - Always includes TypeValidator (even if not explicitly defined)
|
|
61
|
+
# - Validates that all options are registered in Registry
|
|
62
|
+
# - Raises error for unknown options
|
|
63
|
+
#
|
|
64
|
+
# ## Architecture
|
|
65
|
+
#
|
|
66
|
+
# Works with:
|
|
67
|
+
# - Option::Registry - Looks up processor classes
|
|
68
|
+
# - Option::Base - Base class for all processors
|
|
69
|
+
# - AttributeValidator - Uses orchestrator to coordinate processing
|
|
70
|
+
class OptionOrchestrator
|
|
71
|
+
# Creates a new orchestrator instance
|
|
72
|
+
#
|
|
73
|
+
# @param attribute [Attribute::Base] The attribute to orchestrate options for
|
|
74
|
+
def initialize(attribute)
|
|
75
|
+
@attribute = attribute
|
|
76
|
+
@processors = build_processors
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Phase 1: Validates all option schemas
|
|
80
|
+
# Ensures DSL definition is correct and all options are registered
|
|
81
|
+
#
|
|
82
|
+
# @raise [Treaty::Exceptions::Validation] If unknown options found
|
|
83
|
+
# @return [void]
|
|
84
|
+
def validate_schema!
|
|
85
|
+
validate_known_options!
|
|
86
|
+
|
|
87
|
+
@processors.each_value(&:validate_schema!)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Phase 2: Validates value against all option validators
|
|
91
|
+
# Validates runtime data against all defined constraints
|
|
92
|
+
#
|
|
93
|
+
# @param value [Object] The value to validate
|
|
94
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
95
|
+
# @return [void]
|
|
96
|
+
def validate_value!(value)
|
|
97
|
+
@processors.each_value do |processor|
|
|
98
|
+
processor.validate_value!(value)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Phase 3: Transforms value through all option modifiers
|
|
103
|
+
# Applies transformations like defaults, type coercion, etc.
|
|
104
|
+
#
|
|
105
|
+
# @param value [Object] The value to transform
|
|
106
|
+
# @return [Object] Transformed value
|
|
107
|
+
def transform_value(value)
|
|
108
|
+
@processors.values.reduce(value) do |current_value, processor|
|
|
109
|
+
processor.transform_value(current_value)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Checks if any processor transforms the attribute name
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean] True if any processor (like AsModifier) transforms names
|
|
116
|
+
def transforms_name?
|
|
117
|
+
@processors.values.any?(&:transforms_name?)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Gets the target name from the processor that transforms names
|
|
121
|
+
# Returns original name if no transformation
|
|
122
|
+
#
|
|
123
|
+
# @return [Symbol] The target attribute name
|
|
124
|
+
def target_name
|
|
125
|
+
name_transformer = @processors.values.find(&:transforms_name?)
|
|
126
|
+
name_transformer ? name_transformer.target_name : @attribute.name
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Gets specific processor by option name
|
|
130
|
+
#
|
|
131
|
+
# @param option_name [Symbol] The option name (:required, :type, etc.)
|
|
132
|
+
# @return [Option::Base, nil] The processor instance or nil if not found
|
|
133
|
+
def processor_for(option_name)
|
|
134
|
+
@processors[option_name]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
# Builds processor instances for all defined options
|
|
140
|
+
# Always includes TypeValidator even if not explicitly defined
|
|
141
|
+
#
|
|
142
|
+
# @return [Hash<Symbol, Option::Base>] Hash of option_name => processor
|
|
143
|
+
def build_processors # rubocop:disable Metrics/MethodLength
|
|
144
|
+
processors_hash = {}
|
|
145
|
+
|
|
146
|
+
@attribute.options.each do |option_name, option_schema|
|
|
147
|
+
processor_class = Option::Registry.processor_for(option_name)
|
|
148
|
+
|
|
149
|
+
next unless processor_class
|
|
150
|
+
|
|
151
|
+
processors_hash[option_name] = processor_class.new(
|
|
152
|
+
attribute_name: @attribute.name,
|
|
153
|
+
attribute_type: @attribute.type,
|
|
154
|
+
option_schema:
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Always include type validator
|
|
159
|
+
unless processors_hash.key?(:type)
|
|
160
|
+
processors_hash[:type] = Option::Validators::TypeValidator.new(
|
|
161
|
+
attribute_name: @attribute.name,
|
|
162
|
+
attribute_type: @attribute.type,
|
|
163
|
+
option_schema: nil
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
processors_hash
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Validates that all options are registered in the Registry
|
|
171
|
+
#
|
|
172
|
+
# @raise [Treaty::Exceptions::Validation] If unknown options found
|
|
173
|
+
# @return [void]
|
|
174
|
+
def validate_known_options!
|
|
175
|
+
unknown_options = @attribute.options.keys - Option::Registry.all_options
|
|
176
|
+
|
|
177
|
+
return if unknown_options.empty?
|
|
178
|
+
|
|
179
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
180
|
+
raise Treaty::Exceptions::Validation,
|
|
181
|
+
"Unknown options for attribute '#{@attribute.name}': #{unknown_options.join(', ')}. " \
|
|
182
|
+
"Known options: #{Option::Registry.all_options.join(', ')}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Validation
|
|
6
|
+
# Validates and transforms individual attributes.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Acts as the main interface for attribute validation and transformation.
|
|
11
|
+
# Delegates option processing to OptionOrchestrator and handles nested validation.
|
|
12
|
+
#
|
|
13
|
+
# ## Responsibilities
|
|
14
|
+
#
|
|
15
|
+
# 1. **Schema Validation** - Validates DSL definition correctness
|
|
16
|
+
# 2. **Value Validation** - Validates runtime data values
|
|
17
|
+
# 3. **Value Transformation** - Transforms values (defaults, etc.)
|
|
18
|
+
# 4. **Name Transformation** - Provides target name (for `as:` option)
|
|
19
|
+
# 5. **Nested Validation** - Delegates to NestedObjectValidator/NestedArrayValidator
|
|
20
|
+
#
|
|
21
|
+
# ## Usage
|
|
22
|
+
#
|
|
23
|
+
# Used by Orchestrator to validate each attribute:
|
|
24
|
+
#
|
|
25
|
+
# validator = AttributeValidator.new(attribute)
|
|
26
|
+
# validator.validate_schema!
|
|
27
|
+
# validator.validate_value!(value)
|
|
28
|
+
# transformed = validator.transform_value(value)
|
|
29
|
+
# target_name = validator.target_name
|
|
30
|
+
#
|
|
31
|
+
# ## Architecture
|
|
32
|
+
#
|
|
33
|
+
# Delegates to:
|
|
34
|
+
# - `OptionOrchestrator` - Coordinates all option processors
|
|
35
|
+
# - `NestedObjectValidator` - Validates nested object structures
|
|
36
|
+
# - `NestedArrayValidator` - Validates nested array structures
|
|
37
|
+
class AttributeValidator
|
|
38
|
+
attr_reader :attribute, :option_orchestrator
|
|
39
|
+
|
|
40
|
+
# Creates a new attribute validator instance
|
|
41
|
+
#
|
|
42
|
+
# @param attribute [Attribute::Base] The attribute to validate
|
|
43
|
+
def initialize(attribute)
|
|
44
|
+
@attribute = attribute
|
|
45
|
+
@option_orchestrator = OptionOrchestrator.new(attribute)
|
|
46
|
+
@nested_object_validator = nil
|
|
47
|
+
@nested_array_validator = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Validates the attribute schema (DSL definition)
|
|
51
|
+
#
|
|
52
|
+
# @raise [Treaty::Exceptions::Validation] If schema is invalid
|
|
53
|
+
# @return [void]
|
|
54
|
+
def validate_schema!
|
|
55
|
+
option_orchestrator.validate_schema!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validates attribute value against all constraints
|
|
59
|
+
#
|
|
60
|
+
# @param value [Object] The value to validate
|
|
61
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
62
|
+
# @return [void]
|
|
63
|
+
def validate_value!(value)
|
|
64
|
+
option_orchestrator.validate_value!(value)
|
|
65
|
+
validate_nested!(value) if attribute.nested? && !value.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Transforms attribute value through all modifiers
|
|
69
|
+
#
|
|
70
|
+
# @param value [Object] The value to transform
|
|
71
|
+
# @return [Object] Transformed value
|
|
72
|
+
def transform_value(value)
|
|
73
|
+
option_orchestrator.transform_value(value)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Checks if attribute name is transformed
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean] True if name is transformed (as: option)
|
|
79
|
+
def transforms_name?
|
|
80
|
+
option_orchestrator.transforms_name?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Gets the target attribute name
|
|
84
|
+
#
|
|
85
|
+
# @return [Symbol] The target name (or original if not transformed)
|
|
86
|
+
def target_name
|
|
87
|
+
option_orchestrator.target_name
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Validates only the type constraint
|
|
91
|
+
# Used by nested transformers to validate types before nested validation
|
|
92
|
+
#
|
|
93
|
+
# @param value [Object] The value to validate
|
|
94
|
+
# @raise [Treaty::Exceptions::Validation] If type validation fails
|
|
95
|
+
# @return [void]
|
|
96
|
+
def validate_type!(value)
|
|
97
|
+
type_processor = option_orchestrator.processor_for(:type)
|
|
98
|
+
type_processor&.validate_value!(value)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Validates only the required constraint
|
|
102
|
+
# Used by nested transformers to validate presence before nested validation
|
|
103
|
+
#
|
|
104
|
+
# @param value [Object] The value to validate
|
|
105
|
+
# @raise [Treaty::Exceptions::Validation] If required validation fails
|
|
106
|
+
# @return [void]
|
|
107
|
+
def validate_required!(value)
|
|
108
|
+
required_processor = option_orchestrator.processor_for(:required)
|
|
109
|
+
required_processor&.validate_value!(value) if attribute.options.key?(:required)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Validates nested attributes for object/array types
|
|
115
|
+
#
|
|
116
|
+
# @param value [Object] The value to validate
|
|
117
|
+
# @raise [Treaty::Exceptions::Validation] If nested validation fails
|
|
118
|
+
# @return [void]
|
|
119
|
+
def validate_nested!(value)
|
|
120
|
+
case attribute.type
|
|
121
|
+
when :object
|
|
122
|
+
nested_object_validator.validate!(value)
|
|
123
|
+
when :array
|
|
124
|
+
nested_array_validator.validate!(value)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Gets or creates nested object validator
|
|
129
|
+
#
|
|
130
|
+
# @return [NestedObjectValidator] Validator for nested objects
|
|
131
|
+
def nested_object_validator
|
|
132
|
+
@nested_object_validator ||= NestedObjectValidator.new(attribute)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Gets or creates nested array validator
|
|
136
|
+
#
|
|
137
|
+
# @return [NestedArrayValidator] Validator for nested arrays
|
|
138
|
+
def nested_array_validator
|
|
139
|
+
@nested_array_validator ||= NestedArrayValidator.new(attribute)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|