treaty 0.18.0 → 0.19.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 +1 -1
- data/config/locales/en.yml +3 -3
- data/lib/treaty/engine.rb +1 -1
- data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
- data/lib/treaty/entity/attribute/base.rb +184 -0
- data/lib/treaty/entity/attribute/builder/base.rb +275 -0
- data/lib/treaty/entity/attribute/collection.rb +67 -0
- data/lib/treaty/entity/attribute/dsl.rb +92 -0
- data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
- data/lib/treaty/entity/attribute/option/base.rb +190 -0
- data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
- data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
- data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
- data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
- data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
- data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
- data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
- data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
- data/lib/treaty/entity/attribute/option/registry.rb +157 -0
- data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
- data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
- data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
- data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
- data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
- data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
- data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
- data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
- data/lib/treaty/entity/attribute/validation/base.rb +76 -0
- data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
- data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
- data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
- data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
- data/lib/treaty/entity/base.rb +90 -0
- data/lib/treaty/entity/builder.rb +44 -0
- data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
- data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
- data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
- data/lib/treaty/entity.rb +7 -79
- data/lib/treaty/request/attribute/attribute.rb +1 -1
- data/lib/treaty/request/attribute/builder.rb +2 -2
- data/lib/treaty/request/entity.rb +1 -1
- data/lib/treaty/request/factory.rb +5 -5
- data/lib/treaty/request/validator.rb +1 -1
- data/lib/treaty/response/attribute/attribute.rb +1 -1
- data/lib/treaty/response/attribute/builder.rb +2 -2
- data/lib/treaty/response/entity.rb +1 -1
- data/lib/treaty/response/factory.rb +5 -5
- data/lib/treaty/response/validator.rb +1 -1
- data/lib/treaty/version.rb +1 -1
- metadata +35 -34
- data/lib/treaty/attribute/base.rb +0 -182
- data/lib/treaty/attribute/builder/base.rb +0 -273
- data/lib/treaty/attribute/collection.rb +0 -65
- data/lib/treaty/attribute/dsl.rb +0 -90
- data/lib/treaty/attribute/entity/builder.rb +0 -46
- data/lib/treaty/attribute/helper_mapper.rb +0 -72
- data/lib/treaty/attribute/option/base.rb +0 -188
- data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
- data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
- data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
- data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
- data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
- data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
- data/lib/treaty/attribute/option/registry.rb +0 -155
- data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
- data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
- data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
- data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
- data/lib/treaty/attribute/option_normalizer.rb +0 -166
- data/lib/treaty/attribute/option_orchestrator.rb +0 -190
- data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
- data/lib/treaty/attribute/validation/base.rb +0 -74
- data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
- data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
- data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
- data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Entity
|
|
5
|
+
module Attribute
|
|
6
|
+
# Normalizes options from simple mode to advanced mode.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# All options are stored and processed internally in advanced mode.
|
|
11
|
+
# This normalizer converts simple mode to advanced mode automatically.
|
|
12
|
+
#
|
|
13
|
+
# ## Modes Explained
|
|
14
|
+
#
|
|
15
|
+
# ### Simple Mode (Concise syntax)
|
|
16
|
+
# ```ruby
|
|
17
|
+
# {
|
|
18
|
+
# required: true,
|
|
19
|
+
# as: :value,
|
|
20
|
+
# in: %w[twitter linkedin github],
|
|
21
|
+
# default: 12
|
|
22
|
+
# }
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# ### Advanced Mode (With messages)
|
|
26
|
+
# ```ruby
|
|
27
|
+
# {
|
|
28
|
+
# required: { is: true, message: nil },
|
|
29
|
+
# as: { is: :value, message: nil },
|
|
30
|
+
# inclusion: { in: %w[twitter linkedin github], message: nil },
|
|
31
|
+
# default: { is: 12, message: nil }
|
|
32
|
+
# }
|
|
33
|
+
# ```
|
|
34
|
+
#
|
|
35
|
+
# ## Key Mappings
|
|
36
|
+
#
|
|
37
|
+
# Some simple mode keys are renamed in advanced mode:
|
|
38
|
+
# - `in:` → `inclusion:` (with value key `:in`)
|
|
39
|
+
#
|
|
40
|
+
# Others keep the same name:
|
|
41
|
+
# - `required:` → `required:` (with value key `:is`)
|
|
42
|
+
# - `as:` → `as:` (with value key `:is`)
|
|
43
|
+
# - `default:` → `default:` (with value key `:is`)
|
|
44
|
+
#
|
|
45
|
+
# ## Value Keys
|
|
46
|
+
#
|
|
47
|
+
# Each option has a value key in advanced mode:
|
|
48
|
+
# - Default: `:is` (most options)
|
|
49
|
+
# - Special: `:in` (inclusion validator)
|
|
50
|
+
#
|
|
51
|
+
# ## Message Field
|
|
52
|
+
#
|
|
53
|
+
# The `message` field in advanced mode allows custom error messages:
|
|
54
|
+
# - `nil` - Use default message (most common)
|
|
55
|
+
# - String - Custom error message for validation failures
|
|
56
|
+
#
|
|
57
|
+
# ## Usage in DSL
|
|
58
|
+
#
|
|
59
|
+
# Users can write in either mode:
|
|
60
|
+
#
|
|
61
|
+
# Simple mode:
|
|
62
|
+
# string :provider, in: %w[twitter linkedin]
|
|
63
|
+
#
|
|
64
|
+
# Advanced mode:
|
|
65
|
+
# string :provider, inclusion: { in: %w[twitter linkedin], message: "Invalid provider" }
|
|
66
|
+
#
|
|
67
|
+
# Both are normalized to advanced mode internally.
|
|
68
|
+
class OptionNormalizer
|
|
69
|
+
# Maps simple mode option keys to their advanced mode configuration.
|
|
70
|
+
# Format: simple_key => { advanced_key:, value_key: }
|
|
71
|
+
OPTION_KEY_MAPPING = {
|
|
72
|
+
in: { advanced_key: :inclusion, value_key: :in },
|
|
73
|
+
as: { advanced_key: :as, value_key: :is },
|
|
74
|
+
default: { advanced_key: :default, value_key: :is },
|
|
75
|
+
cast: { advanced_key: :cast, value_key: :to }
|
|
76
|
+
}.freeze
|
|
77
|
+
private_constant :OPTION_KEY_MAPPING
|
|
78
|
+
|
|
79
|
+
# Reverse mapping: advanced_key => value_key
|
|
80
|
+
# Used to determine value key when option is already in advanced mode.
|
|
81
|
+
ADVANCED_KEY_TO_VALUE_KEY = OPTION_KEY_MAPPING.each_with_object({}) do |(_, config), result|
|
|
82
|
+
result[config.fetch(:advanced_key)] = config.fetch(:value_key)
|
|
83
|
+
end.freeze
|
|
84
|
+
private_constant :ADVANCED_KEY_TO_VALUE_KEY
|
|
85
|
+
|
|
86
|
+
DEFAULT_VALUE_KEY = :is
|
|
87
|
+
private_constant :DEFAULT_VALUE_KEY
|
|
88
|
+
|
|
89
|
+
class << self
|
|
90
|
+
# Normalizes all options from simple mode to advanced mode
|
|
91
|
+
# and sorts them by position for consistent execution order.
|
|
92
|
+
#
|
|
93
|
+
# @param options [Hash] Options hash in simple or advanced mode
|
|
94
|
+
# @return [Hash] Normalized options in advanced mode, sorted by position
|
|
95
|
+
def normalize(options)
|
|
96
|
+
normalized = options.each_with_object({}) do |(key, value), result|
|
|
97
|
+
advanced_key, normalized_value = normalize_option(key, value)
|
|
98
|
+
result[advanced_key] = normalized_value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sort_by_position(normalized)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Sorts options by their registered position.
|
|
107
|
+
# Options without position (like conditionals) sort first (position 0).
|
|
108
|
+
#
|
|
109
|
+
# @param options_hash [Hash] Normalized options hash
|
|
110
|
+
# @return [Hash] Options sorted by position
|
|
111
|
+
def sort_by_position(options_hash)
|
|
112
|
+
options_hash.sort_by do |option_name, _|
|
|
113
|
+
Option::Registry.position_for(option_name) || 0
|
|
114
|
+
end.to_h
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Normalizes a single option to advanced mode
|
|
118
|
+
#
|
|
119
|
+
# @param key [Symbol] Option key
|
|
120
|
+
# @param value [Object] Option value
|
|
121
|
+
# @return [Array<Symbol, Hash>] Tuple of [advanced_key, normalized_value]
|
|
122
|
+
def normalize_option(key, value) # rubocop:disable Metrics/MethodLength
|
|
123
|
+
mapping = OPTION_KEY_MAPPING.fetch(key, nil)
|
|
124
|
+
|
|
125
|
+
if mapping.present?
|
|
126
|
+
# Special handling for mapped options (e.g., in -> inclusion).
|
|
127
|
+
advanced_key = mapping.fetch(:advanced_key)
|
|
128
|
+
value_key = mapping.fetch(:value_key)
|
|
129
|
+
normalized_value = normalize_value(value, value_key)
|
|
130
|
+
[advanced_key, normalized_value]
|
|
131
|
+
else
|
|
132
|
+
# Check if this key is already an advanced mode key.
|
|
133
|
+
value_key = ADVANCED_KEY_TO_VALUE_KEY.fetch(key, nil) || DEFAULT_VALUE_KEY
|
|
134
|
+
normalized_value = normalize_value(value, value_key)
|
|
135
|
+
[key, normalized_value]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Normalizes option value to advanced mode format
|
|
140
|
+
#
|
|
141
|
+
# @param value [Object] The option value (simple or advanced mode)
|
|
142
|
+
# @param value_key [Symbol] The key to use for the value (:is or :in)
|
|
143
|
+
# @return [Hash] Normalized hash with value_key and :message
|
|
144
|
+
def normalize_value(value, value_key)
|
|
145
|
+
if advanced_mode?(value, value_key)
|
|
146
|
+
# Already in advanced mode, ensure it has both keys.
|
|
147
|
+
# message: nil means use I18n default message from validators
|
|
148
|
+
{ value_key => value.fetch(value_key), message: value.fetch(:message, nil) }
|
|
149
|
+
else
|
|
150
|
+
# Simple mode, convert to advanced.
|
|
151
|
+
# message: nil means use I18n default message from validators
|
|
152
|
+
{ value_key => value, message: nil }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Checks if value is already in advanced mode
|
|
157
|
+
#
|
|
158
|
+
# @param value [Object] The value to check
|
|
159
|
+
# @param value_key [Symbol] The expected value key
|
|
160
|
+
# @return [Boolean] True if value is a hash with the value key
|
|
161
|
+
def advanced_mode?(value, value_key)
|
|
162
|
+
value.is_a?(Hash) && value.key?(value_key)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Entity
|
|
5
|
+
module Attribute
|
|
6
|
+
# Orchestrates all option processors for a single attribute.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Coordinates the execution of all option processors (validators and modifiers)
|
|
11
|
+
# for an attribute through three distinct processing phases.
|
|
12
|
+
#
|
|
13
|
+
# ## Responsibilities
|
|
14
|
+
#
|
|
15
|
+
# 1. **Processor Building** - Creates instances of all relevant option processors
|
|
16
|
+
# 2. **Schema Validation** - Validates DSL definition correctness (phase 1)
|
|
17
|
+
# 3. **Value Validation** - Validates runtime data values (phase 2)
|
|
18
|
+
# 4. **Value Transformation** - Transforms values through modifiers (phase 3)
|
|
19
|
+
# 5. **Name Transformation** - Provides target name if `as:` option is used
|
|
20
|
+
#
|
|
21
|
+
# ## Processing Phases
|
|
22
|
+
#
|
|
23
|
+
# ### Phase 1: Schema Validation
|
|
24
|
+
# Validates that the attribute definition in the DSL is correct.
|
|
25
|
+
# Called once during treaty definition loading.
|
|
26
|
+
#
|
|
27
|
+
# ```ruby
|
|
28
|
+
# orchestrator.validate_schema!
|
|
29
|
+
# ```
|
|
30
|
+
#
|
|
31
|
+
# ### Phase 2: Value Validation
|
|
32
|
+
# Validates that runtime data matches the constraints.
|
|
33
|
+
# Called for each request/response.
|
|
34
|
+
#
|
|
35
|
+
# ```ruby
|
|
36
|
+
# orchestrator.validate_value!(value)
|
|
37
|
+
# ```
|
|
38
|
+
#
|
|
39
|
+
# ### Phase 3: Value Transformation
|
|
40
|
+
# Transforms the value (applies defaults, renaming, etc.).
|
|
41
|
+
# Called for each request/response after validation.
|
|
42
|
+
#
|
|
43
|
+
# ```ruby
|
|
44
|
+
# transformed = orchestrator.transform_value(value)
|
|
45
|
+
# ```
|
|
46
|
+
#
|
|
47
|
+
# ## Usage
|
|
48
|
+
#
|
|
49
|
+
# Used by AttributeValidator to coordinate all option processing:
|
|
50
|
+
#
|
|
51
|
+
# orchestrator = OptionOrchestrator.new(attribute)
|
|
52
|
+
# orchestrator.validate_schema!
|
|
53
|
+
# orchestrator.validate_value!(value)
|
|
54
|
+
# transformed = orchestrator.transform_value(value)
|
|
55
|
+
# target_name = orchestrator.target_name
|
|
56
|
+
#
|
|
57
|
+
# ## Processor Building
|
|
58
|
+
#
|
|
59
|
+
# Automatically:
|
|
60
|
+
# - Builds processor instances for all defined options
|
|
61
|
+
# - Always includes TypeValidator (even if not explicitly defined)
|
|
62
|
+
# - Validates that all options are registered in Registry
|
|
63
|
+
# - Raises error for unknown options
|
|
64
|
+
#
|
|
65
|
+
# ## Architecture
|
|
66
|
+
#
|
|
67
|
+
# Works with:
|
|
68
|
+
# - Option::Registry - Looks up processor classes
|
|
69
|
+
# - Option::Base - Base class for all processors
|
|
70
|
+
# - AttributeValidator - Uses orchestrator to coordinate processing
|
|
71
|
+
class OptionOrchestrator
|
|
72
|
+
# Creates a new orchestrator instance
|
|
73
|
+
#
|
|
74
|
+
# @param attribute [Attribute::Base] The attribute to orchestrate options for
|
|
75
|
+
def initialize(attribute)
|
|
76
|
+
@attribute = attribute
|
|
77
|
+
@processors = build_processors
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Phase 1: Validates all option schemas
|
|
81
|
+
# Ensures DSL definition is correct and all options are registered
|
|
82
|
+
#
|
|
83
|
+
# @raise [Treaty::Exceptions::Validation] If unknown options found
|
|
84
|
+
# @return [void]
|
|
85
|
+
def validate_schema!
|
|
86
|
+
validate_known_options!
|
|
87
|
+
|
|
88
|
+
@processors.each_value(&:validate_schema!)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Phase 2: Validates value against all option validators
|
|
92
|
+
# Validates runtime data against all defined constraints
|
|
93
|
+
#
|
|
94
|
+
# @param value [Object] The value to validate
|
|
95
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
96
|
+
# @return [void]
|
|
97
|
+
def validate_value!(value)
|
|
98
|
+
@processors.each_value do |processor|
|
|
99
|
+
processor.validate_value!(value)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Phase 3: Transforms value through all option modifiers
|
|
104
|
+
# Applies transformations like defaults, type coercion, etc.
|
|
105
|
+
#
|
|
106
|
+
# @param value [Object] The value to transform
|
|
107
|
+
# @param root_data [Hash] Full raw data from root level (used by computed modifier)
|
|
108
|
+
# @return [Object] Transformed value
|
|
109
|
+
def transform_value(value, root_data = {})
|
|
110
|
+
@processors.values.reduce(value) do |current_value, processor|
|
|
111
|
+
processor.transform_value(current_value, root_data)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Checks if any processor transforms the attribute name
|
|
116
|
+
#
|
|
117
|
+
# @return [Boolean] True if any processor (like AsModifier) transforms names
|
|
118
|
+
def transforms_name?
|
|
119
|
+
@processors.values.any?(&:transforms_name?)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Gets the target name from the processor that transforms names
|
|
123
|
+
# Returns original name if no transformation
|
|
124
|
+
#
|
|
125
|
+
# @return [Symbol] The target attribute name
|
|
126
|
+
def target_name
|
|
127
|
+
name_transformer = @processors.values.find(&:transforms_name?)
|
|
128
|
+
name_transformer ? name_transformer.target_name : @attribute.name
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Gets specific processor by option name
|
|
132
|
+
#
|
|
133
|
+
# @param option_name [Symbol] The option name (:required, :type, etc.)
|
|
134
|
+
# @return [Option::Base, nil] The processor instance or nil if not found
|
|
135
|
+
def processor_for(option_name)
|
|
136
|
+
@processors.fetch(option_name)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
# Builds processor instances for all defined options
|
|
142
|
+
# Always includes TypeValidator even if not explicitly defined
|
|
143
|
+
#
|
|
144
|
+
# @return [Hash<Symbol, Option::Base>] Hash of option_name => processor
|
|
145
|
+
def build_processors # rubocop:disable Metrics/MethodLength
|
|
146
|
+
processors_hash = {}
|
|
147
|
+
|
|
148
|
+
@attribute.options.each do |option_name, option_schema|
|
|
149
|
+
processor_class = Option::Registry.processor_for(option_name)
|
|
150
|
+
|
|
151
|
+
next if processor_class.nil?
|
|
152
|
+
|
|
153
|
+
processors_hash[option_name] = processor_class.new(
|
|
154
|
+
attribute_name: @attribute.name,
|
|
155
|
+
attribute_type: @attribute.type,
|
|
156
|
+
option_schema:
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Always include type validator
|
|
161
|
+
unless processors_hash.key?(:type)
|
|
162
|
+
processors_hash[:type] = Option::Validators::TypeValidator.new(
|
|
163
|
+
attribute_name: @attribute.name,
|
|
164
|
+
attribute_type: @attribute.type,
|
|
165
|
+
option_schema: nil
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
processors_hash
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Validates that all options are registered in the Registry
|
|
173
|
+
#
|
|
174
|
+
# @raise [Treaty::Exceptions::Validation] If unknown options found
|
|
175
|
+
# @return [void]
|
|
176
|
+
def validate_known_options!
|
|
177
|
+
unknown_options = @attribute.options.keys - Option::Registry.all_options
|
|
178
|
+
|
|
179
|
+
return if unknown_options.empty?
|
|
180
|
+
|
|
181
|
+
raise Treaty::Exceptions::Validation,
|
|
182
|
+
I18n.t(
|
|
183
|
+
"treaty.attributes.options.unknown",
|
|
184
|
+
attribute: @attribute.name,
|
|
185
|
+
unknown: unknown_options.join(", "),
|
|
186
|
+
known: Option::Registry.all_options.join(", ")
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Entity
|
|
5
|
+
module Attribute
|
|
6
|
+
module Validation
|
|
7
|
+
# Validates and transforms individual attributes.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Acts as the main interface for attribute validation and transformation.
|
|
12
|
+
# Delegates option processing to OptionOrchestrator and handles nested validation.
|
|
13
|
+
#
|
|
14
|
+
# ## Responsibilities
|
|
15
|
+
#
|
|
16
|
+
# 1. **Schema Validation** - Validates DSL definition correctness
|
|
17
|
+
# 2. **Value Validation** - Validates runtime data values
|
|
18
|
+
# 3. **Value Transformation** - Transforms values (defaults, etc.)
|
|
19
|
+
# 4. **Name Transformation** - Provides target name (for `as:` option)
|
|
20
|
+
# 5. **Nested Validation** - Delegates to NestedObjectValidator/NestedArrayValidator
|
|
21
|
+
#
|
|
22
|
+
# ## Usage
|
|
23
|
+
#
|
|
24
|
+
# Used by Orchestrator to validate each attribute:
|
|
25
|
+
#
|
|
26
|
+
# validator = AttributeValidator.new(attribute)
|
|
27
|
+
# validator.validate_schema!
|
|
28
|
+
# validator.validate_value!(value)
|
|
29
|
+
# transformed = validator.transform_value(value)
|
|
30
|
+
# target_name = validator.target_name
|
|
31
|
+
#
|
|
32
|
+
# ## Architecture
|
|
33
|
+
#
|
|
34
|
+
# Delegates to:
|
|
35
|
+
# - `OptionOrchestrator` - Coordinates all option processors
|
|
36
|
+
# - `NestedObjectValidator` - Validates nested object structures
|
|
37
|
+
# - `NestedArrayValidator` - Validates nested array structures
|
|
38
|
+
class AttributeValidator
|
|
39
|
+
attr_reader :attribute, :option_orchestrator
|
|
40
|
+
|
|
41
|
+
# Creates a new attribute validator instance
|
|
42
|
+
#
|
|
43
|
+
# @param attribute [Attribute::Base] The attribute to validate
|
|
44
|
+
def initialize(attribute)
|
|
45
|
+
@attribute = attribute
|
|
46
|
+
@option_orchestrator = OptionOrchestrator.new(attribute)
|
|
47
|
+
@nested_object_validator = nil
|
|
48
|
+
@nested_array_validator = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Validates the attribute schema (DSL definition)
|
|
52
|
+
#
|
|
53
|
+
# @raise [Treaty::Exceptions::Validation] If schema is invalid
|
|
54
|
+
# @return [void]
|
|
55
|
+
def validate_schema!
|
|
56
|
+
option_orchestrator.validate_schema!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Validates attribute value against all constraints
|
|
60
|
+
#
|
|
61
|
+
# @param value [Object] The value to validate
|
|
62
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
63
|
+
# @return [void]
|
|
64
|
+
def validate_value!(value)
|
|
65
|
+
option_orchestrator.validate_value!(value)
|
|
66
|
+
validate_nested!(value) if attribute.nested? && !value.nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Transforms attribute value through all modifiers
|
|
70
|
+
#
|
|
71
|
+
# @param value [Object] The value to transform
|
|
72
|
+
# @param root_data [Hash] Full raw data from root level (used by computed modifier)
|
|
73
|
+
# @return [Object] Transformed value
|
|
74
|
+
def transform_value(value, root_data = {})
|
|
75
|
+
option_orchestrator.transform_value(value, root_data)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Checks if attribute name is transformed
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean] True if name is transformed (as: option)
|
|
81
|
+
def transforms_name?
|
|
82
|
+
option_orchestrator.transforms_name?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Gets the target attribute name
|
|
86
|
+
#
|
|
87
|
+
# @return [Symbol] The target name (or original if not transformed)
|
|
88
|
+
def target_name
|
|
89
|
+
option_orchestrator.target_name
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Validates only the type constraint
|
|
93
|
+
# Used by nested transformers to validate types before nested validation
|
|
94
|
+
#
|
|
95
|
+
# @param value [Object] The value to validate
|
|
96
|
+
# @raise [Treaty::Exceptions::Validation] If type validation fails
|
|
97
|
+
# @return [void]
|
|
98
|
+
def validate_type!(value)
|
|
99
|
+
type_processor = option_orchestrator.processor_for(:type)
|
|
100
|
+
type_processor&.validate_value!(value)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Validates only the required constraint
|
|
104
|
+
# Used by nested transformers to validate presence before nested validation
|
|
105
|
+
#
|
|
106
|
+
# @param value [Object] The value to validate
|
|
107
|
+
# @raise [Treaty::Exceptions::Validation] If required validation fails
|
|
108
|
+
# @return [void]
|
|
109
|
+
def validate_required!(value)
|
|
110
|
+
required_processor = option_orchestrator.processor_for(:required)
|
|
111
|
+
required_processor&.validate_value!(value) if attribute.options.key?(:required)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# Validates nested attributes for object/array types
|
|
117
|
+
#
|
|
118
|
+
# @param value [Object] The value to validate
|
|
119
|
+
# @raise [Treaty::Exceptions::Validation] If nested validation fails
|
|
120
|
+
# @return [void]
|
|
121
|
+
def validate_nested!(value)
|
|
122
|
+
case attribute.type
|
|
123
|
+
when :object
|
|
124
|
+
nested_object_validator.validate!(value)
|
|
125
|
+
when :array
|
|
126
|
+
nested_array_validator.validate!(value)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Gets or creates nested object validator
|
|
131
|
+
#
|
|
132
|
+
# @return [NestedObjectValidator] Validator for nested objects
|
|
133
|
+
def nested_object_validator
|
|
134
|
+
@nested_object_validator ||= NestedObjectValidator.new(attribute)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Gets or creates nested array validator
|
|
138
|
+
#
|
|
139
|
+
# @return [NestedArrayValidator] Validator for nested arrays
|
|
140
|
+
def nested_array_validator
|
|
141
|
+
@nested_array_validator ||= NestedArrayValidator.new(attribute)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Entity
|
|
5
|
+
module Attribute
|
|
6
|
+
module Validation
|
|
7
|
+
# Base class for request and response validation.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Provides common interface for validation used in Treaty.
|
|
12
|
+
# Subclasses implement specific validation logic for requests and responses.
|
|
13
|
+
#
|
|
14
|
+
# ## Responsibilities
|
|
15
|
+
#
|
|
16
|
+
# 1. **Validation Interface** - Defines common validation interface
|
|
17
|
+
# 2. **Factory Pattern** - Provides class-level validate! method
|
|
18
|
+
#
|
|
19
|
+
# ## Subclasses
|
|
20
|
+
#
|
|
21
|
+
# - Request::Validation - Validates request data (uses Orchestrator::Request)
|
|
22
|
+
# - Response::Validation - Validates response data (uses Orchestrator::Response)
|
|
23
|
+
#
|
|
24
|
+
# ## Usage
|
|
25
|
+
#
|
|
26
|
+
# Subclasses must implement:
|
|
27
|
+
# - `validate!` - Performs validation and returns transformed data
|
|
28
|
+
#
|
|
29
|
+
# Example usage:
|
|
30
|
+
# Request::Validation.validate!(version_factory: factory, data: params)
|
|
31
|
+
#
|
|
32
|
+
# ## Factory Method
|
|
33
|
+
#
|
|
34
|
+
# The `self.validate!(...)` class method provides a convenient factory pattern:
|
|
35
|
+
# ```ruby
|
|
36
|
+
# Request::Validation.validate!(version_factory: factory, data: params)
|
|
37
|
+
# # Equivalent to:
|
|
38
|
+
# Request::Validation.new(version_factory: factory).validate!(data: params)
|
|
39
|
+
# ```
|
|
40
|
+
#
|
|
41
|
+
# ## Architecture
|
|
42
|
+
#
|
|
43
|
+
# Works with:
|
|
44
|
+
# - VersionFactory - Provides version information
|
|
45
|
+
# - Orchestrator::Base - Performs actual validation and transformation
|
|
46
|
+
class Base
|
|
47
|
+
# Class-level factory method for validation
|
|
48
|
+
# Creates instance and calls validate!
|
|
49
|
+
#
|
|
50
|
+
# @param args [Hash] Arguments passed to initialize and validate!
|
|
51
|
+
# @return [Hash] Validated and transformed data
|
|
52
|
+
def self.validate!(...)
|
|
53
|
+
new(...).validate!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Creates a new validation instance
|
|
57
|
+
#
|
|
58
|
+
# @param version_factory [VersionFactory] Factory containing version information
|
|
59
|
+
def initialize(version_factory:)
|
|
60
|
+
@version_factory = version_factory
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Performs validation and transformation
|
|
64
|
+
# Must be implemented in subclasses
|
|
65
|
+
#
|
|
66
|
+
# @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
|
|
67
|
+
# @return [Hash] Validated and transformed data
|
|
68
|
+
def validate!
|
|
69
|
+
raise Treaty::Exceptions::Validation,
|
|
70
|
+
I18n.t("treaty.attributes.validators.nested.orchestrator.collection_not_implemented")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|