treaty 0.0.1 → 0.2.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 +106 -17
- data/Rakefile +4 -2
- data/config/locales/en.yml +96 -0
- data/lib/treaty/attribute/base.rb +174 -0
- data/lib/treaty/attribute/builder/base.rb +143 -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 +160 -0
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +88 -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 +92 -0
- data/lib/treaty/attribute/option/validators/type_validator.rb +159 -0
- data/lib/treaty/attribute/option_normalizer.rb +151 -0
- data/lib/treaty/attribute/option_orchestrator.rb +187 -0
- data/lib/treaty/attribute/validation/attribute_validator.rb +144 -0
- data/lib/treaty/attribute/validation/base.rb +92 -0
- data/lib/treaty/attribute/validation/nested_array_validator.rb +199 -0
- data/lib/treaty/attribute/validation/nested_object_validator.rb +103 -0
- data/lib/treaty/attribute/validation/nested_transformer.rb +246 -0
- data/lib/treaty/attribute/validation/orchestrator/base.rb +194 -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 +47 -0
- data/lib/treaty/exceptions/class_name.rb +50 -0
- data/lib/treaty/exceptions/deprecated.rb +54 -0
- data/lib/treaty/exceptions/execution.rb +66 -0
- data/lib/treaty/exceptions/method_name.rb +55 -0
- data/lib/treaty/exceptions/nested_attributes.rb +65 -0
- data/lib/treaty/exceptions/not_implemented.rb +32 -0
- data/lib/treaty/exceptions/strategy.rb +63 -0
- data/lib/treaty/exceptions/unexpected.rb +70 -0
- data/lib/treaty/exceptions/validation.rb +97 -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 +147 -0
- data/lib/treaty/versions/executor.rb +14 -0
- data/lib/treaty/versions/factory.rb +92 -0
- data/lib/treaty/versions/resolver.rb +69 -0
- data/lib/treaty/versions/semantic.rb +22 -0
- data/lib/treaty/versions/workspace.rb +40 -0
- data/lib/treaty.rb +3 -3
- metadata +200 -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,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Option
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that attribute value is included in allowed set.
|
|
8
|
+
#
|
|
9
|
+
# ## Usage Examples
|
|
10
|
+
#
|
|
11
|
+
# Simple mode:
|
|
12
|
+
# string :provider, in: %w[twitter linkedin github]
|
|
13
|
+
#
|
|
14
|
+
# Advanced mode:
|
|
15
|
+
# string :provider, inclusion: { in: %w[twitter linkedin github], message: "Invalid provider" }
|
|
16
|
+
#
|
|
17
|
+
# ## Advanced Mode
|
|
18
|
+
#
|
|
19
|
+
# Uses `:in` as the value key (instead of default `:is`).
|
|
20
|
+
# Schema format: `{ in: [...], message: nil }`
|
|
21
|
+
class InclusionValidator < Treaty::Attribute::Option::Base
|
|
22
|
+
# Validates that allowed values are provided as non-empty array
|
|
23
|
+
#
|
|
24
|
+
# @raise [Treaty::Exceptions::Validation] If allowed values are not valid
|
|
25
|
+
# @return [void]
|
|
26
|
+
def validate_schema!
|
|
27
|
+
allowed_values = option_value
|
|
28
|
+
|
|
29
|
+
return if allowed_values.is_a?(Array) && !allowed_values.empty?
|
|
30
|
+
|
|
31
|
+
raise Treaty::Exceptions::Validation,
|
|
32
|
+
I18n.t("treaty.attributes.validators.inclusion.invalid_schema", attribute: @attribute_name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Validates that value is included in allowed set
|
|
36
|
+
# Skips validation for nil values (handled by RequiredValidator)
|
|
37
|
+
#
|
|
38
|
+
# @param value [Object] The value to validate
|
|
39
|
+
# @raise [Treaty::Exceptions::Validation] If value is not in allowed set
|
|
40
|
+
# @return [void]
|
|
41
|
+
def validate_value!(value)
|
|
42
|
+
return if value.nil? # Inclusion validation doesn't check for nil, required does.
|
|
43
|
+
|
|
44
|
+
allowed_values = option_value
|
|
45
|
+
|
|
46
|
+
return if allowed_values.include?(value)
|
|
47
|
+
|
|
48
|
+
message = custom_message || default_message(allowed_values, value)
|
|
49
|
+
|
|
50
|
+
raise Treaty::Exceptions::Validation, message
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
protected
|
|
54
|
+
|
|
55
|
+
# Returns the value key for inclusion validator
|
|
56
|
+
# Uses :in instead of default :is
|
|
57
|
+
#
|
|
58
|
+
# @return [Symbol] The value key (:in)
|
|
59
|
+
def value_key
|
|
60
|
+
:in
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Generates default error message with allowed values using I18n
|
|
66
|
+
#
|
|
67
|
+
# @param allowed_values [Array] Array of allowed values
|
|
68
|
+
# @param value [Object] The actual value that failed validation
|
|
69
|
+
# @return [String] Default error message
|
|
70
|
+
def default_message(allowed_values, value)
|
|
71
|
+
I18n.t("treaty.attributes.validators.inclusion.not_included",
|
|
72
|
+
attribute: @attribute_name,
|
|
73
|
+
allowed: allowed_values.join(", "),
|
|
74
|
+
value:)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Option
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that attribute value is present (not nil and not empty).
|
|
8
|
+
#
|
|
9
|
+
# ## Usage Examples
|
|
10
|
+
#
|
|
11
|
+
# Helper mode:
|
|
12
|
+
# string :title, :required # Maps to required: true
|
|
13
|
+
# string :bio, :optional # Maps to required: false
|
|
14
|
+
#
|
|
15
|
+
# Simple mode:
|
|
16
|
+
# string :title, required: true
|
|
17
|
+
# string :bio, required: false
|
|
18
|
+
#
|
|
19
|
+
# Advanced mode:
|
|
20
|
+
# string :title, required: { is: true, message: "Title is mandatory" }
|
|
21
|
+
#
|
|
22
|
+
# ## Default Behavior
|
|
23
|
+
#
|
|
24
|
+
# - Request attributes: required by default (required: true)
|
|
25
|
+
# - Response attributes: optional by default (required: false)
|
|
26
|
+
#
|
|
27
|
+
# ## Validation Rules
|
|
28
|
+
#
|
|
29
|
+
# A value is considered present if:
|
|
30
|
+
# - It is not nil
|
|
31
|
+
# - It is not empty (for String, Array, Hash)
|
|
32
|
+
#
|
|
33
|
+
# ## Advanced Mode
|
|
34
|
+
#
|
|
35
|
+
# Schema format: `{ is: true/false, message: nil }`
|
|
36
|
+
class RequiredValidator < Treaty::Attribute::Option::Base
|
|
37
|
+
# Validates schema (no validation needed, already normalized)
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
40
|
+
def validate_schema!
|
|
41
|
+
# Schema structure is already normalized by OptionNormalizer.
|
|
42
|
+
# Nothing to validate here.
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Validates that required attribute has a present value
|
|
46
|
+
#
|
|
47
|
+
# @param value [Object] The value to validate
|
|
48
|
+
# @raise [Treaty::Exceptions::Validation] If required but value is missing/empty
|
|
49
|
+
# @return [void]
|
|
50
|
+
def validate_value!(value)
|
|
51
|
+
return unless required?
|
|
52
|
+
return if present?(value)
|
|
53
|
+
|
|
54
|
+
message = custom_message || default_message
|
|
55
|
+
|
|
56
|
+
raise Treaty::Exceptions::Validation, message
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Checks if attribute is required
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean] True if attribute is required
|
|
64
|
+
def required?
|
|
65
|
+
return false if @option_schema.nil?
|
|
66
|
+
|
|
67
|
+
# Use option_value helper which correctly extracts value based on mode
|
|
68
|
+
option_value == true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Checks if value is present (not nil and not empty)
|
|
72
|
+
#
|
|
73
|
+
# @param value [Object] The value to check
|
|
74
|
+
# @return [Boolean] True if value is present
|
|
75
|
+
def present?(value)
|
|
76
|
+
return false if value.nil?
|
|
77
|
+
return false if value.respond_to?(:empty?) && value.empty?
|
|
78
|
+
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Generates default error message using I18n
|
|
83
|
+
#
|
|
84
|
+
# @return [String] Default error message
|
|
85
|
+
def default_message
|
|
86
|
+
I18n.t("treaty.attributes.validators.required.blank", attribute: @attribute_name)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
raise Treaty::Exceptions::Validation,
|
|
54
|
+
I18n.t("treaty.attributes.validators.type.unknown_type",
|
|
55
|
+
type: @attribute_type,
|
|
56
|
+
attribute: @attribute_name,
|
|
57
|
+
allowed: ALLOWED_TYPES.join(", "))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Validates that the value matches the declared type
|
|
61
|
+
# Skips validation for nil values (handled by RequiredValidator)
|
|
62
|
+
#
|
|
63
|
+
# @param value [Object] The value to validate
|
|
64
|
+
# @raise [Treaty::Exceptions::Validation] If value type doesn't match
|
|
65
|
+
# @return [void]
|
|
66
|
+
def validate_value!(value) # rubocop:disable Metrics/MethodLength
|
|
67
|
+
return if value.nil? # Type validation doesn't check for nil, required does.
|
|
68
|
+
|
|
69
|
+
case @attribute_type
|
|
70
|
+
when :integer
|
|
71
|
+
validate_integer!(value)
|
|
72
|
+
when :string
|
|
73
|
+
validate_string!(value)
|
|
74
|
+
when :object
|
|
75
|
+
validate_object!(value)
|
|
76
|
+
when :array
|
|
77
|
+
validate_array!(value)
|
|
78
|
+
when :datetime
|
|
79
|
+
validate_datetime!(value)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Validates that value is an Integer
|
|
86
|
+
#
|
|
87
|
+
# @param value [Object] The value to validate
|
|
88
|
+
# @raise [Treaty::Exceptions::Validation] If value is not an Integer
|
|
89
|
+
# @return [void]
|
|
90
|
+
def validate_integer!(value)
|
|
91
|
+
return if value.is_a?(Integer)
|
|
92
|
+
|
|
93
|
+
raise Treaty::Exceptions::Validation,
|
|
94
|
+
I18n.t("treaty.attributes.validators.type.mismatch.integer",
|
|
95
|
+
attribute: @attribute_name,
|
|
96
|
+
actual: value.class)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Validates that value is a String
|
|
100
|
+
#
|
|
101
|
+
# @param value [Object] The value to validate
|
|
102
|
+
# @raise [Treaty::Exceptions::Validation] If value is not a String
|
|
103
|
+
# @return [void]
|
|
104
|
+
def validate_string!(value)
|
|
105
|
+
return if value.is_a?(String)
|
|
106
|
+
|
|
107
|
+
raise Treaty::Exceptions::Validation,
|
|
108
|
+
I18n.t("treaty.attributes.validators.type.mismatch.string",
|
|
109
|
+
attribute: @attribute_name,
|
|
110
|
+
actual: value.class)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Validates that value is a Hash (object type)
|
|
114
|
+
#
|
|
115
|
+
# @param value [Object] The value to validate
|
|
116
|
+
# @raise [Treaty::Exceptions::Validation] If value is not a Hash
|
|
117
|
+
# @return [void]
|
|
118
|
+
def validate_object!(value)
|
|
119
|
+
return if value.is_a?(Hash)
|
|
120
|
+
|
|
121
|
+
raise Treaty::Exceptions::Validation,
|
|
122
|
+
I18n.t("treaty.attributes.validators.type.mismatch.object",
|
|
123
|
+
attribute: @attribute_name,
|
|
124
|
+
actual: value.class)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Validates that value is an Array
|
|
128
|
+
#
|
|
129
|
+
# @param value [Object] The value to validate
|
|
130
|
+
# @raise [Treaty::Exceptions::Validation] If value is not an Array
|
|
131
|
+
# @return [void]
|
|
132
|
+
def validate_array!(value)
|
|
133
|
+
return if value.is_a?(Array)
|
|
134
|
+
|
|
135
|
+
raise Treaty::Exceptions::Validation,
|
|
136
|
+
I18n.t("treaty.attributes.validators.type.mismatch.array",
|
|
137
|
+
attribute: @attribute_name,
|
|
138
|
+
actual: value.class)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Validates that value is a DateTime, Time, or Date
|
|
142
|
+
#
|
|
143
|
+
# @param value [Object] The value to validate
|
|
144
|
+
# @raise [Treaty::Exceptions::Validation] If value is not a datetime type
|
|
145
|
+
# @return [void]
|
|
146
|
+
def validate_datetime!(value)
|
|
147
|
+
# TODO: It is better to divide it into different methods for each class.
|
|
148
|
+
return if value.is_a?(DateTime) || value.is_a?(Time) || value.is_a?(Date)
|
|
149
|
+
|
|
150
|
+
raise Treaty::Exceptions::Validation,
|
|
151
|
+
I18n.t("treaty.attributes.validators.type.mismatch.datetime",
|
|
152
|
+
attribute: @attribute_name,
|
|
153
|
+
actual: value.class)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
# message: nil means use I18n default message from validators
|
|
132
|
+
{ value_key => value.fetch(value_key), message: value.fetch(:message, nil) }
|
|
133
|
+
else
|
|
134
|
+
# Simple mode, convert to advanced.
|
|
135
|
+
# message: nil means use I18n default message from validators
|
|
136
|
+
{ value_key => value, message: nil }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Checks if value is already in advanced mode
|
|
141
|
+
#
|
|
142
|
+
# @param value [Object] The value to check
|
|
143
|
+
# @param value_key [Symbol] The expected value key
|
|
144
|
+
# @return [Boolean] True if value is a hash with the value key
|
|
145
|
+
def advanced_mode?(value, value_key)
|
|
146
|
+
value.is_a?(Hash) && value.key?(value_key)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
raise Treaty::Exceptions::Validation,
|
|
180
|
+
I18n.t("treaty.attributes.options.unknown",
|
|
181
|
+
attribute: @attribute.name,
|
|
182
|
+
unknown: unknown_options.join(", "),
|
|
183
|
+
known: Option::Registry.all_options.join(", "))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|