treaty 0.18.0 → 0.20.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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/locales/en.yml +3 -3
  4. data/lib/treaty/action/base.rb +11 -0
  5. data/lib/treaty/action/context/callable.rb +90 -0
  6. data/lib/treaty/action/context/dsl.rb +56 -0
  7. data/lib/treaty/action/context/workspace.rb +92 -0
  8. data/lib/treaty/action/executor/inventory.rb +136 -0
  9. data/lib/treaty/{info/rest → action/info}/builder.rb +2 -2
  10. data/lib/treaty/{info/rest → action/info}/dsl.rb +2 -2
  11. data/lib/treaty/{info/rest → action/info}/result.rb +2 -2
  12. data/lib/treaty/action/inventory/collection.rb +77 -0
  13. data/lib/treaty/action/inventory/factory.rb +108 -0
  14. data/lib/treaty/action/inventory/inventory.rb +146 -0
  15. data/lib/treaty/action/request/attribute/attribute.rb +76 -0
  16. data/lib/treaty/action/request/attribute/builder.rb +98 -0
  17. data/lib/treaty/action/request/entity.rb +78 -0
  18. data/lib/treaty/action/request/factory.rb +116 -0
  19. data/lib/treaty/action/request/validator.rb +120 -0
  20. data/lib/treaty/action/response/attribute/attribute.rb +79 -0
  21. data/lib/treaty/action/response/attribute/builder.rb +96 -0
  22. data/lib/treaty/action/response/entity.rb +79 -0
  23. data/lib/treaty/action/response/factory.rb +129 -0
  24. data/lib/treaty/action/response/validator.rb +111 -0
  25. data/lib/treaty/action/result.rb +81 -0
  26. data/lib/treaty/action/versions/collection.rb +47 -0
  27. data/lib/treaty/action/versions/dsl.rb +116 -0
  28. data/lib/treaty/action/versions/execution/request.rb +287 -0
  29. data/lib/treaty/action/versions/executor.rb +61 -0
  30. data/lib/treaty/action/versions/factory.rb +253 -0
  31. data/lib/treaty/action/versions/resolver.rb +150 -0
  32. data/lib/treaty/action/versions/semantic.rb +64 -0
  33. data/lib/treaty/action/versions/workspace.rb +106 -0
  34. data/lib/treaty/action.rb +31 -0
  35. data/lib/treaty/controller/dsl.rb +1 -1
  36. data/lib/treaty/engine.rb +1 -1
  37. data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
  38. data/lib/treaty/entity/attribute/base.rb +184 -0
  39. data/lib/treaty/entity/attribute/builder/base.rb +275 -0
  40. data/lib/treaty/entity/attribute/collection.rb +67 -0
  41. data/lib/treaty/entity/attribute/dsl.rb +92 -0
  42. data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
  43. data/lib/treaty/entity/attribute/option/base.rb +190 -0
  44. data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
  45. data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
  46. data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
  47. data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
  48. data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
  49. data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
  50. data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
  51. data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
  52. data/lib/treaty/entity/attribute/option/registry.rb +157 -0
  53. data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
  54. data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
  55. data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
  56. data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
  57. data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
  58. data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
  59. data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
  60. data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
  61. data/lib/treaty/entity/attribute/validation/base.rb +76 -0
  62. data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
  63. data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
  64. data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
  65. data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
  66. data/lib/treaty/entity/base.rb +90 -0
  67. data/lib/treaty/entity/builder.rb +101 -0
  68. data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
  69. data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
  70. data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
  71. data/lib/treaty/entity.rb +7 -79
  72. data/lib/treaty/version.rb +1 -1
  73. metadata +66 -64
  74. data/lib/treaty/attribute/base.rb +0 -182
  75. data/lib/treaty/attribute/builder/base.rb +0 -273
  76. data/lib/treaty/attribute/collection.rb +0 -65
  77. data/lib/treaty/attribute/dsl.rb +0 -90
  78. data/lib/treaty/attribute/entity/builder.rb +0 -46
  79. data/lib/treaty/attribute/helper_mapper.rb +0 -72
  80. data/lib/treaty/attribute/option/base.rb +0 -188
  81. data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
  82. data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
  83. data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
  84. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
  85. data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
  86. data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
  87. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
  88. data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
  89. data/lib/treaty/attribute/option/registry.rb +0 -155
  90. data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
  91. data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
  92. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
  93. data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
  94. data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
  95. data/lib/treaty/attribute/option_normalizer.rb +0 -166
  96. data/lib/treaty/attribute/option_orchestrator.rb +0 -190
  97. data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
  98. data/lib/treaty/attribute/validation/base.rb +0 -74
  99. data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
  100. data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
  101. data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
  102. data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
  103. data/lib/treaty/base.rb +0 -9
  104. data/lib/treaty/context/callable.rb +0 -26
  105. data/lib/treaty/context/dsl.rb +0 -12
  106. data/lib/treaty/context/workspace.rb +0 -32
  107. data/lib/treaty/executor/inventory.rb +0 -122
  108. data/lib/treaty/inventory/collection.rb +0 -71
  109. data/lib/treaty/inventory/factory.rb +0 -91
  110. data/lib/treaty/inventory/inventory.rb +0 -92
  111. data/lib/treaty/request/attribute/attribute.rb +0 -25
  112. data/lib/treaty/request/attribute/builder.rb +0 -46
  113. data/lib/treaty/request/entity.rb +0 -33
  114. data/lib/treaty/request/factory.rb +0 -81
  115. data/lib/treaty/request/validator.rb +0 -60
  116. data/lib/treaty/response/attribute/attribute.rb +0 -25
  117. data/lib/treaty/response/attribute/builder.rb +0 -46
  118. data/lib/treaty/response/entity.rb +0 -33
  119. data/lib/treaty/response/factory.rb +0 -87
  120. data/lib/treaty/response/validator.rb +0 -53
  121. data/lib/treaty/result.rb +0 -23
  122. data/lib/treaty/versions/collection.rb +0 -15
  123. data/lib/treaty/versions/dsl.rb +0 -42
  124. data/lib/treaty/versions/execution/request.rb +0 -177
  125. data/lib/treaty/versions/executor.rb +0 -14
  126. data/lib/treaty/versions/factory.rb +0 -112
  127. data/lib/treaty/versions/resolver.rb +0 -70
  128. data/lib/treaty/versions/semantic.rb +0 -22
  129. data/lib/treaty/versions/workspace.rb +0 -43
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Validators
8
+ # Validates that attribute value matches the declared type.
9
+ #
10
+ # ## Supported Types
11
+ #
12
+ # - `:integer` - Ruby Integer
13
+ # - `:string` - Ruby String
14
+ # - `:boolean` - Ruby TrueClass or FalseClass
15
+ # - `:object` - Ruby Hash (for nested objects)
16
+ # - `:array` - Ruby Array (for collections)
17
+ # - `:date` - Ruby Date
18
+ # - `:time` - Ruby Time
19
+ # - `:datetime` - Ruby DateTime
20
+ #
21
+ # ## Usage Examples
22
+ #
23
+ # Simple types:
24
+ # integer :age
25
+ # string :name
26
+ # boolean :published
27
+ # date :published_on
28
+ # time :created_at
29
+ # datetime :updated_at
30
+ #
31
+ # Nested structures:
32
+ # object :author do
33
+ # string :name
34
+ # end
35
+ #
36
+ # array :tags do
37
+ # string :_self # Simple array of strings
38
+ # end
39
+ #
40
+ # ## Validation Rules
41
+ #
42
+ # - Validates only non-nil values (nil handling is done by RequiredValidator)
43
+ # - Type mismatch raises Treaty::Exceptions::Validation
44
+ # - Date accepts only Date objects (not DateTime or Time)
45
+ # - Time accepts only Time objects (not Date or DateTime)
46
+ # - DateTime accepts only DateTime objects (not Date or Time)
47
+ #
48
+ # ## Note
49
+ #
50
+ # TypeValidator doesn't use option_schema - it validates based on attribute_type.
51
+ # This validator is always active for all attributes.
52
+ class TypeValidator < Treaty::Entity::Attribute::Option::Base
53
+ ALLOWED_TYPES = %i[integer string boolean object array date time datetime].freeze
54
+
55
+ # Validates that the attribute type is one of the allowed types
56
+ #
57
+ # @raise [Treaty::Exceptions::Validation] If type is not allowed
58
+ # @return [void]
59
+ def validate_schema!
60
+ return if ALLOWED_TYPES.include?(@attribute_type)
61
+
62
+ raise Treaty::Exceptions::Validation,
63
+ I18n.t(
64
+ "treaty.attributes.validators.type.unknown_type",
65
+ type: @attribute_type,
66
+ attribute: @attribute_name,
67
+ allowed: ALLOWED_TYPES.join(", ")
68
+ )
69
+ end
70
+
71
+ # Validates that the value matches the declared type
72
+ # Skips validation for nil values (handled by RequiredValidator)
73
+ #
74
+ # @param value [Object] The value to validate
75
+ # @raise [Treaty::Exceptions::Validation] If value type doesn't match
76
+ # @return [void]
77
+ def validate_value!(value) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
78
+ return if value.nil? # Type validation doesn't check for nil, required does.
79
+
80
+ case @attribute_type
81
+ when :integer
82
+ validate_integer!(value)
83
+ when :string
84
+ validate_string!(value)
85
+ when :boolean
86
+ validate_boolean!(value)
87
+ when :object
88
+ validate_object!(value)
89
+ when :array
90
+ validate_array!(value)
91
+ when :date
92
+ validate_date!(value)
93
+ when :time
94
+ validate_time!(value)
95
+ when :datetime
96
+ validate_datetime!(value)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ # Common type validation logic
103
+ # Checks if value matches expected type and raises exception with appropriate message
104
+ #
105
+ # @param value [Object] The value to validate
106
+ # @param expected_type [Symbol] The expected type symbol
107
+ # @yield Block that returns true if value is valid
108
+ # @raise [Treaty::Exceptions::Validation] If type validation fails
109
+ # @return [void]
110
+ def validate_type!(value, expected_type)
111
+ return if yield(value)
112
+
113
+ actual_type = value.class
114
+
115
+ attributes = {
116
+ attribute: @attribute_name,
117
+ value:,
118
+ expected_type:,
119
+ actual_type:
120
+ }
121
+
122
+ message = resolve_custom_message(**attributes) || default_message(**attributes)
123
+
124
+ raise Treaty::Exceptions::Validation, message
125
+ end
126
+
127
+ # Generates default error message for type mismatch using I18n
128
+ #
129
+ # @param attribute [Symbol] The attribute name
130
+ # @param expected_type [Symbol] The expected type
131
+ # @param actual_type [Class] The actual class of the value
132
+ # @return [String] Default error message
133
+ def default_message(attribute:, expected_type:, actual_type:, **)
134
+ I18n.t(
135
+ "treaty.attributes.validators.type.mismatch.#{expected_type}",
136
+ attribute:,
137
+ actual: actual_type
138
+ )
139
+ end
140
+
141
+ # Validates that value is an Integer
142
+ #
143
+ # @param value [Object] The value to validate
144
+ # @raise [Treaty::Exceptions::Validation] If value is not an Integer
145
+ # @return [void]
146
+ def validate_integer!(value)
147
+ validate_type!(value, :integer) { |v| v.is_a?(Integer) }
148
+ end
149
+
150
+ # Validates that value is a String
151
+ #
152
+ # @param value [Object] The value to validate
153
+ # @raise [Treaty::Exceptions::Validation] If value is not a String
154
+ # @return [void]
155
+ def validate_string!(value)
156
+ validate_type!(value, :string) { |v| v.is_a?(String) }
157
+ end
158
+
159
+ # Validates that value is a Boolean (TrueClass or FalseClass)
160
+ #
161
+ # @param value [Object] The value to validate
162
+ # @raise [Treaty::Exceptions::Validation] If value is not a Boolean
163
+ # @return [void]
164
+ def validate_boolean!(value)
165
+ validate_type!(value, :boolean) { |v| v.is_a?(TrueClass) || v.is_a?(FalseClass) }
166
+ end
167
+
168
+ # Validates that value is a Hash (object type)
169
+ #
170
+ # @param value [Object] The value to validate
171
+ # @raise [Treaty::Exceptions::Validation] If value is not a Hash
172
+ # @return [void]
173
+ def validate_object!(value)
174
+ validate_type!(value, :object) { |v| v.is_a?(Hash) }
175
+ end
176
+
177
+ # Validates that value is an Array
178
+ #
179
+ # @param value [Object] The value to validate
180
+ # @raise [Treaty::Exceptions::Validation] If value is not an Array
181
+ # @return [void]
182
+ def validate_array!(value)
183
+ validate_type!(value, :array) { |v| v.is_a?(Array) }
184
+ end
185
+
186
+ # Validates that value is a Date (but not DateTime, since DateTime < Date)
187
+ #
188
+ # @param value [Object] The value to validate
189
+ # @raise [Treaty::Exceptions::Validation] If value is not a Date
190
+ # @return [void]
191
+ def validate_date!(value)
192
+ validate_type!(value, :date) { |v| v.is_a?(Date) && !v.is_a?(DateTime) }
193
+ end
194
+
195
+ # Validates that value is a Time or ActiveSupport::TimeWithZone
196
+ #
197
+ # @param value [Object] The value to validate
198
+ # @raise [Treaty::Exceptions::Validation] If value is not a Time
199
+ # @return [void]
200
+ def validate_time!(value)
201
+ validate_type!(value, :time) do |v|
202
+ v.is_a?(Time) || (defined?(ActiveSupport::TimeWithZone) && v.is_a?(ActiveSupport::TimeWithZone))
203
+ end
204
+ end
205
+
206
+ # Validates that value is a DateTime
207
+ #
208
+ # @param value [Object] The value to validate
209
+ # @raise [Treaty::Exceptions::Validation] If value is not a DateTime
210
+ # @return [void]
211
+ def validate_datetime!(value)
212
+ validate_type!(value, :datetime) { |v| v.is_a?(DateTime) }
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -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