treaty 0.13.0 → 0.15.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 600a422c9ddcdff83b3ec4cde1c8124adb8ae52e1c1d88744d8474bc4afa8c68
4
- data.tar.gz: dd425b19d451b34f46b07747586f35ba7df4dcac9c554a481ac5dd27e1fe2141
3
+ metadata.gz: a3fcf5acc02ad345e7ff61ba70124596a89be46af035b6ee5d4114de8509f883
4
+ data.tar.gz: bc5d32074c9cab57438851555fb6849014b52da7f0732517a54db2f48563a198
5
5
  SHA512:
6
- metadata.gz: 1739edd5d1c38fe70fc6ab892b06ec15df3a665724df68c95f351675dae4e8c6a00368d65489bbfde0900a67841b9aad1e7eb57f81b948248ae902755f485519
7
- data.tar.gz: 627d321a706c846e3249afb9e97f4ceb48573af52fcf962459e38316ed76ed7d979c4fd0fcffa8146cff5e7cfeab36b4cf391f9bfb3d7c82ca8de3cd6c5ea439
6
+ metadata.gz: 3ebb477674d48c4e4e0aafa30eeb9ca3ad20dd96c65f19d38d31b55f6256b7e816768b149720662c9de239242a9aeab78ed038685dd0d82754b005c9e31380c8
7
+ data.tar.gz: b04b8248df32a42c41b833920cada319fcfb73344a7fa9520867b59fbba60655716f8152342f9a34765c06aa05d99160b7ff9812b826ff7bebca2ccd6a1cda94
data/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  </div>
14
14
 
15
15
  > [!WARNING]
16
- > **Development Status**: Treaty is currently under active development in the 0.x version series. Breaking changes may occur between minor versions (0.x) as we refine the API and add new features. The library will stabilize with the 1.0 release. We recommend pinning to specific patch versions in your Gemfile (e.g., `gem "treaty", "~> 0.12.0"`) until the 1.0 release.
16
+ > **Development Status**: Treaty is currently under active development in the 0.x version series. Breaking changes may occur between minor versions (0.x) as we refine the API and add new features. The library will stabilize with the 1.0 release. We recommend pinning to specific patch versions in your Gemfile (e.g., `gem "treaty", "~> 0.15.0"`) until the 1.0 release.
17
17
 
18
18
  ## 📚 Documentation
19
19
 
@@ -79,7 +79,7 @@ module Posts
79
79
  string :title
80
80
  string :content
81
81
  string :summary
82
- datetime :created_at
82
+ time :created_at
83
83
  end
84
84
  end
85
85
 
@@ -133,4 +133,4 @@ Thank you to all [contributors](https://github.com/servactory/treaty/graphs/cont
133
133
 
134
134
  ## 📄 License
135
135
 
136
- Treaty is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
136
+ Treaty is available as open source under the terms of the [MIT License](./LICENSE).
@@ -17,7 +17,9 @@ en:
17
17
  boolean: "Attribute '%{attribute}' must be a Boolean (true or false), got %{actual}"
18
18
  object: "Attribute '%{attribute}' must be a Hash (object), got %{actual}"
19
19
  array: "Attribute '%{attribute}' must be an Array, got %{actual}"
20
- datetime: "Attribute '%{attribute}' must be a DateTime/Time/Date, got %{actual}"
20
+ date: "Attribute '%{attribute}' must be a Date, got %{actual}"
21
+ time: "Attribute '%{attribute}' must be a Time, got %{actual}"
22
+ datetime: "Attribute '%{attribute}' must be a DateTime, got %{actual}"
21
23
 
22
24
  inclusion:
23
25
  invalid_schema: "Option 'inclusion' for attribute '%{attribute}' must have a non-empty array of allowed values"
@@ -43,6 +45,7 @@ en:
43
45
  # Attribute options
44
46
  options:
45
47
  unknown: "Unknown options for attribute '%{attribute}': %{unknown}. Known options: %{known}"
48
+ message_evaluation_error: "Custom message evaluation failed for attribute '%{attribute}': %{error}"
46
49
 
47
50
  # Attribute modifiers
48
51
  modifiers:
@@ -60,6 +63,18 @@ en:
60
63
  conversion_not_supported: "Option 'cast' for attribute '%{attribute}' does not support conversion from '%{from}' to '%{to}'"
61
64
  conversion_error: "Cast failed for attribute '%{attribute}' from '%{from}' to '%{to}'. Value: '%{value}'. Error: %{error}"
62
65
 
66
+ # Attribute conditionals
67
+ conditionals:
68
+ if:
69
+ invalid_type: "Option 'if' for attribute '%{attribute}' must be a Proc or Lambda. Got: %{type}"
70
+ evaluation_error: "Conditional evaluation failed for attribute '%{attribute}': %{error}"
71
+
72
+ unless:
73
+ invalid_type: "Option 'unless' for attribute '%{attribute}' must be a Proc or Lambda. Got: %{type}"
74
+ evaluation_error: "Conditional evaluation failed for attribute '%{attribute}': %{error}"
75
+
76
+ mutual_exclusivity_error: "Attribute '%{attribute}' cannot have both 'if' and 'unless' options. Use only one conditional option."
77
+
63
78
  # Attribute builder DSL
64
79
  builder:
65
80
  not_implemented: "%{class} must implement #create_attribute"
@@ -143,10 +143,12 @@ module Treaty
143
143
 
144
144
  # Resolves custom message with lambda support
145
145
  # If message is a lambda, calls it with provided named arguments
146
+ # Catches all exceptions from lambda execution and re-raises as Validation errors
146
147
  #
147
148
  # @param attributes [Hash] Named arguments to pass to lambda
148
149
  # @return [String, nil] Resolved message string or nil
149
- def resolve_custom_message(**attributes)
150
+ # @raise [Treaty::Exceptions::Validation] If custom message lambda raises an exception
151
+ def resolve_custom_message(**attributes) # rubocop:disable Metrics/MethodLength
150
152
  message = custom_message
151
153
  return nil if message.nil?
152
154
 
@@ -155,6 +157,15 @@ module Treaty
155
157
  else
156
158
  message
157
159
  end
160
+ rescue StandardError => e
161
+ # Catch all exceptions from custom message lambda execution
162
+ error_message = I18n.t(
163
+ "treaty.attributes.options.message_evaluation_error",
164
+ attribute: @attribute_name,
165
+ error: e.message
166
+ )
167
+
168
+ raise Treaty::Exceptions::Validation, error_message
158
169
  end
159
170
 
160
171
  # Checks if schema is in advanced mode
@@ -0,0 +1,90 @@
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
@@ -0,0 +1,134 @@
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: ->(**attrs) {
21
+ # attrs.dig(:user, :role) == "admin" && attrs.dig(:post, :flagged)
22
+ # }
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: ->(**attrs) { attrs.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: ->(**attrs) { attrs.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
@@ -0,0 +1,151 @@
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: ->(**attrs) {
21
+ # attrs.dig(:user, :role) == "admin" && attrs.dig(:post, :flagged)
22
+ # }
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: ->(**attrs) { attrs.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: ->(**attrs) { attrs.dig(:post, :published_at).present? }
78
+ # integer :rating, unless: ->(**attrs) { attrs.dig(:post, :published_at).blank? }
79
+ #
80
+ # # These are also equivalent:
81
+ # integer :draft_views, unless: ->(**attrs) { attrs.dig(:post, :published_at).present? }
82
+ # integer :draft_views, if: ->(**attrs) { attrs.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: ->(**attrs) { attrs.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
@@ -50,24 +50,42 @@ module Treaty
50
50
  # ### From Integer
51
51
  # - integer -> string: Converts to string representation
52
52
  # - integer -> boolean: 0 = false, non-zero = true
53
- # - integer -> datetime: Treats as Unix timestamp
53
+ # - integer -> date: Treats as Unix timestamp, converts to date
54
+ # - integer -> time: Treats as Unix timestamp
55
+ # - integer -> datetime: Treats as Unix timestamp, converts to datetime
54
56
  #
55
57
  # ### From String
56
58
  # - string -> integer: Parses integer from string
57
59
  # - string -> boolean: Parses truthy/falsy strings (true/false, yes/no, 1/0, on/off)
60
+ # - string -> date: Parses date string
61
+ # - string -> time: Parses time string
58
62
  # - string -> datetime: Parses datetime string (ISO8601, RFC3339, etc.)
59
63
  #
60
64
  # ### From Boolean
61
65
  # - boolean -> string: Converts to "true" or "false"
62
66
  # - boolean -> integer: true = 1, false = 0
63
67
  #
68
+ # ### From Date
69
+ # - date -> string: Converts to ISO8601 format
70
+ # - date -> integer: Converts to Unix timestamp
71
+ # - date -> time: Converts to Time at midnight
72
+ # - date -> datetime: Converts to DateTime at midnight
73
+ #
74
+ # ### From Time
75
+ # - time -> string: Converts to ISO8601 format
76
+ # - time -> integer: Converts to Unix timestamp
77
+ # - time -> date: Converts to Date
78
+ # - time -> datetime: Converts to DateTime
79
+ #
64
80
  # ### From DateTime
65
81
  # - datetime -> string: Converts to ISO8601 format
66
82
  # - datetime -> integer: Converts to Unix timestamp
83
+ # - datetime -> date: Converts to Date
84
+ # - datetime -> time: Converts to Time
67
85
  #
68
86
  # ## Important Notes
69
87
  #
70
- # - Cast option only works with scalar types (integer, string, boolean, datetime)
88
+ # - Cast option only works with scalar types (integer, string, boolean, date, time, datetime)
71
89
  # - Array and Object types are not supported for casting
72
90
  # - Casting to the same type is allowed (no-op)
73
91
  # - Nil values are not transformed (handled by RequiredValidator)
@@ -82,9 +100,9 @@ module Treaty
82
100
  #
83
101
  # Schema format: `{ to: :target_type, message: "Custom error" }`
84
102
  # Note: Uses `:to` key instead of the default `:is` key.
85
- class CastModifier < Treaty::Attribute::Option::Base
103
+ class CastModifier < Treaty::Attribute::Option::Base # rubocop:disable Metrics/ClassLength
86
104
  # Types that support casting (scalar types only)
87
- ALLOWED_CAST_TYPES = %i[integer string boolean datetime].freeze
105
+ ALLOWED_CAST_TYPES = %i[integer string boolean date time datetime].freeze
88
106
 
89
107
  # Validates that cast option is correctly configured
90
108
  #
@@ -202,12 +220,16 @@ module Treaty
202
220
  integer: ->(value:) { value }, # No-op for same type
203
221
  string: ->(value:) { value.to_s },
204
222
  boolean: ->(value:) { value != 0 },
205
- datetime: ->(value:) { Time.at(value) }
223
+ date: ->(value:) { Time.at(value).to_date },
224
+ time: ->(value:) { Time.at(value) },
225
+ datetime: ->(value:) { Time.at(value).to_datetime }
206
226
  },
207
227
  string: {
208
228
  string: ->(value:) { value }, # No-op for same type
209
229
  integer: ->(value:) { Integer(value) },
210
230
  boolean: ->(value:) { parse_boolean(value) },
231
+ date: ->(value:) { Date.parse(value) },
232
+ time: ->(value:) { Time.parse(value) },
211
233
  datetime: ->(value:) { DateTime.parse(value) }
212
234
  },
213
235
  boolean: {
@@ -215,10 +237,26 @@ module Treaty
215
237
  string: ->(value:) { value.to_s },
216
238
  integer: ->(value:) { value ? 1 : 0 }
217
239
  },
240
+ date: {
241
+ date: ->(value:) { value }, # No-op for same type
242
+ string: ->(value:) { value.iso8601 },
243
+ integer: ->(value:) { value.to_time.to_i },
244
+ time: ->(value:) { value.to_time },
245
+ datetime: ->(value:) { value.to_datetime }
246
+ },
247
+ time: {
248
+ time: ->(value:) { value }, # No-op for same type
249
+ string: ->(value:) { value.iso8601 },
250
+ integer: ->(value:) { value.to_i },
251
+ date: ->(value:) { value.to_date },
252
+ datetime: ->(value:) { value.to_datetime }
253
+ },
218
254
  datetime: {
219
255
  datetime: ->(value:) { value }, # No-op for same type
220
256
  string: ->(value:) { value.iso8601 },
221
- integer: ->(value:) { value.to_i }
257
+ integer: ->(value:) { value.to_i },
258
+ date: ->(value:) { value.to_date },
259
+ time: ->(value:) { value.to_time }
222
260
  }
223
261
  }
224
262
  end
@@ -3,7 +3,7 @@
3
3
  module Treaty
4
4
  module Attribute
5
5
  module Option
6
- # Central registry for all option processors (validators and modifiers).
6
+ # Central registry for all option processors (validators, modifiers, and conditionals).
7
7
  #
8
8
  # ## Purpose
9
9
  #
@@ -14,7 +14,7 @@ module Treaty
14
14
  #
15
15
  # 1. **Registration** - Stores option processor classes
16
16
  # 2. **Retrieval** - Provides access to registered processors
17
- # 3. **Categorization** - Organizes processors by category (validator/modifier)
17
+ # 3. **Categorization** - Organizes processors by category (validator/modifier/conditional)
18
18
  # 4. **Validation** - Checks if options are registered
19
19
  #
20
20
  # ## Registered Options
@@ -23,15 +23,23 @@ module Treaty
23
23
  # - `:required` → RequiredValidator
24
24
  # - `:type` → TypeValidator
25
25
  # - `:inclusion` → InclusionValidator
26
+ # - `:format` → FormatValidator
26
27
  #
27
28
  # ### Modifiers
28
29
  # - `:as` → AsModifier
29
30
  # - `:default` → DefaultModifier
31
+ # - `:transform` → TransformModifier
32
+ # - `:cast` → CastModifier
33
+ #
34
+ # ### Conditionals
35
+ # - `:if` → IfConditional
36
+ # - `:unless` → UnlessConditional
30
37
  #
31
38
  # ## Usage
32
39
  #
33
40
  # Registration (done in RegistryInitializer):
34
41
  # Registry.register(:required, RequiredValidator, category: :validator)
42
+ # Registry.register(:if, IfConditional, category: :conditional)
35
43
  #
36
44
  # Retrieval (done in OptionOrchestrator):
37
45
  # processor_class = Registry.processor_for(:required)
@@ -111,6 +119,14 @@ module Treaty
111
119
  .transform_values { |info| info.fetch(:processor_class) }
112
120
  end
113
121
 
122
+ # Get all conditionals
123
+ #
124
+ # @return [Hash] Hash of option_name => processor_class for conditionals
125
+ def conditionals
126
+ registry.select { |_, info| info.fetch(:category) == :conditional }
127
+ .transform_values { |info| info.fetch(:processor_class) }
128
+ end
129
+
114
130
  # Reset registry (mainly for testing)
115
131
  def reset!
116
132
  @registry = nil
@@ -7,14 +7,15 @@ module Treaty
7
7
  #
8
8
  # ## Purpose
9
9
  #
10
- # Centralized registration point for all option processors (validators and modifiers).
10
+ # Centralized registration point for all option processors (validators, modifiers, and conditionals).
11
11
  # Automatically registers all built-in options when loaded.
12
12
  #
13
13
  # ## Responsibilities
14
14
  #
15
15
  # 1. **Validator Registration** - Registers all built-in validators
16
16
  # 2. **Modifier Registration** - Registers all built-in modifiers
17
- # 3. **Auto-Loading** - Executes automatically when file is loaded
17
+ # 3. **Conditional Registration** - Registers all built-in conditionals
18
+ # 4. **Auto-Loading** - Executes automatically when file is loaded
18
19
  #
19
20
  # ## Built-in Validators
20
21
  #
@@ -30,6 +31,11 @@ module Treaty
30
31
  # - `:transform` → TransformModifier - Transforms values using custom lambdas
31
32
  # - `:cast` → CastModifier - Converts values between types automatically
32
33
  #
34
+ # ## Built-in Conditionals
35
+ #
36
+ # - `:if` → IfConditional - Conditionally includes attributes based on runtime data
37
+ # - `:unless` → UnlessConditional - Conditionally excludes attributes based on runtime data
38
+ #
33
39
  # ## Auto-Registration
34
40
  #
35
41
  # This file calls `register_all!` when loaded, ensuring all processors
@@ -63,6 +69,7 @@ module Treaty
63
69
  def register_all!
64
70
  register_validators!
65
71
  register_modifiers!
72
+ register_conditionals!
66
73
  end
67
74
 
68
75
  private
@@ -86,6 +93,14 @@ module Treaty
86
93
  Registry.register(:transform, Modifiers::TransformModifier, category: :modifier)
87
94
  Registry.register(:cast, Modifiers::CastModifier, category: :modifier)
88
95
  end
96
+
97
+ # Registers all built-in conditionals
98
+ #
99
+ # @return [void]
100
+ def register_conditionals!
101
+ Registry.register(:if, Conditionals::IfConditional, category: :conditional)
102
+ Registry.register(:unless, Conditionals::UnlessConditional, category: :conditional)
103
+ end
89
104
  end
90
105
  end
91
106
  end
@@ -13,7 +13,9 @@ module Treaty
13
13
  # - `:boolean` - Ruby TrueClass or FalseClass
14
14
  # - `:object` - Ruby Hash (for nested objects)
15
15
  # - `:array` - Ruby Array (for collections)
16
- # - `:datetime` - Ruby DateTime, Time, or Date
16
+ # - `:date` - Ruby Date
17
+ # - `:time` - Ruby Time
18
+ # - `:datetime` - Ruby DateTime
17
19
  #
18
20
  # ## Usage Examples
19
21
  #
@@ -21,7 +23,9 @@ module Treaty
21
23
  # integer :age
22
24
  # string :name
23
25
  # boolean :published
24
- # datetime :created_at
26
+ # date :published_on
27
+ # time :created_at
28
+ # datetime :updated_at
25
29
  #
26
30
  # Nested structures:
27
31
  # object :author do
@@ -36,14 +40,16 @@ module Treaty
36
40
  #
37
41
  # - Validates only non-nil values (nil handling is done by RequiredValidator)
38
42
  # - Type mismatch raises Treaty::Exceptions::Validation
39
- # - Datetime accepts DateTime, Time, or Date objects
43
+ # - Date accepts only Date objects (not DateTime or Time)
44
+ # - Time accepts only Time objects (not Date or DateTime)
45
+ # - DateTime accepts only DateTime objects (not Date or Time)
40
46
  #
41
47
  # ## Note
42
48
  #
43
49
  # TypeValidator doesn't use option_schema - it validates based on attribute_type.
44
50
  # This validator is always active for all attributes.
45
51
  class TypeValidator < Treaty::Attribute::Option::Base
46
- ALLOWED_TYPES = %i[integer string boolean object array datetime].freeze
52
+ ALLOWED_TYPES = %i[integer string boolean object array date time datetime].freeze
47
53
 
48
54
  # Validates that the attribute type is one of the allowed types
49
55
  #
@@ -81,6 +87,10 @@ module Treaty
81
87
  validate_object!(value)
82
88
  when :array
83
89
  validate_array!(value)
90
+ when :date
91
+ validate_date!(value)
92
+ when :time
93
+ validate_time!(value)
84
94
  when :datetime
85
95
  validate_datetime!(value)
86
96
  end
@@ -172,14 +182,33 @@ module Treaty
172
182
  validate_type!(value, :array) { |v| v.is_a?(Array) }
173
183
  end
174
184
 
175
- # Validates that value is a DateTime, Time, or Date
185
+ # Validates that value is a Date (but not DateTime, since DateTime < Date)
176
186
  #
177
187
  # @param value [Object] The value to validate
178
- # @raise [Treaty::Exceptions::Validation] If value is not a datetime type
188
+ # @raise [Treaty::Exceptions::Validation] If value is not a Date
189
+ # @return [void]
190
+ def validate_date!(value)
191
+ validate_type!(value, :date) { |v| v.is_a?(Date) && !v.is_a?(DateTime) }
192
+ end
193
+
194
+ # Validates that value is a Time or ActiveSupport::TimeWithZone
195
+ #
196
+ # @param value [Object] The value to validate
197
+ # @raise [Treaty::Exceptions::Validation] If value is not a Time
198
+ # @return [void]
199
+ def validate_time!(value)
200
+ validate_type!(value, :time) do |v|
201
+ v.is_a?(Time) || (defined?(ActiveSupport::TimeWithZone) && v.is_a?(ActiveSupport::TimeWithZone))
202
+ end
203
+ end
204
+
205
+ # Validates that value is a DateTime
206
+ #
207
+ # @param value [Object] The value to validate
208
+ # @raise [Treaty::Exceptions::Validation] If value is not a DateTime
179
209
  # @return [void]
180
210
  def validate_datetime!(value)
181
- # TODO: It is better to divide it into different methods for each class.
182
- validate_type!(value, :datetime) { |v| v.is_a?(DateTime) || v.is_a?(Time) || v.is_a?(Date) }
211
+ validate_type!(value, :datetime) { |v| v.is_a?(DateTime) }
183
212
  end
184
213
  end
185
214
  end
@@ -79,6 +79,9 @@ module Treaty
79
79
  transformed = {}
80
80
 
81
81
  attribute.collection_of_attributes.each do |nested_attribute|
82
+ # Check if conditional (if/unless option) - skip attribute if condition evaluates to skip
83
+ next unless should_process_attribute?(nested_attribute, value)
84
+
82
85
  process_attribute(nested_attribute, value, transformed)
83
86
  end
84
87
 
@@ -87,6 +90,87 @@ module Treaty
87
90
 
88
91
  private
89
92
 
93
+ # Returns the conditional option name if present (:if or :unless)
94
+ # Raises error if both are present (mutual exclusivity)
95
+ #
96
+ # @param nested_attribute [Attribute::Base] The attribute to check
97
+ # @raise [Treaty::Exceptions::Validation] If both :if and :unless are present
98
+ # @return [Symbol, nil] :if, :unless, or nil
99
+ def conditional_option_for(nested_attribute) # rubocop:disable Metrics/MethodLength
100
+ has_if = nested_attribute.options.key?(:if)
101
+ has_unless = nested_attribute.options.key?(:unless)
102
+
103
+ if has_if && has_unless
104
+ raise Treaty::Exceptions::Validation,
105
+ I18n.t(
106
+ "treaty.attributes.conditionals.mutual_exclusivity_error",
107
+ attribute: nested_attribute.name
108
+ )
109
+ end
110
+
111
+ return :if if has_if
112
+ return :unless if has_unless
113
+
114
+ nil
115
+ end
116
+
117
+ # Gets cached conditional processors for attributes or builds them
118
+ #
119
+ # @return [Hash] Hash of attribute => conditional processor
120
+ def conditionals_for_attributes
121
+ @conditionals_for_attributes ||= build_conditionals_for_attributes
122
+ end
123
+
124
+ # Builds conditional processors for attributes with :if or :unless option
125
+ # Validates schema at definition time for performance
126
+ #
127
+ # @return [Hash] Hash of attribute => conditional processor
128
+ def build_conditionals_for_attributes # rubocop:disable Metrics/MethodLength
129
+ attribute.collection_of_attributes.each_with_object({}) do |nested_attribute, cache|
130
+ # Get conditional option name (:if or :unless)
131
+ conditional_type = conditional_option_for(nested_attribute)
132
+ next if conditional_type.nil?
133
+
134
+ processor_class = Option::Registry.processor_for(conditional_type)
135
+ next if processor_class.nil?
136
+
137
+ # Create processor instance
138
+ conditional = processor_class.new(
139
+ attribute_name: nested_attribute.name,
140
+ attribute_type: nested_attribute.type,
141
+ option_schema: nested_attribute.options.fetch(conditional_type)
142
+ )
143
+
144
+ # Validate schema at definition time (not runtime)
145
+ conditional.validate_schema!
146
+
147
+ cache[nested_attribute] = conditional
148
+ end
149
+ end
150
+
151
+ # Checks if an attribute should be processed based on its conditional (if/unless option)
152
+ # Returns true if no conditional is defined or if conditional evaluates appropriately
153
+ #
154
+ # @param nested_attribute [Attribute::Base] The attribute to check
155
+ # @param source_hash [Hash] Source data to pass to conditional
156
+ # @return [Boolean] True if attribute should be processed, false to skip it
157
+ def should_process_attribute?(nested_attribute, source_hash)
158
+ # Check if attribute has a conditional option
159
+ conditional_type = conditional_option_for(nested_attribute)
160
+ return true if conditional_type.nil?
161
+
162
+ # Get cached conditional processor
163
+ conditional = conditionals_for_attributes[nested_attribute]
164
+ return true if conditional.nil?
165
+
166
+ # Evaluate condition with source hash data wrapped with parent object name
167
+ wrapped_data = { attribute.name => source_hash }
168
+ conditional.evaluate_condition(wrapped_data)
169
+ rescue StandardError
170
+ # If conditional evaluation fails, skip the attribute
171
+ false
172
+ end
173
+
90
174
  # Processes a single nested attribute
91
175
  # Validates, transforms, and adds to target hash
92
176
  #
@@ -117,7 +201,7 @@ module Treaty
117
201
  end
118
202
 
119
203
  # Transforms array with nested attributes
120
- class ArrayTransformer
204
+ class ArrayTransformer # rubocop:disable Metrics/ClassLength
121
205
  SELF_OBJECT = :_self
122
206
  private_constant :SELF_OBJECT
123
207
 
@@ -147,6 +231,87 @@ module Treaty
147
231
 
148
232
  private
149
233
 
234
+ # Returns the conditional option name if present (:if or :unless)
235
+ # Raises error if both are present (mutual exclusivity)
236
+ #
237
+ # @param nested_attribute [Attribute::Base] The attribute to check
238
+ # @raise [Treaty::Exceptions::Validation] If both :if and :unless are present
239
+ # @return [Symbol, nil] :if, :unless, or nil
240
+ def conditional_option_for(nested_attribute) # rubocop:disable Metrics/MethodLength
241
+ has_if = nested_attribute.options.key?(:if)
242
+ has_unless = nested_attribute.options.key?(:unless)
243
+
244
+ if has_if && has_unless
245
+ raise Treaty::Exceptions::Validation,
246
+ I18n.t(
247
+ "treaty.attributes.conditionals.mutual_exclusivity_error",
248
+ attribute: nested_attribute.name
249
+ )
250
+ end
251
+
252
+ return :if if has_if
253
+ return :unless if has_unless
254
+
255
+ nil
256
+ end
257
+
258
+ # Gets cached conditional processors for attributes or builds them
259
+ #
260
+ # @return [Hash] Hash of attribute => conditional processor
261
+ def conditionals_for_attributes
262
+ @conditionals_for_attributes ||= build_conditionals_for_attributes
263
+ end
264
+
265
+ # Builds conditional processors for attributes with :if or :unless option
266
+ # Validates schema at definition time for performance
267
+ #
268
+ # @return [Hash] Hash of attribute => conditional processor
269
+ def build_conditionals_for_attributes # rubocop:disable Metrics/MethodLength
270
+ attribute.collection_of_attributes.each_with_object({}) do |nested_attribute, cache|
271
+ # Get conditional option name (:if or :unless)
272
+ conditional_type = conditional_option_for(nested_attribute)
273
+ next if conditional_type.nil?
274
+
275
+ processor_class = Option::Registry.processor_for(conditional_type)
276
+ next if processor_class.nil?
277
+
278
+ # Create processor instance
279
+ conditional = processor_class.new(
280
+ attribute_name: nested_attribute.name,
281
+ attribute_type: nested_attribute.type,
282
+ option_schema: nested_attribute.options.fetch(conditional_type)
283
+ )
284
+
285
+ # Validate schema at definition time (not runtime)
286
+ conditional.validate_schema!
287
+
288
+ cache[nested_attribute] = conditional
289
+ end
290
+ end
291
+
292
+ # Checks if an attribute should be processed based on its conditional (if/unless option)
293
+ # Returns true if no conditional is defined or if conditional evaluates appropriately
294
+ #
295
+ # @param nested_attribute [Attribute::Base] The attribute to check
296
+ # @param source_hash [Hash] Source data to pass to conditional
297
+ # @return [Boolean] True if attribute should be processed, false to skip it
298
+ def should_process_attribute?(nested_attribute, source_hash)
299
+ # Check if attribute has a conditional option
300
+ conditional_type = conditional_option_for(nested_attribute)
301
+ return true if conditional_type.nil?
302
+
303
+ # Get cached conditional processor
304
+ conditional = conditionals_for_attributes[nested_attribute]
305
+ return true if conditional.nil?
306
+
307
+ # Evaluate condition with source hash data wrapped with parent array attribute name
308
+ wrapped_data = { attribute.name => source_hash }
309
+ conditional.evaluate_condition(wrapped_data)
310
+ rescue StandardError
311
+ # If conditional evaluation fails, skip the attribute
312
+ false
313
+ end
314
+
150
315
  # Checks if this is a simple array (primitive values)
151
316
  #
152
317
  # @return [Boolean] True if array contains primitive values with :_self attribute
@@ -163,8 +328,8 @@ module Treaty
163
328
  # @raise [Treaty::Exceptions::Validation] If validation fails
164
329
  # @return [Object] Transformed element value
165
330
  def transform_simple_element(item, index) # rubocop:disable Metrics/MethodLength
166
- self_attr = attribute.collection_of_attributes.first
167
- validator = AttributeValidator.new(self_attr)
331
+ self_attribute = attribute.collection_of_attributes.first
332
+ validator = AttributeValidator.new(self_attribute)
168
333
  validator.validate_schema!
169
334
 
170
335
  begin
@@ -201,6 +366,9 @@ module Treaty
201
366
  transformed = {}
202
367
 
203
368
  attribute.collection_of_attributes.each do |nested_attribute|
369
+ # Check if conditional (if/unless option) - skip attribute if condition evaluates to skip
370
+ next unless should_process_attribute?(nested_attribute, item)
371
+
204
372
  process_attribute(nested_attribute, item, transformed, index)
205
373
  end
206
374
 
@@ -70,12 +70,16 @@ module Treaty
70
70
 
71
71
  # Validates and transforms all attributes
72
72
  # Iterates through attributes, processes them, handles :_self objects
73
+ # Skips attributes with false conditional (if/unless option)
73
74
  #
74
75
  # @return [Hash] Transformed data with all attributes processed
75
- def validate!
76
+ def validate! # rubocop:disable Metrics/MethodLength
76
77
  transformed_data = {}
77
78
 
78
79
  collection_of_attributes.each do |attribute|
80
+ # Check if conditional (if/unless option) - skip attribute if condition evaluates to skip
81
+ next unless should_process_attribute?(attribute)
82
+
79
83
  transformed_value = validate_and_transform_attribute!(attribute)
80
84
 
81
85
  if attribute.name == SELF_OBJECT && attribute.type == :object
@@ -91,6 +95,49 @@ module Treaty
91
95
 
92
96
  private
93
97
 
98
+ # Returns the conditional option name if present (:if or :unless)
99
+ # Raises error if both are present (mutual exclusivity)
100
+ #
101
+ # @param attribute [Attribute::Base] The attribute to check
102
+ # @raise [Treaty::Exceptions::Validation] If both :if and :unless are present
103
+ # @return [Symbol, nil] :if, :unless, or nil
104
+ def conditional_option_for(attribute) # rubocop:disable Metrics/MethodLength
105
+ has_if = attribute.options.key?(:if)
106
+ has_unless = attribute.options.key?(:unless)
107
+
108
+ if has_if && has_unless
109
+ raise Treaty::Exceptions::Validation,
110
+ I18n.t(
111
+ "treaty.attributes.conditionals.mutual_exclusivity_error",
112
+ attribute: attribute.name
113
+ )
114
+ end
115
+
116
+ return :if if has_if
117
+ return :unless if has_unless
118
+
119
+ nil
120
+ end
121
+
122
+ # Checks if an attribute should be processed based on its conditional (if/unless option)
123
+ # Returns true if no conditional is defined or if conditional evaluates appropriately
124
+ #
125
+ # @param attribute [Attribute::Base] The attribute to check
126
+ # @return [Boolean] True if attribute should be processed, false to skip it
127
+ def should_process_attribute?(attribute)
128
+ # Check if attribute has a conditional option
129
+ conditional_type = conditional_option_for(attribute)
130
+ return true if conditional_type.nil?
131
+
132
+ # Get cached conditional processor
133
+ conditional = conditionals_for_attributes[attribute]
134
+ return true if conditional.nil?
135
+
136
+ # Evaluate condition with raw data
137
+ # The processor's evaluate_condition already handles if/unless logic
138
+ conditional.evaluate_condition(data)
139
+ end
140
+
94
141
  # Returns collection of attributes for this context
95
142
  # Must be implemented in subclasses
96
143
  #
@@ -119,6 +166,40 @@ module Treaty
119
166
  end
120
167
  end
121
168
 
169
+ # Gets cached conditional processors for attributes or builds them
170
+ #
171
+ # @return [Hash] Hash of attribute => conditional processor
172
+ def conditionals_for_attributes
173
+ @conditionals_for_attributes ||= build_conditionals_for_attributes
174
+ end
175
+
176
+ # Builds conditional processors for attributes with :if or :unless option
177
+ # Validates schema at definition time for performance
178
+ #
179
+ # @return [Hash] Hash of attribute => conditional processor
180
+ def build_conditionals_for_attributes # rubocop:disable Metrics/MethodLength
181
+ collection_of_attributes.each_with_object({}) do |attribute, cache|
182
+ # Get conditional option name (:if or :unless)
183
+ conditional_type = conditional_option_for(attribute)
184
+ next if conditional_type.nil?
185
+
186
+ processor_class = Option::Registry.processor_for(conditional_type)
187
+ next if processor_class.nil?
188
+
189
+ # Create processor instance
190
+ conditional = processor_class.new(
191
+ attribute_name: attribute.name,
192
+ attribute_type: attribute.type,
193
+ option_schema: attribute.options.fetch(conditional_type)
194
+ )
195
+
196
+ # Validate schema at definition time (not runtime)
197
+ conditional.validate_schema!
198
+
199
+ cache[attribute] = conditional
200
+ end
201
+ end
202
+
122
203
  # Validates and transforms a single attribute
123
204
  # Handles both nested and regular attributes
124
205
  #
data/lib/treaty/result.rb CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Treaty
4
4
  class Result
5
- attr_reader :data, :status
5
+ attr_reader :data, :status, :version
6
6
 
7
- def initialize(data:, status:)
7
+ def initialize(data:, status:, version:)
8
8
  @data = data
9
9
  @status = status
10
+ @version = version
10
11
  end
11
12
 
12
13
  def inspect
@@ -16,7 +17,7 @@ module Treaty
16
17
  private
17
18
 
18
19
  def draw_result
19
- "@data=#{@data.inspect}, @status=#{@status.inspect}"
20
+ "@data=#{@data.inspect}, @status=#{@status.inspect}, @version=#{@version.inspect}"
20
21
  end
21
22
  end
22
23
  end
@@ -3,7 +3,7 @@
3
3
  module Treaty
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 13
6
+ MINOR = 15
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
 
@@ -34,7 +34,8 @@ module Treaty
34
34
 
35
35
  Treaty::Result.new(
36
36
  data: validated_response,
37
- status:
37
+ status:,
38
+ version: version_factory.version.version
38
39
  )
39
40
  end
40
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: treaty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Sokolov
@@ -155,6 +155,9 @@ files:
155
155
  - lib/treaty/attribute/entity/builder.rb
156
156
  - lib/treaty/attribute/helper_mapper.rb
157
157
  - lib/treaty/attribute/option/base.rb
158
+ - lib/treaty/attribute/option/conditionals/base.rb
159
+ - lib/treaty/attribute/option/conditionals/if_conditional.rb
160
+ - lib/treaty/attribute/option/conditionals/unless_conditional.rb
158
161
  - lib/treaty/attribute/option/modifiers/as_modifier.rb
159
162
  - lib/treaty/attribute/option/modifiers/cast_modifier.rb
160
163
  - lib/treaty/attribute/option/modifiers/default_modifier.rb