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.
Files changed (80) 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/engine.rb +1 -1
  5. data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
  6. data/lib/treaty/entity/attribute/base.rb +184 -0
  7. data/lib/treaty/entity/attribute/builder/base.rb +275 -0
  8. data/lib/treaty/entity/attribute/collection.rb +67 -0
  9. data/lib/treaty/entity/attribute/dsl.rb +92 -0
  10. data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
  11. data/lib/treaty/entity/attribute/option/base.rb +190 -0
  12. data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
  13. data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
  14. data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
  15. data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
  16. data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
  17. data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
  18. data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
  19. data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
  20. data/lib/treaty/entity/attribute/option/registry.rb +157 -0
  21. data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
  22. data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
  23. data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
  24. data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
  25. data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
  26. data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
  27. data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
  28. data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
  29. data/lib/treaty/entity/attribute/validation/base.rb +76 -0
  30. data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
  31. data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
  32. data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
  33. data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
  34. data/lib/treaty/entity/base.rb +90 -0
  35. data/lib/treaty/entity/builder.rb +44 -0
  36. data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
  37. data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
  38. data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
  39. data/lib/treaty/entity.rb +7 -79
  40. data/lib/treaty/request/attribute/attribute.rb +1 -1
  41. data/lib/treaty/request/attribute/builder.rb +2 -2
  42. data/lib/treaty/request/entity.rb +1 -1
  43. data/lib/treaty/request/factory.rb +5 -5
  44. data/lib/treaty/request/validator.rb +1 -1
  45. data/lib/treaty/response/attribute/attribute.rb +1 -1
  46. data/lib/treaty/response/attribute/builder.rb +2 -2
  47. data/lib/treaty/response/entity.rb +1 -1
  48. data/lib/treaty/response/factory.rb +5 -5
  49. data/lib/treaty/response/validator.rb +1 -1
  50. data/lib/treaty/version.rb +1 -1
  51. metadata +35 -34
  52. data/lib/treaty/attribute/base.rb +0 -182
  53. data/lib/treaty/attribute/builder/base.rb +0 -273
  54. data/lib/treaty/attribute/collection.rb +0 -65
  55. data/lib/treaty/attribute/dsl.rb +0 -90
  56. data/lib/treaty/attribute/entity/builder.rb +0 -46
  57. data/lib/treaty/attribute/helper_mapper.rb +0 -72
  58. data/lib/treaty/attribute/option/base.rb +0 -188
  59. data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
  60. data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
  61. data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
  62. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
  63. data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
  64. data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
  65. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
  66. data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
  67. data/lib/treaty/attribute/option/registry.rb +0 -155
  68. data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
  69. data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
  70. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
  71. data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
  72. data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
  73. data/lib/treaty/attribute/option_normalizer.rb +0 -166
  74. data/lib/treaty/attribute/option_orchestrator.rb +0 -190
  75. data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
  76. data/lib/treaty/attribute/validation/base.rb +0 -74
  77. data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
  78. data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
  79. data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
  80. data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ # DSL module for defining attributes in Entity-like classes.
7
+ #
8
+ # This module provides the class-level DSL for defining attributes.
9
+ # It can be included in any class that needs attribute definition capabilities.
10
+ #
11
+ # ## Usage
12
+ #
13
+ # ```ruby
14
+ # class MyEntity
15
+ # include Treaty::Entity::Attribute::DSL
16
+ #
17
+ # string :name
18
+ # integer :age
19
+ # end
20
+ # ```
21
+ module DSL
22
+ def self.included(base)
23
+ base.extend(ClassMethods)
24
+ end
25
+
26
+ module ClassMethods
27
+ # Defines an attribute with explicit type
28
+ #
29
+ # @param name [Symbol] The attribute name
30
+ # @param type [Symbol] The attribute type
31
+ # @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
32
+ # @param options [Hash] Attribute options
33
+ # @param block [Proc] Block for nested attributes
34
+ # @return [void]
35
+ def attribute(name, type, *helpers, **options, &block)
36
+ collection_of_attributes << create_attribute(
37
+ name,
38
+ type,
39
+ *helpers,
40
+ nesting_level: 0,
41
+ **options,
42
+ &block
43
+ )
44
+ end
45
+
46
+ # Returns collection of attributes for this class
47
+ #
48
+ # @return [Collection] Collection of attributes
49
+ def collection_of_attributes
50
+ @collection_of_attributes ||= Treaty::Entity::Attribute::Collection.new
51
+ end
52
+
53
+ # Handles DSL methods like `string :name` where method name is the type
54
+ #
55
+ # @param type [Symbol] The attribute type (method name)
56
+ # @param name [Symbol] The attribute name (first argument)
57
+ # @param helpers [Array<Symbol>] Helper symbols
58
+ # @param options [Hash] Attribute options
59
+ # @param block [Proc] Block for nested attributes
60
+ # @return [void]
61
+ def method_missing(type, *helpers, **options, &block)
62
+ name = helpers.shift
63
+
64
+ # If no attribute name provided, this is not an attribute definition
65
+ # Pass to super to handle it properly (e.g., for methods like 'info', 'call!', etc.)
66
+ return super if name.nil?
67
+
68
+ attribute(name, type, *helpers, **options, &block)
69
+ end
70
+
71
+ def respond_to_missing?(name, *)
72
+ super
73
+ end
74
+
75
+ private
76
+
77
+ # Creates an attribute instance (must be implemented by including class)
78
+ #
79
+ # @raise [Treaty::Exceptions::NotImplemented] If not implemented
80
+ # @return [Attribute::Base] Created attribute instance
81
+ def create_attribute(*)
82
+ raise Treaty::Exceptions::NotImplemented,
83
+ I18n.t(
84
+ "treaty.attributes.dsl.create_attribute_not_implemented",
85
+ class: self
86
+ )
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ # Maps DSL helper symbols to their simple mode option equivalents.
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Helpers provide the most concise syntax for common options.
11
+ # They are syntactic sugar that gets converted to simple mode options.
12
+ #
13
+ # ## Available Helpers
14
+ #
15
+ # - `:required` → `required: true`
16
+ # - `:optional` → `required: false`
17
+ #
18
+ # ## Usage Examples
19
+ #
20
+ # Helper mode (most concise):
21
+ # string :title, :required
22
+ # string :bio, :optional
23
+ #
24
+ # Equivalent to simple mode:
25
+ # string :title, required: true
26
+ # string :bio, required: false
27
+ #
28
+ # ## Processing Flow
29
+ #
30
+ # 1. Helper mode: `string :title, :required`
31
+ # 2. HelperMapper: `:required` → `required: true`
32
+ # 3. OptionNormalizer: `required: true` → `{ is: true, message: nil }`
33
+ # 4. Final: Advanced mode used internally
34
+ #
35
+ # ## Adding New Helpers
36
+ #
37
+ # To add a new helper:
38
+ # ```ruby
39
+ # HELPER_MAPPINGS = {
40
+ # required: { required: true },
41
+ # optional: { required: false },
42
+ # my_helper: { my_option: :smth } # New helper example
43
+ # }.freeze
44
+ # ```
45
+ class HelperMapper
46
+ HELPER_MAPPINGS = {
47
+ required: { required: true },
48
+ optional: { required: false }
49
+ }.freeze
50
+
51
+ class << self
52
+ # Maps helper symbols to their simple mode equivalents
53
+ #
54
+ # @param helpers [Array<Symbol>] Array of helper symbols
55
+ # @return [Hash] Simple mode options hash
56
+ def map(helpers)
57
+ helpers.each_with_object({}) do |helper, result|
58
+ mapping = HELPER_MAPPINGS.fetch(helper)
59
+ result.merge!(mapping) if mapping.present?
60
+ end
61
+ end
62
+
63
+ # Checks if a symbol is a registered helper
64
+ #
65
+ # @param symbol [Symbol] Symbol to check
66
+ # @return [Boolean] True if symbol is a helper
67
+ def helper?(symbol)
68
+ HELPER_MAPPINGS.key?(symbol)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ # Base class for all option processors (validators and modifiers).
8
+ #
9
+ # ## Option Modes
10
+ #
11
+ # Treaty supports two modes for defining options:
12
+ #
13
+ # 1. **Simple Mode** - Concise syntax for common cases:
14
+ # - `required: true`
15
+ # - `as: :value`
16
+ # - `default: 12`
17
+ # - `in: %w[twitter linkedin]`
18
+ #
19
+ # 2. **Advanced Mode** - Extended syntax with custom messages:
20
+ # - `required: { is: true, message: "Custom error" }`
21
+ # - `as: { is: :value, message: nil }`
22
+ # - `inclusion: { in: %w[...], message: "Must be one of..." }`
23
+ #
24
+ # ## Helpers
25
+ #
26
+ # Helpers are shortcuts in DSL that map to simple mode options:
27
+ # - `:required` → `required: true`
28
+ # - `:optional` → `required: false`
29
+ #
30
+ # ## Advanced Mode Keys
31
+ #
32
+ # Each option in advanced mode has a value key:
33
+ # - Default key: `:is` (used by most options)
34
+ # - Special key: `:in` (used by inclusion validator)
35
+ #
36
+ # The value key is defined by overriding `value_key` method in subclasses.
37
+ #
38
+ # ## Processing Phases
39
+ #
40
+ # Each option processor can participate in three phases:
41
+ # - Phase 1: Schema validation (validate DSL definition correctness)
42
+ # - Phase 2: Value validation (validate runtime data values)
43
+ # - Phase 3: Value transformation (transform values: defaults, renaming, etc.)
44
+ class Base
45
+ # Creates a new option processor instance
46
+ #
47
+ # @param attribute_name [Symbol] The name of the attribute
48
+ # @param attribute_type [Symbol] The type of the attribute
49
+ # @param option_schema [Object] The option schema (simple or advanced mode)
50
+ def initialize(attribute_name:, attribute_type:, option_schema:)
51
+ @attribute_name = attribute_name
52
+ @attribute_type = attribute_type
53
+ @option_schema = option_schema
54
+ end
55
+
56
+ # Phase 1: Validates schema (DSL definition)
57
+ # Override in subclasses if validation is needed
58
+ #
59
+ # @raise [Treaty::Exceptions::Validation] If schema is invalid
60
+ # @return [void]
61
+ def validate_schema!
62
+ # No-op by default
63
+ end
64
+
65
+ # Phase 2: Validates value (runtime data)
66
+ # Override in subclasses if validation is needed
67
+ #
68
+ # @param value [Object] The value to validate
69
+ # @raise [Treaty::Exceptions::Validation] If value is invalid
70
+ # @return [void]
71
+ def validate_value!(value)
72
+ # No-op by default
73
+ end
74
+
75
+ # Phase 3: Transforms value
76
+ # Returns transformed value or original if no transformation needed
77
+ # Override in subclasses if transformation is needed
78
+ #
79
+ # @param value [Object] The value to transform
80
+ # @param _root_data [Hash] Full raw data from root level (used by computed modifier)
81
+ # @return [Object] Transformed value
82
+ def transform_value(value, _root_data = {})
83
+ value
84
+ end
85
+
86
+ # Indicates if this option processor transforms attribute names
87
+ # Override in subclasses if needed (e.g., AsModifier)
88
+ #
89
+ # @return [Boolean] True if this processor transforms names
90
+ def transforms_name?
91
+ false
92
+ end
93
+
94
+ # Returns the target name for the attribute if this processor transforms names
95
+ # Override in subclasses if needed (e.g., AsModifier)
96
+ #
97
+ # @return [Symbol] The target attribute name
98
+ def target_name
99
+ @attribute_name
100
+ end
101
+
102
+ protected
103
+
104
+ # Returns the value key for this option in advanced mode
105
+ # Default is :is, but can be overridden (e.g., :in for inclusion)
106
+ #
107
+ # @return [Symbol] The key used to store the value in advanced mode
108
+ def value_key
109
+ :is
110
+ end
111
+
112
+ # Checks if option is enabled
113
+ # Handles both simple mode (boolean) and advanced mode (hash with value key)
114
+ #
115
+ # @return [Boolean] Whether the option is enabled
116
+ def option_enabled?
117
+ return false if @option_schema.nil?
118
+ return @option_schema if @option_schema.is_a?(TrueClass) || @option_schema.is_a?(FalseClass)
119
+
120
+ @option_schema.fetch(value_key, false)
121
+ end
122
+
123
+ # Extracts the actual value from normalized schema
124
+ # Works with both simple mode and advanced mode
125
+ #
126
+ # In simple mode: returns the value directly
127
+ # In advanced mode: extracts value using the appropriate key (is/in)
128
+ #
129
+ # @return [Object] The actual value from the option schema
130
+ def option_value
131
+ return @option_schema unless @option_schema.is_a?(Hash)
132
+
133
+ @option_schema.fetch(value_key, nil)
134
+ end
135
+
136
+ # Gets custom error message from advanced mode schema
137
+ # Returns nil if no custom message, which triggers I18n default message
138
+ #
139
+ # @return [String, Proc, nil] Custom error message, lambda, or nil for default message
140
+ def custom_message
141
+ return nil unless @option_schema.is_a?(Hash)
142
+
143
+ @option_schema.fetch(:message, nil)
144
+ end
145
+
146
+ # Resolves custom message with lambda support
147
+ # If message is a lambda, calls it with provided named arguments
148
+ # Catches all exceptions from lambda execution and re-raises as Validation errors
149
+ #
150
+ # @param attributes [Hash] Named arguments to pass to lambda
151
+ # @return [String, nil] Resolved message string or nil
152
+ # @raise [Treaty::Exceptions::Validation] If custom message lambda raises an exception
153
+ def resolve_custom_message(**attributes) # rubocop:disable Metrics/MethodLength
154
+ message = custom_message
155
+ return nil if message.nil?
156
+
157
+ if message.respond_to?(:call)
158
+ message.call(**attributes)
159
+ else
160
+ message
161
+ end
162
+ rescue StandardError => e
163
+ # Catch all exceptions from custom message lambda execution
164
+ error_message = I18n.t(
165
+ "treaty.attributes.options.message_evaluation_error",
166
+ attribute: @attribute_name,
167
+ error: e.message
168
+ )
169
+
170
+ raise Treaty::Exceptions::Validation, error_message
171
+ end
172
+
173
+ # Checks if schema is in advanced mode
174
+ #
175
+ # @return [Boolean] True if schema is in advanced mode (hash with value key)
176
+ def advanced_mode?
177
+ @option_schema.is_a?(Hash) && @option_schema.key?(value_key)
178
+ end
179
+
180
+ # Checks if schema is in simple mode
181
+ #
182
+ # @return [Boolean] True if schema is in simple mode (not a hash or no value key)
183
+ def simple_mode?
184
+ !advanced_mode?
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Conditionals
8
+ # Base class for conditional option processors.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # Conditionals control whether an attribute should be processed at all.
13
+ # Unlike validators (which check data) and modifiers (which transform data),
14
+ # conditionals determine attribute visibility based on runtime conditions.
15
+ #
16
+ # ## Key Difference from Validators/Modifiers
17
+ #
18
+ # - **Validators**: Check if data is valid
19
+ # - **Modifiers**: Transform data values
20
+ # - **Conditionals**: Decide if attribute exists in output
21
+ #
22
+ # ## Processing
23
+ #
24
+ # Conditionals are evaluated BEFORE validators and modifiers:
25
+ # 1. If condition evaluates to `false` → attribute is skipped entirely
26
+ # 2. If condition evaluates to `true` → attribute is processed normally
27
+ #
28
+ # ## Mode Support
29
+ #
30
+ # Conditionals do NOT support simple/advanced modes.
31
+ # They only accept lambda/proc directly:
32
+ #
33
+ # ```ruby
34
+ # # Correct
35
+ # integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
36
+ # array :tags, if: ->(post:) { post[:published_at].present? }
37
+ #
38
+ # # Incorrect - no simple/advanced mode
39
+ # integer :rating, if: true # Not supported
40
+ # integer :rating, if: { is: ..., message: ... } # Not supported
41
+ # ```
42
+ #
43
+ # ## Implementation
44
+ #
45
+ # Subclasses must implement:
46
+ # - `validate_schema!` - Validate the conditional schema at definition time
47
+ # - `evaluate_condition(data)` - Evaluate condition with runtime data
48
+ class Base < Treaty::Entity::Attribute::Option::Base
49
+ # Phase 1: Validates conditional schema
50
+ # Must be overridden in subclasses
51
+ #
52
+ # @raise [Treaty::Exceptions::Validation] If schema is invalid
53
+ # @return [void]
54
+ def validate_schema!
55
+ raise Treaty::Exceptions::NotImplemented,
56
+ "#{self.class} must implement #validate_schema!"
57
+ end
58
+
59
+ # Evaluates the conditional with runtime data
60
+ # Must be overridden in subclasses
61
+ #
62
+ # @param _data [Hash] Raw data to evaluate condition against
63
+ # @raise [Treaty::Exceptions::Validation] If evaluation fails
64
+ # @return [Boolean] True if attribute should be processed, false otherwise
65
+ def evaluate_condition(_data)
66
+ raise Treaty::Exceptions::NotImplemented,
67
+ "#{self.class} must implement #evaluate_condition"
68
+ end
69
+
70
+ # Conditionals do not validate values
71
+ # This is a no-op for conditionals
72
+ #
73
+ # @param _value [Object] The value (unused)
74
+ # @return [void]
75
+ def validate_value!(_value)
76
+ # No-op: conditionals don't validate values
77
+ end
78
+
79
+ # Conditionals do not transform values
80
+ # This is a no-op for conditionals
81
+ #
82
+ # @param value [Object] The value to pass through
83
+ # @return [Object] The unchanged value
84
+ def transform_value(value)
85
+ value
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Conditionals
8
+ # Conditionally includes attributes based on runtime data evaluation.
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Basic usage with keyword arguments splat:
13
+ # array :tags, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
14
+ # integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
15
+ #
16
+ # Named argument pattern:
17
+ # array :tags, if: ->(post:) { post[:published_at].present? }
18
+ # integer :views, if: ->(post:) { post[:published_at].present? }
19
+ #
20
+ # Complex conditions:
21
+ # string :admin_note, if: (lambda do |**attributes|
22
+ # attributes.dig(:user, :role) == "admin" && attributes.dig(:post, :flagged)
23
+ # end)
24
+ #
25
+ # ## Use Cases
26
+ #
27
+ # 1. **Show fields only when published**:
28
+ # ```ruby
29
+ # response 200 do
30
+ # object :post do
31
+ # string :id
32
+ # string :title
33
+ # datetime :published_at, :optional
34
+ # integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
35
+ # end
36
+ # end
37
+ # # If published_at is nil → rating is excluded from response
38
+ # # If published_at exists → rating is included
39
+ # ```
40
+ #
41
+ # 2. **Role-based field visibility**:
42
+ # ```ruby
43
+ # response 200 do
44
+ # object :user do
45
+ # string :name
46
+ # string :email, if: ->(user:) { user[:role] == "admin" }
47
+ # end
48
+ # end
49
+ # ```
50
+ #
51
+ # 3. **Nested attribute conditionals**:
52
+ # ```ruby
53
+ # object :post do
54
+ # string :title
55
+ # array :tags, if: ->(post:) { post[:published_at].present? } do
56
+ # string :_self
57
+ # end
58
+ # end
59
+ # ```
60
+ #
61
+ # ## Important Notes
62
+ #
63
+ # - Lambda receives raw data as named arguments
64
+ # - Lambda MUST return truthy/falsy value
65
+ # - If condition is false → attribute is completely omitted
66
+ # - If condition is true → attribute is validated and transformed normally
67
+ # - All exceptions in lambda are caught and wrapped in Treaty::Exceptions::Validation
68
+ # - Does NOT support simple mode (if: true) or advanced mode (if: { is: ..., message: ... })
69
+ #
70
+ # ## Error Handling
71
+ #
72
+ # If the lambda raises any exception, it's caught and converted to a
73
+ # Treaty::Exceptions::Validation with detailed error message including:
74
+ # - Attribute name
75
+ # - Original exception message
76
+ #
77
+ # ## Data Access Pattern
78
+ #
79
+ # The lambda receives the same data structure that the orchestrator processes.
80
+ # For nested attributes, you can access parent data using dig:
81
+ #
82
+ # ```ruby
83
+ # # For response with { post: { title: "...", published_at: "..." } }
84
+ # integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
85
+ #
86
+ # # Alternative: named argument pattern
87
+ # integer :rating, if: ->(post:) { post[:published_at].present? }
88
+ # ```
89
+ class IfConditional < Treaty::Entity::Attribute::Option::Conditionals::Base
90
+ # Validates that if option is a callable (Proc/Lambda)
91
+ #
92
+ # @raise [Treaty::Exceptions::Validation] If if is not a Proc/lambda
93
+ # @return [void]
94
+ def validate_schema!
95
+ conditional_lambda = @option_schema
96
+
97
+ return if conditional_lambda.respond_to?(:call)
98
+
99
+ raise Treaty::Exceptions::Validation,
100
+ I18n.t(
101
+ "treaty.attributes.conditionals.if.invalid_type",
102
+ attribute: @attribute_name,
103
+ type: conditional_lambda.class
104
+ )
105
+ end
106
+
107
+ # Evaluates the conditional lambda with runtime data
108
+ # Returns boolean indicating if attribute should be processed
109
+ #
110
+ # @param data [Hash] Raw data from request/response/entity
111
+ # @raise [Treaty::Exceptions::Validation] If lambda execution fails
112
+ # @return [Boolean] True if attribute should be processed, false to skip it
113
+ def evaluate_condition(data)
114
+ conditional_lambda = @option_schema
115
+
116
+ # Call lambda with raw data as named arguments
117
+ # The lambda can use **attributes or specific named args like post:
118
+ result = conditional_lambda.call(**data)
119
+
120
+ # Convert result to boolean
121
+ !!result
122
+ rescue StandardError => e
123
+ # Catch all exceptions from lambda execution
124
+ raise Treaty::Exceptions::Validation,
125
+ I18n.t(
126
+ "treaty.attributes.conditionals.if.evaluation_error",
127
+ attribute: @attribute_name,
128
+ error: e.message
129
+ )
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end