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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +106 -17
  3. data/Rakefile +4 -2
  4. data/config/locales/en.yml +96 -0
  5. data/lib/treaty/attribute/base.rb +174 -0
  6. data/lib/treaty/attribute/builder/base.rb +143 -0
  7. data/lib/treaty/attribute/collection.rb +65 -0
  8. data/lib/treaty/attribute/helper_mapper.rb +72 -0
  9. data/lib/treaty/attribute/option/base.rb +160 -0
  10. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +88 -0
  11. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +103 -0
  12. data/lib/treaty/attribute/option/registry.rb +128 -0
  13. data/lib/treaty/attribute/option/registry_initializer.rb +90 -0
  14. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +80 -0
  15. data/lib/treaty/attribute/option/validators/required_validator.rb +92 -0
  16. data/lib/treaty/attribute/option/validators/type_validator.rb +159 -0
  17. data/lib/treaty/attribute/option_normalizer.rb +151 -0
  18. data/lib/treaty/attribute/option_orchestrator.rb +187 -0
  19. data/lib/treaty/attribute/validation/attribute_validator.rb +144 -0
  20. data/lib/treaty/attribute/validation/base.rb +92 -0
  21. data/lib/treaty/attribute/validation/nested_array_validator.rb +199 -0
  22. data/lib/treaty/attribute/validation/nested_object_validator.rb +103 -0
  23. data/lib/treaty/attribute/validation/nested_transformer.rb +246 -0
  24. data/lib/treaty/attribute/validation/orchestrator/base.rb +194 -0
  25. data/lib/treaty/base.rb +9 -0
  26. data/lib/treaty/configuration.rb +17 -0
  27. data/lib/treaty/context/callable.rb +24 -0
  28. data/lib/treaty/context/dsl.rb +12 -0
  29. data/lib/treaty/context/workspace.rb +28 -0
  30. data/lib/treaty/controller/dsl.rb +38 -0
  31. data/lib/treaty/engine.rb +37 -0
  32. data/lib/treaty/exceptions/base.rb +47 -0
  33. data/lib/treaty/exceptions/class_name.rb +50 -0
  34. data/lib/treaty/exceptions/deprecated.rb +54 -0
  35. data/lib/treaty/exceptions/execution.rb +66 -0
  36. data/lib/treaty/exceptions/method_name.rb +55 -0
  37. data/lib/treaty/exceptions/nested_attributes.rb +65 -0
  38. data/lib/treaty/exceptions/not_implemented.rb +32 -0
  39. data/lib/treaty/exceptions/strategy.rb +63 -0
  40. data/lib/treaty/exceptions/unexpected.rb +70 -0
  41. data/lib/treaty/exceptions/validation.rb +97 -0
  42. data/lib/treaty/info/builder.rb +122 -0
  43. data/lib/treaty/info/dsl.rb +26 -0
  44. data/lib/treaty/info/result.rb +13 -0
  45. data/lib/treaty/request/attribute/attribute.rb +24 -0
  46. data/lib/treaty/request/attribute/builder.rb +22 -0
  47. data/lib/treaty/request/attribute/validation/orchestrator.rb +27 -0
  48. data/lib/treaty/request/attribute/validator.rb +50 -0
  49. data/lib/treaty/request/factory.rb +32 -0
  50. data/lib/treaty/request/scope/collection.rb +21 -0
  51. data/lib/treaty/request/scope/factory.rb +42 -0
  52. data/lib/treaty/response/attribute/attribute.rb +24 -0
  53. data/lib/treaty/response/attribute/builder.rb +22 -0
  54. data/lib/treaty/response/attribute/validation/orchestrator.rb +27 -0
  55. data/lib/treaty/response/attribute/validator.rb +44 -0
  56. data/lib/treaty/response/factory.rb +38 -0
  57. data/lib/treaty/response/scope/collection.rb +21 -0
  58. data/lib/treaty/response/scope/factory.rb +42 -0
  59. data/lib/treaty/result.rb +22 -0
  60. data/lib/treaty/strategy.rb +31 -0
  61. data/lib/treaty/support/loader.rb +24 -0
  62. data/lib/treaty/version.rb +8 -1
  63. data/lib/treaty/versions/collection.rb +15 -0
  64. data/lib/treaty/versions/dsl.rb +30 -0
  65. data/lib/treaty/versions/execution/request.rb +147 -0
  66. data/lib/treaty/versions/executor.rb +14 -0
  67. data/lib/treaty/versions/factory.rb +92 -0
  68. data/lib/treaty/versions/resolver.rb +69 -0
  69. data/lib/treaty/versions/semantic.rb +22 -0
  70. data/lib/treaty/versions/workspace.rb +40 -0
  71. data/lib/treaty.rb +3 -3
  72. metadata +200 -27
  73. data/.standard.yml +0 -3
  74. data/CHANGELOG.md +0 -5
  75. data/CODE_OF_CONDUCT.md +0 -84
  76. data/LICENSE.txt +0 -21
  77. data/sig/treaty.rbs +0 -4
  78. 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