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,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
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Conditionals
8
+ # Conditionally excludes attributes based on runtime data evaluation.
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Basic usage with keyword arguments splat:
13
+ # array :tags, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
14
+ # integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
15
+ #
16
+ # Named argument pattern:
17
+ # array :draft_notes, unless: ->(post:) { post[:published_at].present? }
18
+ # integer :edit_count, unless: ->(post:) { post[:published_at].present? }
19
+ #
20
+ # Complex conditions:
21
+ # string :internal_note, unless: (lambda do |**attributes|
22
+ # attributes.dig(:user, :role) == "admin" && attributes.dig(:post, :flagged)
23
+ # end)
24
+ #
25
+ # ## Use Cases
26
+ #
27
+ # 1. **Hide fields 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 :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
35
+ # end
36
+ # end
37
+ # # If published_at is nil → draft_views is included in response
38
+ # # If published_at exists → draft_views is excluded
39
+ # ```
40
+ #
41
+ # 2. **Role-based field exclusion**:
42
+ # ```ruby
43
+ # response 200 do
44
+ # object :user do
45
+ # string :name
46
+ # string :internal_id, unless: ->(user:) { user[:role] == "public" }
47
+ # end
48
+ # end
49
+ # ```
50
+ #
51
+ # 3. **Nested attribute conditionals**:
52
+ # ```ruby
53
+ # object :post do
54
+ # string :title
55
+ # array :draft_notes, unless: ->(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 true → attribute is completely omitted (OPPOSITE of `if`)
66
+ # - If condition is false → 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 (unless: true) or advanced mode (unless: { is: ..., message: ... })
69
+ #
70
+ # ## Difference from `if` Option
71
+ #
72
+ # `unless` is the logical opposite of `if`:
73
+ # - `if` includes attribute when condition is TRUE
74
+ # - `unless` includes attribute when condition is FALSE
75
+ #
76
+ # ```ruby
77
+ # # These are equivalent:
78
+ # integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
79
+ # integer :rating, unless: ->(**attributes) { attributes.dig(:post, :published_at).blank? }
80
+ #
81
+ # # These are also equivalent:
82
+ # integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
83
+ # integer :draft_views, if: ->(**attributes) { attributes.dig(:post, :published_at).blank? }
84
+ # ```
85
+ #
86
+ # ## Error Handling
87
+ #
88
+ # If the lambda raises any exception, it's caught and converted to a
89
+ # Treaty::Exceptions::Validation with detailed error message including:
90
+ # - Attribute name
91
+ # - Original exception message
92
+ #
93
+ # ## Data Access Pattern
94
+ #
95
+ # The lambda receives the same data structure that the orchestrator processes.
96
+ # For nested attributes, you can access parent data using dig:
97
+ #
98
+ # ```ruby
99
+ # # For response with { post: { title: "...", published_at: "..." } }
100
+ # integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
101
+ #
102
+ # # Alternative: named argument pattern
103
+ # integer :draft_views, unless: ->(post:) { post[:published_at].present? }
104
+ # ```
105
+ class UnlessConditional < Treaty::Entity::Attribute::Option::Conditionals::Base
106
+ # Validates that unless option is a callable (Proc/Lambda)
107
+ #
108
+ # @raise [Treaty::Exceptions::Validation] If unless is not a Proc/lambda
109
+ # @return [void]
110
+ def validate_schema!
111
+ conditional_lambda = @option_schema
112
+
113
+ return if conditional_lambda.respond_to?(:call)
114
+
115
+ raise Treaty::Exceptions::Validation,
116
+ I18n.t(
117
+ "treaty.attributes.conditionals.unless.invalid_type",
118
+ attribute: @attribute_name,
119
+ type: conditional_lambda.class
120
+ )
121
+ end
122
+
123
+ # Evaluates the conditional lambda with runtime data
124
+ # Returns boolean indicating if attribute should be processed
125
+ #
126
+ # @param data [Hash] Raw data from request/response/entity
127
+ # @raise [Treaty::Exceptions::Validation] If lambda execution fails
128
+ # @return [Boolean] True if attribute should be processed (when condition is FALSE), false to skip it
129
+ def evaluate_condition(data)
130
+ conditional_lambda = @option_schema
131
+
132
+ # Call lambda with raw data as named arguments
133
+ # The lambda can use **attributes or specific named args like post:
134
+ result = conditional_lambda.call(**data)
135
+
136
+ # Convert result to boolean and NEGATE it (opposite of if)
137
+ # unless includes attribute when condition is FALSE
138
+ !result
139
+ rescue StandardError => e
140
+ # Catch all exceptions from lambda execution
141
+ raise Treaty::Exceptions::Validation,
142
+ I18n.t(
143
+ "treaty.attributes.conditionals.unless.evaluation_error",
144
+ attribute: @attribute_name,
145
+ error: e.message
146
+ )
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Modifiers
8
+ # Transforms attribute names during data processing.
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Simple mode:
13
+ # # Request: expects "handle", outputs as "value"
14
+ # string :handle, as: :value
15
+ #
16
+ # Advanced mode:
17
+ # string :handle, as: { is: :value, message: nil }
18
+ #
19
+ # ## Use Cases
20
+ #
21
+ # 1. **Request to Service mapping**:
22
+ # ```ruby
23
+ # request do
24
+ # string :user_id, as: :id
25
+ # end
26
+ # # Input: { user_id: "123" }
27
+ # # Service receives: { id: "123" }
28
+ # ```
29
+ #
30
+ # 2. **Service to Response mapping**:
31
+ # ```ruby
32
+ # response 200 do
33
+ # string :id, as: :user_id
34
+ # end
35
+ # # Service returns: { id: "123" }
36
+ # # Output: { user_id: "123" }
37
+ # ```
38
+ #
39
+ # ## How It Works
40
+ #
41
+ # AsModifier doesn't transform values - it transforms attribute names.
42
+ # The orchestrator uses `target_name` to map source name to target name.
43
+ #
44
+ # ## Advanced Mode
45
+ #
46
+ # Schema format: `{ is: :symbol, message: nil }`
47
+ class AsModifier < Treaty::Entity::Attribute::Option::Base
48
+ # Validates that target name is a Symbol
49
+ #
50
+ # @raise [Treaty::Exceptions::Validation] If target is not a Symbol
51
+ # @return [void]
52
+ def validate_schema!
53
+ target = option_value
54
+
55
+ return if target.is_a?(Symbol)
56
+
57
+ raise Treaty::Exceptions::Validation,
58
+ I18n.t(
59
+ "treaty.attributes.modifiers.as.invalid_type",
60
+ attribute: @attribute_name,
61
+ type: target.class
62
+ )
63
+ end
64
+
65
+ # Indicates that AsModifier transforms attribute names
66
+ #
67
+ # @return [Boolean] Always returns true
68
+ def transforms_name?
69
+ true
70
+ end
71
+
72
+ # Returns the target name for the attribute
73
+ #
74
+ # @return [Symbol] The target attribute name
75
+ def target_name
76
+ option_value
77
+ end
78
+
79
+ # AsModifier doesn't modify the value itself, only the name
80
+ # The renaming is handled by the orchestrator using target_name
81
+ #
82
+ # @param value [Object] The value to transform
83
+ # @param _root_data [Hash] Unused root data parameter
84
+ # @return [Object] Unchanged value
85
+ def transform_value(value, _root_data = {})
86
+ value
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end