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