treaty 0.11.0 → 0.13.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 +4 -4
- data/README.md +14 -3
- data/config/locales/en.yml +32 -5
- data/lib/treaty/attribute/option/base.rb +3 -3
- data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +244 -0
- data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +111 -0
- data/lib/treaty/attribute/option/registry_initializer.rb +4 -0
- data/lib/treaty/attribute/option_normalizer.rb +2 -1
- data/lib/treaty/attribute/validation/base.rb +6 -24
- data/lib/treaty/attribute/validation/nested_transformer.rb +7 -6
- data/lib/treaty/configuration.rb +1 -1
- data/lib/treaty/context/callable.rb +7 -5
- data/lib/treaty/context/workspace.rb +4 -0
- data/lib/treaty/controller/dsl.rb +25 -3
- data/lib/treaty/exceptions/base.rb +0 -1
- data/lib/treaty/exceptions/inventory.rb +70 -0
- data/lib/treaty/exceptions/method_name.rb +0 -2
- data/lib/treaty/executor/inventory.rb +122 -0
- data/lib/treaty/info/rest/builder.rb +0 -1
- data/lib/treaty/inventory/collection.rb +71 -0
- data/lib/treaty/inventory/factory.rb +91 -0
- data/lib/treaty/inventory/inventory.rb +92 -0
- data/lib/treaty/request/validator.rb +2 -7
- data/lib/treaty/response/validator.rb +1 -5
- data/lib/treaty/version.rb +1 -1
- data/lib/treaty/versions/execution/request.rb +29 -4
- data/lib/treaty/versions/factory.rb +0 -6
- data/lib/treaty/versions/workspace.rb +3 -1
- metadata +8 -3
- data/lib/treaty/exceptions/strategy.rb +0 -63
- data/lib/treaty/strategy.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 600a422c9ddcdff83b3ec4cde1c8124adb8ae52e1c1d88744d8474bc4afa8c68
|
|
4
|
+
data.tar.gz: dd425b19d451b34f46b07747586f35ba7df4dcac9c554a481ac5dd27e1fe2141
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1739edd5d1c38fe70fc6ab892b06ec15df3a665724df68c95f351675dae4e8c6a00368d65489bbfde0900a67841b9aad1e7eb57f81b948248ae902755f485519
|
|
7
|
+
data.tar.gz: 627d321a706c846e3249afb9e97f4ceb48573af52fcf962459e38316ed76ed7d979c4fd0fcffa8146cff5e7cfeab36b4cf391f9bfb3d7c82ca8de3cd6c5ea439
|
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.
|
|
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.
|
|
17
17
|
|
|
18
18
|
## 📚 Documentation
|
|
19
19
|
|
|
@@ -36,6 +36,7 @@ Treaty provides a complete solution for building versioned APIs in Ruby on Rails
|
|
|
36
36
|
- **Entity Classes (DTOs)** - Define reusable data transfer objects for better code organization
|
|
37
37
|
- **Built-in Validation** - Validate incoming requests and outgoing responses automatically
|
|
38
38
|
- **Data Transformation** - Transform data seamlessly between different API versions
|
|
39
|
+
- **Inventory System** - Pass controller-specific data to services efficiently
|
|
39
40
|
- **Deprecation Management** - Mark versions as deprecated with flexible conditions
|
|
40
41
|
- **Internationalization** - Full I18n support for multilingual error messages
|
|
41
42
|
- **Well-documented** - Comprehensive guides and examples for every feature
|
|
@@ -64,8 +65,6 @@ Create your first API contract in `app/treaties/posts/create_treaty.rb`:
|
|
|
64
65
|
module Posts
|
|
65
66
|
class CreateTreaty < ApplicationTreaty
|
|
66
67
|
version 1, default: true do
|
|
67
|
-
strategy Treaty::Strategy::ADAPTER
|
|
68
|
-
|
|
69
68
|
request do
|
|
70
69
|
object :post do
|
|
71
70
|
string :title
|
|
@@ -102,6 +101,18 @@ class PostsController < ApplicationController
|
|
|
102
101
|
# 3. Validates service response according to response definition
|
|
103
102
|
# 4. Returns transformed data to client
|
|
104
103
|
treaty :create
|
|
104
|
+
|
|
105
|
+
# Optional: Provide additional data from controller to service
|
|
106
|
+
treaty :index do
|
|
107
|
+
provide :current_user
|
|
108
|
+
provide :posts, from: :load_posts
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def load_posts
|
|
114
|
+
Post.published.limit(10)
|
|
115
|
+
end
|
|
105
116
|
end
|
|
106
117
|
```
|
|
107
118
|
|
data/config/locales/en.yml
CHANGED
|
@@ -49,6 +49,17 @@ en:
|
|
|
49
49
|
as:
|
|
50
50
|
invalid_type: "Option 'as' for attribute '%{attribute}' must be a Symbol. Got: %{type}"
|
|
51
51
|
|
|
52
|
+
transform:
|
|
53
|
+
invalid_type: "Option 'transform' for attribute '%{attribute}' must be a Proc or Lambda. Got: %{type}"
|
|
54
|
+
execution_error: "Transform failed for attribute '%{attribute}': %{error}"
|
|
55
|
+
|
|
56
|
+
cast:
|
|
57
|
+
invalid_type: "Option 'cast' for attribute '%{attribute}' must be a Symbol. Got: %{type}"
|
|
58
|
+
source_not_supported: "Option 'cast' for attribute '%{attribute}' cannot be used with type '%{source_type}'. Casting is only supported for: %{allowed}"
|
|
59
|
+
target_not_supported: "Option 'cast' for attribute '%{attribute}' cannot cast to '%{target_type}'. Supported target types: %{allowed}"
|
|
60
|
+
conversion_not_supported: "Option 'cast' for attribute '%{attribute}' does not support conversion from '%{from}' to '%{to}'"
|
|
61
|
+
conversion_error: "Cast failed for attribute '%{attribute}' from '%{from}' to '%{to}'. Value: '%{value}'. Error: %{error}"
|
|
62
|
+
|
|
52
63
|
# Attribute builder DSL
|
|
53
64
|
builder:
|
|
54
65
|
not_implemented: "%{class} must implement #create_attribute"
|
|
@@ -91,14 +102,10 @@ en:
|
|
|
91
102
|
# Version factory
|
|
92
103
|
factory:
|
|
93
104
|
invalid_default_option: "Default option for version must be true, false, or a Proc, got: %{type}"
|
|
94
|
-
unknown_method: "Unknown method '%{method}' in version definition. Available methods: summary,
|
|
105
|
+
unknown_method: "Unknown method '%{method}' in version definition. Available methods: summary, deprecated, request, response, delegate_to"
|
|
95
106
|
default_deprecated_conflict: "Version %{version} cannot be both default and deprecated. A default version must be active and usable. Either remove 'default: true' or remove the 'deprecated' declaration."
|
|
96
107
|
multiple_defaults: "Cannot have multiple versions marked as default. Only one version can be the default. Please review your treaty definition and ensure only one version has 'default: true'."
|
|
97
108
|
|
|
98
|
-
# Strategy validation
|
|
99
|
-
strategy:
|
|
100
|
-
unknown: "Unknown strategy: %{strategy}"
|
|
101
|
-
|
|
102
109
|
# ============================================================================
|
|
103
110
|
# Execution: Service and executor invocation
|
|
104
111
|
# ============================================================================
|
|
@@ -120,3 +127,23 @@ en:
|
|
|
120
127
|
# ============================================================================
|
|
121
128
|
controller:
|
|
122
129
|
treaty_class_not_found: "%{class_name}"
|
|
130
|
+
|
|
131
|
+
# ============================================================================
|
|
132
|
+
# Inventory: Controller data provisioning system
|
|
133
|
+
# ============================================================================
|
|
134
|
+
inventory:
|
|
135
|
+
# Factory DSL errors
|
|
136
|
+
unknown_method: "Unknown method '%{method}' in treaty block for action '%{action}'. Only 'provide' method is supported. Use: provide :name, from: :source OR provide :name"
|
|
137
|
+
name_must_be_symbol: "Inventory name must be a Symbol, got %{name}. Use: provide :name, from: :source OR provide :name"
|
|
138
|
+
|
|
139
|
+
# Inventory validation errors
|
|
140
|
+
invalid_name: "Inventory name must be a non-empty Symbol, got %{name}"
|
|
141
|
+
source_required: "Inventory source cannot be nil. Provide a Symbol (method name), Proc/Lambda, or direct value"
|
|
142
|
+
evaluation_error: "Failed to evaluate inventory item '%{name}': %{error}"
|
|
143
|
+
|
|
144
|
+
# ============================================================================
|
|
145
|
+
# Executor: Inventory executor wrapper
|
|
146
|
+
# ============================================================================
|
|
147
|
+
executor:
|
|
148
|
+
inventory:
|
|
149
|
+
item_not_found: "Inventory item '%{name}' not found. Available items: %{available}"
|
|
@@ -144,14 +144,14 @@ module Treaty
|
|
|
144
144
|
# Resolves custom message with lambda support
|
|
145
145
|
# If message is a lambda, calls it with provided named arguments
|
|
146
146
|
#
|
|
147
|
-
# @param
|
|
147
|
+
# @param attributes [Hash] Named arguments to pass to lambda
|
|
148
148
|
# @return [String, nil] Resolved message string or nil
|
|
149
|
-
def resolve_custom_message(**
|
|
149
|
+
def resolve_custom_message(**attributes)
|
|
150
150
|
message = custom_message
|
|
151
151
|
return nil if message.nil?
|
|
152
152
|
|
|
153
153
|
if message.respond_to?(:call)
|
|
154
|
-
message.call(**
|
|
154
|
+
message.call(**attributes)
|
|
155
155
|
else
|
|
156
156
|
message
|
|
157
157
|
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Option
|
|
6
|
+
module Modifiers
|
|
7
|
+
# Converts attribute values between different types automatically.
|
|
8
|
+
#
|
|
9
|
+
# ## Usage Examples
|
|
10
|
+
#
|
|
11
|
+
# Simple mode:
|
|
12
|
+
# string :created_at, cast: :datetime
|
|
13
|
+
# datetime :timestamp, cast: :string
|
|
14
|
+
# integer :active, cast: :boolean
|
|
15
|
+
#
|
|
16
|
+
# Advanced mode with custom error message:
|
|
17
|
+
# string :created_at, cast: {
|
|
18
|
+
# to: :datetime,
|
|
19
|
+
# message: "Invalid date format"
|
|
20
|
+
# }
|
|
21
|
+
#
|
|
22
|
+
# ## Use Cases
|
|
23
|
+
#
|
|
24
|
+
# 1. **Request type conversion**:
|
|
25
|
+
# ```ruby
|
|
26
|
+
# request do
|
|
27
|
+
# string :created_at, cast: :datetime
|
|
28
|
+
# end
|
|
29
|
+
# # Input: { created_at: "2024-01-15T10:30:00Z" }
|
|
30
|
+
# # Service receives: { created_at: DateTime object }
|
|
31
|
+
# ```
|
|
32
|
+
#
|
|
33
|
+
# 2. **Response type conversion**:
|
|
34
|
+
# ```ruby
|
|
35
|
+
# response 200 do
|
|
36
|
+
# datetime :created_at, cast: :string
|
|
37
|
+
# end
|
|
38
|
+
# # Service returns: { created_at: DateTime object }
|
|
39
|
+
# # Output: { created_at: "2024-01-15T10:30:00Z" }
|
|
40
|
+
# ```
|
|
41
|
+
#
|
|
42
|
+
# 3. **Unix timestamp conversion**:
|
|
43
|
+
# ```ruby
|
|
44
|
+
# integer :timestamp, cast: :datetime
|
|
45
|
+
# datetime :created_at, cast: :integer
|
|
46
|
+
# ```
|
|
47
|
+
#
|
|
48
|
+
# ## Supported Conversions
|
|
49
|
+
#
|
|
50
|
+
# ### From Integer
|
|
51
|
+
# - integer -> string: Converts to string representation
|
|
52
|
+
# - integer -> boolean: 0 = false, non-zero = true
|
|
53
|
+
# - integer -> datetime: Treats as Unix timestamp
|
|
54
|
+
#
|
|
55
|
+
# ### From String
|
|
56
|
+
# - string -> integer: Parses integer from string
|
|
57
|
+
# - string -> boolean: Parses truthy/falsy strings (true/false, yes/no, 1/0, on/off)
|
|
58
|
+
# - string -> datetime: Parses datetime string (ISO8601, RFC3339, etc.)
|
|
59
|
+
#
|
|
60
|
+
# ### From Boolean
|
|
61
|
+
# - boolean -> string: Converts to "true" or "false"
|
|
62
|
+
# - boolean -> integer: true = 1, false = 0
|
|
63
|
+
#
|
|
64
|
+
# ### From DateTime
|
|
65
|
+
# - datetime -> string: Converts to ISO8601 format
|
|
66
|
+
# - datetime -> integer: Converts to Unix timestamp
|
|
67
|
+
#
|
|
68
|
+
# ## Important Notes
|
|
69
|
+
#
|
|
70
|
+
# - Cast option only works with scalar types (integer, string, boolean, datetime)
|
|
71
|
+
# - Array and Object types are not supported for casting
|
|
72
|
+
# - Casting to the same type is allowed (no-op)
|
|
73
|
+
# - Nil values are not transformed (handled by RequiredValidator)
|
|
74
|
+
# - All conversion errors are caught and re-raised as Validation errors
|
|
75
|
+
#
|
|
76
|
+
# ## Error Handling
|
|
77
|
+
#
|
|
78
|
+
# If conversion fails (e.g., invalid date string, non-numeric string to integer),
|
|
79
|
+
# the error is caught and converted to a Treaty::Exceptions::Validation error.
|
|
80
|
+
#
|
|
81
|
+
# ## Advanced Mode
|
|
82
|
+
#
|
|
83
|
+
# Schema format: `{ to: :target_type, message: "Custom error" }`
|
|
84
|
+
# Note: Uses `:to` key instead of the default `:is` key.
|
|
85
|
+
class CastModifier < Treaty::Attribute::Option::Base
|
|
86
|
+
# Types that support casting (scalar types only)
|
|
87
|
+
ALLOWED_CAST_TYPES = %i[integer string boolean datetime].freeze
|
|
88
|
+
|
|
89
|
+
# Validates that cast option is correctly configured
|
|
90
|
+
#
|
|
91
|
+
# @raise [Treaty::Exceptions::Validation] If cast configuration is invalid
|
|
92
|
+
# @return [void]
|
|
93
|
+
def validate_schema! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
94
|
+
# If option_schema is nil, cast is not used for this attribute
|
|
95
|
+
return if @option_schema.nil?
|
|
96
|
+
|
|
97
|
+
target_type = option_value
|
|
98
|
+
|
|
99
|
+
# Validate that target type is a Symbol
|
|
100
|
+
unless target_type.is_a?(Symbol)
|
|
101
|
+
raise Treaty::Exceptions::Validation,
|
|
102
|
+
I18n.t(
|
|
103
|
+
"treaty.attributes.modifiers.cast.invalid_type",
|
|
104
|
+
attribute: @attribute_name,
|
|
105
|
+
type: target_type.class
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validate that source type supports casting
|
|
110
|
+
unless ALLOWED_CAST_TYPES.include?(@attribute_type)
|
|
111
|
+
raise Treaty::Exceptions::Validation,
|
|
112
|
+
I18n.t(
|
|
113
|
+
"treaty.attributes.modifiers.cast.source_not_supported",
|
|
114
|
+
attribute: @attribute_name,
|
|
115
|
+
source_type: @attribute_type,
|
|
116
|
+
allowed: ALLOWED_CAST_TYPES.join(", ")
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Validate that target type is allowed
|
|
121
|
+
unless ALLOWED_CAST_TYPES.include?(target_type)
|
|
122
|
+
raise Treaty::Exceptions::Validation,
|
|
123
|
+
I18n.t(
|
|
124
|
+
"treaty.attributes.modifiers.cast.target_not_supported",
|
|
125
|
+
attribute: @attribute_name,
|
|
126
|
+
target_type:,
|
|
127
|
+
allowed: ALLOWED_CAST_TYPES.join(", ")
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Validate that conversion from source to target is supported
|
|
132
|
+
return if conversion_supported?(@attribute_type, target_type)
|
|
133
|
+
|
|
134
|
+
raise Treaty::Exceptions::Validation,
|
|
135
|
+
I18n.t(
|
|
136
|
+
"treaty.attributes.modifiers.cast.conversion_not_supported",
|
|
137
|
+
attribute: @attribute_name,
|
|
138
|
+
from: @attribute_type,
|
|
139
|
+
to: target_type
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Applies type conversion to the value
|
|
144
|
+
# Skips conversion for nil values (handled by RequiredValidator)
|
|
145
|
+
#
|
|
146
|
+
# @param value [Object] The current value
|
|
147
|
+
# @return [Object] Converted value
|
|
148
|
+
def transform_value(value) # rubocop:disable Metrics/MethodLength
|
|
149
|
+
return value if value.nil? # Cast doesn't modify nil, required validator handles it.
|
|
150
|
+
|
|
151
|
+
target_type = option_value
|
|
152
|
+
conversion_lambda = conversion_matrix.dig(@attribute_type, target_type)
|
|
153
|
+
|
|
154
|
+
# Call conversion lambda
|
|
155
|
+
conversion_lambda.call(value:)
|
|
156
|
+
rescue StandardError => e
|
|
157
|
+
attributes = {
|
|
158
|
+
attribute: @attribute_name,
|
|
159
|
+
from: @attribute_type,
|
|
160
|
+
to: target_type,
|
|
161
|
+
value:,
|
|
162
|
+
error: e.message
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Catch all exceptions from conversion execution
|
|
166
|
+
error_message = resolve_custom_message(**attributes) || I18n.t(
|
|
167
|
+
"treaty.attributes.modifiers.cast.conversion_error",
|
|
168
|
+
**attributes
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
raise Treaty::Exceptions::Validation, error_message
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
protected
|
|
175
|
+
|
|
176
|
+
# Override value_key to use :to instead of :is
|
|
177
|
+
# This makes advanced mode syntax: cast: { to: :datetime }
|
|
178
|
+
#
|
|
179
|
+
# @return [Symbol] The key :to
|
|
180
|
+
def value_key
|
|
181
|
+
:to
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
# Checks if conversion from source type to target type is supported
|
|
187
|
+
#
|
|
188
|
+
# @param from_type [Symbol] Source type
|
|
189
|
+
# @param to_type [Symbol] Target type
|
|
190
|
+
# @return [Boolean] True if conversion is supported
|
|
191
|
+
def conversion_supported?(from_type, to_type)
|
|
192
|
+
conversion_matrix.dig(from_type, to_type).present?
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Matrix of all supported type conversions
|
|
196
|
+
# Maps from_type => to_type => conversion_lambda
|
|
197
|
+
#
|
|
198
|
+
# @return [Hash] Conversion matrix
|
|
199
|
+
def conversion_matrix # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
200
|
+
@conversion_matrix ||= {
|
|
201
|
+
integer: {
|
|
202
|
+
integer: ->(value:) { value }, # No-op for same type
|
|
203
|
+
string: ->(value:) { value.to_s },
|
|
204
|
+
boolean: ->(value:) { value != 0 },
|
|
205
|
+
datetime: ->(value:) { Time.at(value) }
|
|
206
|
+
},
|
|
207
|
+
string: {
|
|
208
|
+
string: ->(value:) { value }, # No-op for same type
|
|
209
|
+
integer: ->(value:) { Integer(value) },
|
|
210
|
+
boolean: ->(value:) { parse_boolean(value) },
|
|
211
|
+
datetime: ->(value:) { DateTime.parse(value) }
|
|
212
|
+
},
|
|
213
|
+
boolean: {
|
|
214
|
+
boolean: ->(value:) { value }, # No-op for same type
|
|
215
|
+
string: ->(value:) { value.to_s },
|
|
216
|
+
integer: ->(value:) { value ? 1 : 0 }
|
|
217
|
+
},
|
|
218
|
+
datetime: {
|
|
219
|
+
datetime: ->(value:) { value }, # No-op for same type
|
|
220
|
+
string: ->(value:) { value.iso8601 },
|
|
221
|
+
integer: ->(value:) { value.to_i }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Parses a string value into a boolean
|
|
227
|
+
# Recognizes: true/false, yes/no, 1/0, on/off (case-insensitive)
|
|
228
|
+
#
|
|
229
|
+
# @param value [String] The string value to parse
|
|
230
|
+
# @return [Boolean] Parsed boolean value
|
|
231
|
+
# @raise [ArgumentError] If string is not a recognized boolean value
|
|
232
|
+
def parse_boolean(value)
|
|
233
|
+
normalized = value.to_s.downcase.strip
|
|
234
|
+
|
|
235
|
+
return true if %w[true 1 yes on].include?(normalized)
|
|
236
|
+
return false if %w[false 0 no off].include?(normalized)
|
|
237
|
+
|
|
238
|
+
raise ArgumentError, "Cannot convert '#{value}' to boolean"
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Option
|
|
6
|
+
module Modifiers
|
|
7
|
+
# Transforms attribute values using custom lambda functions.
|
|
8
|
+
#
|
|
9
|
+
# ## Usage Examples
|
|
10
|
+
#
|
|
11
|
+
# Simple mode:
|
|
12
|
+
# integer :amount, transform: ->(value:) { value * 100 }
|
|
13
|
+
# string :title, transform: ->(value:) { value.strip.upcase }
|
|
14
|
+
#
|
|
15
|
+
# Advanced mode with custom error message:
|
|
16
|
+
# integer :amount, transform: {
|
|
17
|
+
# is: ->(value:) { value * 100 },
|
|
18
|
+
# message: "Failed to transform amount"
|
|
19
|
+
# }
|
|
20
|
+
#
|
|
21
|
+
# ## Use Cases
|
|
22
|
+
#
|
|
23
|
+
# 1. **Request transformation**:
|
|
24
|
+
# ```ruby
|
|
25
|
+
# request do
|
|
26
|
+
# integer :amount_cents, transform: ->(value:) { value * 100 }
|
|
27
|
+
# end
|
|
28
|
+
# # Input: { amount_cents: 10 }
|
|
29
|
+
# # Service receives: { amount_cents: 1000 }
|
|
30
|
+
# ```
|
|
31
|
+
#
|
|
32
|
+
# 2. **Response transformation**:
|
|
33
|
+
# ```ruby
|
|
34
|
+
# response 200 do
|
|
35
|
+
# string :title, transform: ->(value:) { value.titleize }
|
|
36
|
+
# end
|
|
37
|
+
# # Service returns: { title: "hello world" }
|
|
38
|
+
# # Output: { title: "Hello World" }
|
|
39
|
+
# ```
|
|
40
|
+
#
|
|
41
|
+
# 3. **Complex transformations**:
|
|
42
|
+
# ```ruby
|
|
43
|
+
# string :email, transform: ->(value:) { value.downcase.strip }
|
|
44
|
+
# datetime :timestamp, transform: ->(value:) { value.iso8601 }
|
|
45
|
+
# ```
|
|
46
|
+
#
|
|
47
|
+
# ## Important Notes
|
|
48
|
+
#
|
|
49
|
+
# - Lambda must accept named argument `value:`
|
|
50
|
+
# - All exceptions raised in lambda are caught and re-raised as Validation errors
|
|
51
|
+
# - Transformation is applied during Phase 3 (after validation)
|
|
52
|
+
# - Can be combined with other options (required, default, as, etc.)
|
|
53
|
+
#
|
|
54
|
+
# ## Error Handling
|
|
55
|
+
#
|
|
56
|
+
# If the lambda raises any exception, it's caught and converted to a
|
|
57
|
+
# Treaty::Exceptions::Validation with appropriate error message.
|
|
58
|
+
#
|
|
59
|
+
# ## Advanced Mode
|
|
60
|
+
#
|
|
61
|
+
# Schema format: `{ is: lambda, message: nil }`
|
|
62
|
+
class TransformModifier < Treaty::Attribute::Option::Base
|
|
63
|
+
# Validates that transform value is a lambda
|
|
64
|
+
#
|
|
65
|
+
# @raise [Treaty::Exceptions::Validation] If transform is not a Proc/lambda
|
|
66
|
+
# @return [void]
|
|
67
|
+
def validate_schema!
|
|
68
|
+
transform_lambda = option_value
|
|
69
|
+
|
|
70
|
+
return if transform_lambda.respond_to?(:call)
|
|
71
|
+
|
|
72
|
+
raise Treaty::Exceptions::Validation,
|
|
73
|
+
I18n.t(
|
|
74
|
+
"treaty.attributes.modifiers.transform.invalid_type",
|
|
75
|
+
attribute: @attribute_name,
|
|
76
|
+
type: transform_lambda.class
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Applies transformation to the value using the provided lambda
|
|
81
|
+
# Catches all exceptions and re-raises as Validation errors
|
|
82
|
+
# Skips transformation for nil values (handled by RequiredValidator)
|
|
83
|
+
#
|
|
84
|
+
# @param value [Object] The current value
|
|
85
|
+
# @return [Object] Transformed value
|
|
86
|
+
def transform_value(value) # rubocop:disable Metrics/MethodLength
|
|
87
|
+
return value if value.nil? # Transform doesn't modify nil, required validator handles it.
|
|
88
|
+
|
|
89
|
+
transform_lambda = option_value
|
|
90
|
+
|
|
91
|
+
# Call lambda with named argument
|
|
92
|
+
transform_lambda.call(value:)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
attributes = {
|
|
95
|
+
attribute: @attribute_name,
|
|
96
|
+
error: e.message
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Catch all exceptions from lambda execution
|
|
100
|
+
error_message = resolve_custom_message(**attributes) || I18n.t(
|
|
101
|
+
"treaty.attributes.modifiers.transform.execution_error",
|
|
102
|
+
**attributes
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
raise Treaty::Exceptions::Validation, error_message
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -27,6 +27,8 @@ module Treaty
|
|
|
27
27
|
#
|
|
28
28
|
# - `:as` → AsModifier - Renames attributes
|
|
29
29
|
# - `:default` → DefaultModifier - Provides default values
|
|
30
|
+
# - `:transform` → TransformModifier - Transforms values using custom lambdas
|
|
31
|
+
# - `:cast` → CastModifier - Converts values between types automatically
|
|
30
32
|
#
|
|
31
33
|
# ## Auto-Registration
|
|
32
34
|
#
|
|
@@ -81,6 +83,8 @@ module Treaty
|
|
|
81
83
|
def register_modifiers!
|
|
82
84
|
Registry.register(:as, Modifiers::AsModifier, category: :modifier)
|
|
83
85
|
Registry.register(:default, Modifiers::DefaultModifier, category: :modifier)
|
|
86
|
+
Registry.register(:transform, Modifiers::TransformModifier, category: :modifier)
|
|
87
|
+
Registry.register(:cast, Modifiers::CastModifier, category: :modifier)
|
|
84
88
|
end
|
|
85
89
|
end
|
|
86
90
|
end
|
|
@@ -70,7 +70,8 @@ module Treaty
|
|
|
70
70
|
OPTION_KEY_MAPPING = {
|
|
71
71
|
in: { advanced_key: :inclusion, value_key: :in },
|
|
72
72
|
as: { advanced_key: :as, value_key: :is },
|
|
73
|
-
default: { advanced_key: :default, value_key: :is }
|
|
73
|
+
default: { advanced_key: :default, value_key: :is },
|
|
74
|
+
cast: { advanced_key: :cast, value_key: :to }
|
|
74
75
|
}.freeze
|
|
75
76
|
private_constant :OPTION_KEY_MAPPING
|
|
76
77
|
|
|
@@ -3,18 +3,17 @@
|
|
|
3
3
|
module Treaty
|
|
4
4
|
module Attribute
|
|
5
5
|
module Validation
|
|
6
|
-
# Base class for
|
|
6
|
+
# Base class for request and response validation.
|
|
7
7
|
#
|
|
8
8
|
# ## Purpose
|
|
9
9
|
#
|
|
10
|
-
# Provides common interface for validation
|
|
11
|
-
# Subclasses implement specific validation logic for
|
|
10
|
+
# Provides common interface for validation used in Treaty.
|
|
11
|
+
# Subclasses implement specific validation logic for requests and responses.
|
|
12
12
|
#
|
|
13
13
|
# ## Responsibilities
|
|
14
14
|
#
|
|
15
|
-
# 1. **
|
|
15
|
+
# 1. **Validation Interface** - Defines common validation interface
|
|
16
16
|
# 2. **Factory Pattern** - Provides class-level validate! method
|
|
17
|
-
# 3. **Strategy Detection** - Checks if adapter strategy is active
|
|
18
17
|
#
|
|
19
18
|
# ## Subclasses
|
|
20
19
|
#
|
|
@@ -29,14 +28,6 @@ module Treaty
|
|
|
29
28
|
# Example usage:
|
|
30
29
|
# Request::Validation.validate!(version_factory: factory, data: params)
|
|
31
30
|
#
|
|
32
|
-
# ## Strategy Pattern
|
|
33
|
-
#
|
|
34
|
-
# The validation system supports two strategies:
|
|
35
|
-
# - **Adapter Strategy** - Adapts between different API versions
|
|
36
|
-
# - **Standard Strategy** - Direct version handling
|
|
37
|
-
#
|
|
38
|
-
# This base class provides `adapter_strategy?` helper to check current strategy.
|
|
39
|
-
#
|
|
40
31
|
# ## Factory Method
|
|
41
32
|
#
|
|
42
33
|
# The `self.validate!(...)` class method provides a convenient factory pattern:
|
|
@@ -49,7 +40,7 @@ module Treaty
|
|
|
49
40
|
# ## Architecture
|
|
50
41
|
#
|
|
51
42
|
# Works with:
|
|
52
|
-
# - VersionFactory - Provides version
|
|
43
|
+
# - VersionFactory - Provides version information
|
|
53
44
|
# - Orchestrator::Base - Performs actual validation and transformation
|
|
54
45
|
class Base
|
|
55
46
|
# Class-level factory method for validation
|
|
@@ -63,7 +54,7 @@ module Treaty
|
|
|
63
54
|
|
|
64
55
|
# Creates a new validation instance
|
|
65
56
|
#
|
|
66
|
-
# @param version_factory [VersionFactory] Factory containing version
|
|
57
|
+
# @param version_factory [VersionFactory] Factory containing version information
|
|
67
58
|
def initialize(version_factory:)
|
|
68
59
|
@version_factory = version_factory
|
|
69
60
|
end
|
|
@@ -77,15 +68,6 @@ module Treaty
|
|
|
77
68
|
raise Treaty::Exceptions::Validation,
|
|
78
69
|
I18n.t("treaty.attributes.validators.nested.orchestrator.collection_not_implemented")
|
|
79
70
|
end
|
|
80
|
-
|
|
81
|
-
private
|
|
82
|
-
|
|
83
|
-
# Checks if adapter strategy is active
|
|
84
|
-
#
|
|
85
|
-
# @return [Boolean] True if using adapter strategy
|
|
86
|
-
def adapter_strategy?
|
|
87
|
-
@version_factory.strategy_instance.adapter?
|
|
88
|
-
end
|
|
89
71
|
end
|
|
90
72
|
end
|
|
91
73
|
end
|
|
@@ -138,8 +138,7 @@ module Treaty
|
|
|
138
138
|
def transform(value)
|
|
139
139
|
value.each_with_index.map do |item, index|
|
|
140
140
|
if simple_array?
|
|
141
|
-
|
|
142
|
-
item
|
|
141
|
+
transform_simple_element(item, index)
|
|
143
142
|
else
|
|
144
143
|
transform_array_item(item, index)
|
|
145
144
|
end
|
|
@@ -156,19 +155,21 @@ module Treaty
|
|
|
156
155
|
attribute.collection_of_attributes.first.name == SELF_OBJECT
|
|
157
156
|
end
|
|
158
157
|
|
|
159
|
-
#
|
|
158
|
+
# Transforms a simple array element (primitive value)
|
|
159
|
+
# Validates and applies transformations to the element
|
|
160
160
|
#
|
|
161
|
-
# @param item [Object] Array element to
|
|
161
|
+
# @param item [Object] Array element to transform
|
|
162
162
|
# @param index [Integer] Element index for error messages
|
|
163
163
|
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
164
|
-
# @return [
|
|
165
|
-
def
|
|
164
|
+
# @return [Object] Transformed element value
|
|
165
|
+
def transform_simple_element(item, index) # rubocop:disable Metrics/MethodLength
|
|
166
166
|
self_attr = attribute.collection_of_attributes.first
|
|
167
167
|
validator = AttributeValidator.new(self_attr)
|
|
168
168
|
validator.validate_schema!
|
|
169
169
|
|
|
170
170
|
begin
|
|
171
171
|
validator.validate_value!(item)
|
|
172
|
+
validator.transform_value(item)
|
|
172
173
|
rescue Treaty::Exceptions::Validation => e
|
|
173
174
|
raise Treaty::Exceptions::Validation,
|
|
174
175
|
I18n.t(
|
data/lib/treaty/configuration.rb
CHANGED
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
module Treaty
|
|
4
4
|
module Context
|
|
5
5
|
module Callable
|
|
6
|
-
def call!(version:, params:)
|
|
7
|
-
|
|
6
|
+
def call!(version:, params:, context: nil, inventory: nil)
|
|
7
|
+
treaty_instance = send(:new)
|
|
8
8
|
|
|
9
|
-
_call!(
|
|
9
|
+
_call!(treaty_instance, context:, inventory:, version:, params:)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
|
-
def _call!(
|
|
15
|
-
|
|
14
|
+
def _call!(treaty_instance, context:, inventory:, version:, params:)
|
|
15
|
+
treaty_instance.send(
|
|
16
16
|
:_call!,
|
|
17
|
+
context:,
|
|
18
|
+
inventory:,
|
|
17
19
|
version:,
|
|
18
20
|
params:,
|
|
19
21
|
collection_of_versions:
|