castkit 0.1.2 → 0.2.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/.rspec_status +196 -219
- data/CHANGELOG.md +42 -0
- data/README.md +469 -84
- data/lib/castkit/attribute.rb +6 -24
- data/lib/castkit/castkit.rb +58 -10
- data/lib/castkit/configuration.rb +94 -47
- data/lib/castkit/contract/data_object.rb +62 -0
- data/lib/castkit/contract/generic.rb +168 -0
- data/lib/castkit/contract/result.rb +74 -0
- data/lib/castkit/contract/validator.rb +248 -0
- data/lib/castkit/contract.rb +67 -0
- data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
- data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
- data/lib/castkit/core/config.rb +74 -0
- data/lib/castkit/core/registerable.rb +59 -0
- data/lib/castkit/data_object.rb +45 -60
- data/lib/castkit/default_serializer.rb +85 -54
- data/lib/castkit/error.rb +15 -3
- data/lib/castkit/ext/attribute/access.rb +67 -0
- data/lib/castkit/ext/attribute/error_handling.rb +63 -0
- data/lib/castkit/ext/attribute/options.rb +142 -0
- data/lib/castkit/ext/attribute/validation.rb +85 -0
- data/lib/castkit/ext/data_object/contract.rb +96 -0
- data/lib/castkit/ext/data_object/deserialization.rb +167 -0
- data/lib/castkit/ext/data_object/serialization.rb +61 -0
- data/lib/castkit/inflector.rb +47 -0
- data/lib/castkit/types/boolean.rb +43 -0
- data/lib/castkit/types/collection.rb +24 -0
- data/lib/castkit/types/date.rb +34 -0
- data/lib/castkit/types/date_time.rb +34 -0
- data/lib/castkit/types/float.rb +46 -0
- data/lib/castkit/types/generic.rb +123 -0
- data/lib/castkit/types/integer.rb +46 -0
- data/lib/castkit/types/string.rb +44 -0
- data/lib/castkit/types.rb +15 -0
- data/lib/castkit/validators/base_validator.rb +39 -0
- data/lib/castkit/validators/numeric_validator.rb +2 -2
- data/lib/castkit/validators/string_validator.rb +3 -3
- data/lib/castkit/version.rb +1 -1
- data/lib/castkit.rb +2 -0
- metadata +29 -13
- data/lib/castkit/attribute_extensions/access.rb +0 -65
- data/lib/castkit/attribute_extensions/casting.rb +0 -147
- data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
- data/lib/castkit/attribute_extensions/options.rb +0 -131
- data/lib/castkit/attribute_extensions/serialization.rb +0 -89
- data/lib/castkit/attribute_extensions/validation.rb +0 -72
- data/lib/castkit/data_object_extensions/config.rb +0 -113
- data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
- data/lib/castkit/validators.rb +0 -4
data/README.md
CHANGED
@@ -1,189 +1,575 @@
|
|
1
|
-
|
2
1
|
# Castkit
|
3
2
|
|
4
|
-
|
3
|
+
Castkit is a lightweight, type-safe data object system for Ruby. It provides a declarative DSL for defining data transfer objects (DTOs) with built-in support for typecasting, validation, nested data structures, serialization, deserialization, and contract-driven programming.
|
4
|
+
|
5
|
+
Inspired by tools like Jackson (Java) and Python dataclasses, Castkit brings structured data modeling to Ruby in a way that emphasizes:
|
5
6
|
|
6
|
-
|
7
|
+
- **Simplicity**: Minimal API surface and predictable behavior.
|
8
|
+
- **Explicitness**: Every field and type is declared clearly.
|
9
|
+
- **Composition**: Support for nested objects, collections, and modular design.
|
10
|
+
- **Performance**: Fast and efficient with minimal runtime overhead.
|
11
|
+
- **Extensibility**: Easy to extend with custom types, serializers, and integrations.
|
12
|
+
|
13
|
+
Castkit is designed to work seamlessly in service-oriented and API-driven architectures, providing structure without overreach.
|
7
14
|
|
8
15
|
---
|
9
16
|
|
10
|
-
##
|
17
|
+
## 🚀 Features
|
11
18
|
|
12
|
-
-
|
13
|
-
-
|
14
|
-
-
|
15
|
-
-
|
16
|
-
-
|
17
|
-
-
|
18
|
-
-
|
19
|
+
- [Configuration](#configuration)
|
20
|
+
- [Attribute DSL](#attribute-dsl)
|
21
|
+
- [DataObjects](#dataobjects)
|
22
|
+
- [Contracts](#contracts)
|
23
|
+
- [Advance Usage](#advanced-usage-coming-soon)
|
24
|
+
- [Testing](#testing)
|
25
|
+
- [Compatibility](#compatibility)
|
26
|
+
- [License](#license)
|
19
27
|
|
20
28
|
---
|
21
29
|
|
22
|
-
##
|
30
|
+
## Configuration
|
23
31
|
|
24
|
-
|
32
|
+
Castkit provides a global configuration interface to customize behavior across the entire system. You can configure Castkit by passing a block to `Castkit.configure`.
|
25
33
|
|
26
34
|
```ruby
|
27
|
-
|
35
|
+
Castkit.configure do |config|
|
36
|
+
config.enable_warnings = false
|
37
|
+
config.enforce_typing = true
|
38
|
+
end
|
28
39
|
```
|
29
40
|
|
30
|
-
|
41
|
+
### ⚙️ Available Settings
|
42
|
+
|
43
|
+
| Option | Type | Default | Description |
|
44
|
+
|----------------------------|---------|---------|-------------|
|
45
|
+
| `enable_warnings` | Boolean | `true` | Enables runtime warnings for misconfigurations. |
|
46
|
+
| `enforce_typing` | Boolean | `true` | Raises if type mismatch during load (e.g., `true` vs. `"true"`). |
|
47
|
+
| `enforce_attribute_access` | Boolean | `true` | Raises if an unknown access level is defined. |
|
48
|
+
| `enforce_unwrapped_prefix` | Boolean | `true` | Requires `unwrapped: true` when using attribute prefixes. |
|
49
|
+
| `enforce_array_options` | Boolean | `true` | Raises if an array attribute is missing the `of:` option. |
|
50
|
+
| `raise_type_errors` | Boolean | `true` | Raises if an unregistered or invalid type is used. |
|
51
|
+
| `strict_by_default` | Boolean | `true` | Applies `strict: true` by default to all DTOs and Contracts. |
|
52
|
+
|
53
|
+
### 🔧 Type System
|
31
54
|
|
32
|
-
|
33
|
-
|
55
|
+
Castkit comes with built-in support for primitive types and allows registration of custom ones:
|
56
|
+
|
57
|
+
#### Default types
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
{
|
61
|
+
array: Castkit::Types::Collection,
|
62
|
+
boolean: Castkit::Types::Boolean,
|
63
|
+
date: Castkit::Types::Date,
|
64
|
+
datetime: Castkit::Types::DateTime,
|
65
|
+
float: Castkit::Types::Float,
|
66
|
+
hash: Castkit::Types::Generic,
|
67
|
+
integer: Castkit::Types::Integer,
|
68
|
+
string: Castkit::Types::String
|
69
|
+
}
|
34
70
|
```
|
35
71
|
|
36
|
-
|
72
|
+
#### Type Aliases
|
37
73
|
|
38
|
-
|
39
|
-
|
74
|
+
| Alias | Canonical |
|
75
|
+
|------------|-----------|
|
76
|
+
| `collection` | `array` |
|
77
|
+
| `bool` | `boolean` |
|
78
|
+
| `int` | `integer` |
|
79
|
+
| `map` | `hash` |
|
80
|
+
| `number` | `float` |
|
81
|
+
| `str` | `string` |
|
82
|
+
| `timestamp` | `datetime`|
|
83
|
+
| `uuid` | `string` |
|
84
|
+
|
85
|
+
#### Registering Custom Types
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
Castkit.configure do |config|
|
89
|
+
config.register_type(:mytype, MyTypeClass, aliases: [:custom])
|
90
|
+
end
|
40
91
|
```
|
41
92
|
|
42
93
|
---
|
43
94
|
|
44
|
-
##
|
95
|
+
## Attribute DSL
|
96
|
+
|
97
|
+
Castkit attributes define the shape, type, and behavior of fields on a DataObject. Attributes are declared using the `attribute` method or shorthand type methods provided by `Castkit::Core::AttributeTypes`.
|
45
98
|
|
46
99
|
```ruby
|
47
100
|
class UserDto < Castkit::DataObject
|
48
|
-
string :name
|
49
|
-
integer :age, required: false
|
101
|
+
string :name, required: true
|
50
102
|
boolean :admin, default: false
|
103
|
+
array :tags, of: :string, ignore_nil: true
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
---
|
108
|
+
|
109
|
+
### 🧠 Supported Types
|
110
|
+
|
111
|
+
Castkit supports a strict set of primitive types defined in `Castkit::Configuration::DEFAULT_TYPES` and aliased in `TYPE_ALIASES`.
|
112
|
+
|
113
|
+
#### Canonical Types:
|
114
|
+
- `:array`
|
115
|
+
- `:boolean`
|
116
|
+
- `:date`
|
117
|
+
- `:datetime`
|
118
|
+
- `:float`
|
119
|
+
- `:hash`
|
120
|
+
- `:integer`
|
121
|
+
- `:string`
|
122
|
+
|
123
|
+
#### Type Aliases:
|
124
|
+
|
125
|
+
Castkit provides shorthand aliases for common primitive types:
|
126
|
+
|
127
|
+
| Alias | Canonical | Description |
|
128
|
+
|--------------|-------------|-------------------------------------|
|
129
|
+
| `collection` | `array` | Alias for arrays |
|
130
|
+
| `bool` | `boolean` | Alias for true/false types |
|
131
|
+
| `int` | `integer` | Alias for integer values |
|
132
|
+
| `map` | `hash` | Alias for hashes (key-value pairs) |
|
133
|
+
| `number` | `float` | Alias for numeric values |
|
134
|
+
| `str` | `string` | Alias for strings |
|
135
|
+
| `timestamp` | `datetime` | Alias for date-time values |
|
136
|
+
| `uuid` | `string` | Commonly used for identifiers |
|
137
|
+
|
138
|
+
No other types are supported unless explicitly registered via `Castkit.configuration.register_type`.
|
139
|
+
|
140
|
+
---
|
141
|
+
|
142
|
+
|
143
|
+
### ⚙️ Attribute Options
|
144
|
+
|
145
|
+
| Option | Type | Default | Description |
|
146
|
+
|-------------------|------------|----------------|-------------|
|
147
|
+
| `required` | Boolean | `true` | Whether the field is required on initialization. |
|
148
|
+
| `default` | Object/Proc| `nil` | Default value or lambda called at runtime. |
|
149
|
+
| `access` | Array<Symbol> | `[:read, :write]` | Controls read/write visibility. |
|
150
|
+
| `ignore_nil` | Boolean | `false` | Exclude `nil` values from serialization. |
|
151
|
+
| `ignore_blank` | Boolean | `false` | Exclude empty strings, arrays, and hashes. |
|
152
|
+
| `ignore` | Boolean | `false` | Fully ignore the field (no serialization/deserialization). |
|
153
|
+
| `composite` | Boolean | `false` | Used for computed, virtual fields. |
|
154
|
+
| `transient` | Boolean | `false` | Excluded from serialized output. |
|
155
|
+
| `unwrapped` | Boolean | `false` | Merges nested DataObject fields into parent. |
|
156
|
+
| `prefix` | String | `nil` | Used with `unwrapped` to prefix keys. |
|
157
|
+
| `aliases` | Array<Symbol> | `[]` | Accept alternative keys during deserialization. |
|
158
|
+
| `of:` | Symbol | `nil` | Required for `:array` attributes. |
|
159
|
+
| `validator:` | Proc | `nil` | Optional callable that validates the value. |
|
160
|
+
|
161
|
+
---
|
162
|
+
|
163
|
+
### 🔒 Access Control
|
164
|
+
|
165
|
+
Access determines when the field is considered readable/writable.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
string :email, access: [:read]
|
169
|
+
string :password, access: [:write]
|
170
|
+
```
|
171
|
+
|
172
|
+
---
|
173
|
+
|
174
|
+
### 🧩 Attribute Grouping
|
175
|
+
|
176
|
+
Castkit supports grouping attributes using `required` and `optional` blocks to reduce repetition and improve clarity when defining large DTOs.
|
177
|
+
|
178
|
+
#### Example
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
class UserDto < Castkit::DataObject
|
182
|
+
required do
|
183
|
+
string :id
|
184
|
+
string :name
|
185
|
+
end
|
186
|
+
|
187
|
+
optional do
|
188
|
+
integer :age
|
189
|
+
boolean :admin
|
190
|
+
end
|
191
|
+
end
|
192
|
+
```
|
193
|
+
|
194
|
+
This is equivalent to:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
class UserDto < Castkit::DataObject
|
198
|
+
string :id # required: true
|
199
|
+
string :name # required: true
|
200
|
+
integer :age, required: false
|
201
|
+
boolean :admin, required: false
|
202
|
+
end
|
203
|
+
```
|
204
|
+
Grouped declarations are especially useful when your DTO has many optional fields or a mix of required/optional fields across different types.
|
205
|
+
|
206
|
+
---
|
207
|
+
|
208
|
+
### 🧬 Unwrapped & Composite
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class Metadata < Castkit::DataObject
|
212
|
+
string :locale
|
213
|
+
end
|
51
214
|
|
52
|
-
|
215
|
+
class PageDto < Castkit::DataObject
|
216
|
+
dataobject :metadata, unwrapped: true, prefix: "meta"
|
53
217
|
end
|
54
218
|
|
55
|
-
|
56
|
-
|
57
|
-
|
219
|
+
# Serializes as:
|
220
|
+
# { "meta_locale": "en" }
|
221
|
+
```
|
222
|
+
|
223
|
+
#### Composite Attributes
|
224
|
+
|
225
|
+
Composite fields are computed virtual attributes:
|
58
226
|
|
59
|
-
|
60
|
-
|
227
|
+
```ruby
|
228
|
+
class ProductDto < Castkit::DataObject
|
229
|
+
string :name, required: true
|
230
|
+
string :sku, access: [:read]
|
231
|
+
float :price, default: 0.0
|
232
|
+
|
233
|
+
composite :description, :string do
|
234
|
+
"#{name}: #{sku} - #{price}"
|
235
|
+
end
|
236
|
+
end
|
61
237
|
```
|
62
238
|
|
63
239
|
---
|
64
240
|
|
65
|
-
|
241
|
+
### 🔍 Transient Attributes
|
242
|
+
|
243
|
+
Transient fields are excluded from serialization and can be defined in two ways:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
class ProductDto < Castkit::DataObject
|
247
|
+
string :id, transient: true
|
66
248
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
| `float` | `float :rating` |
|
73
|
-
| `date` | `date :published_on` |
|
74
|
-
| `datetime` | `datetime :created_at` |
|
75
|
-
| `array` | `array :tags, of: :string` |
|
76
|
-
| `hash` | `hash :metadata` |
|
77
|
-
| `dataobject`| `dataobject :profile, ProfileDto` |
|
249
|
+
transient do
|
250
|
+
string :internal_token
|
251
|
+
end
|
252
|
+
end
|
253
|
+
```
|
78
254
|
|
79
255
|
---
|
80
256
|
|
81
|
-
|
257
|
+
### 🪞 Aliases and Key Paths
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
string :email, aliases: ["emailAddress", "user.email"]
|
261
|
+
|
262
|
+
dto.load({ "emailAddress" => "foo@bar.com" })
|
263
|
+
```
|
264
|
+
|
265
|
+
---
|
82
266
|
|
83
|
-
|
267
|
+
### 🧪 Example
|
84
268
|
|
85
269
|
```ruby
|
86
|
-
class
|
87
|
-
|
88
|
-
|
270
|
+
class ProductDto < Castkit::DataObject
|
271
|
+
string :name, required: true
|
272
|
+
float :price, default: 0.0, validator: ->(v) { raise "too low" if v < 0 }
|
273
|
+
array :tags, of: :string, ignore_blank: true
|
274
|
+
string :sku, access: [:read]
|
275
|
+
|
276
|
+
composite :description, :string do
|
277
|
+
"#{name}: #{sku} - #{price}"
|
278
|
+
end
|
279
|
+
|
280
|
+
transient do
|
281
|
+
string :id
|
89
282
|
end
|
90
283
|
end
|
284
|
+
```
|
91
285
|
|
92
|
-
|
93
|
-
|
286
|
+
---
|
287
|
+
|
288
|
+
## DataObjects
|
289
|
+
|
290
|
+
`Castkit::DataObject` is the base class for all structured DTOs. It offers a complete lifecycle for data ingestion, transformation, and output, supporting strict typing, validation, access control, aliasing, serialization, and root-wrapped payloads.
|
291
|
+
|
292
|
+
---
|
293
|
+
|
294
|
+
### ✍️ Defining a DTO
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
class UserDto < Castkit::DataObject
|
298
|
+
string :id
|
299
|
+
string :name
|
300
|
+
integer :age, required: false
|
94
301
|
end
|
95
302
|
```
|
96
303
|
|
97
|
-
|
304
|
+
---
|
305
|
+
|
306
|
+
### 🚀 Instantiation & Usage
|
98
307
|
|
99
308
|
```ruby
|
100
|
-
|
309
|
+
user = UserDto.new(name: "Alice", age: 30)
|
310
|
+
user.to_h #=> { name: "Alice", age: 30 }
|
311
|
+
user.to_json #=> '{"name":"Alice","age":30}'
|
101
312
|
```
|
102
313
|
|
103
314
|
---
|
104
315
|
|
105
|
-
|
316
|
+
### ⚖️ Strict Mode vs. Unknown Key Handling
|
317
|
+
|
318
|
+
By default, Castkit operates in strict mode and raises if unknown keys are passed. You can override this:
|
106
319
|
|
107
320
|
```ruby
|
108
|
-
class
|
109
|
-
|
110
|
-
|
321
|
+
class LooseDto < Castkit::DataObject
|
322
|
+
strict false
|
323
|
+
ignore_unknown true # equivalent to strict false
|
324
|
+
warn_on_unknown true # emits a warning instead of raising
|
111
325
|
end
|
112
326
|
```
|
113
327
|
|
328
|
+
To build a relaxed version dynamically:
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
LooseClone = MyDto.relaxed(warn_on_unknown: true)
|
332
|
+
```
|
333
|
+
|
114
334
|
---
|
115
335
|
|
116
|
-
|
336
|
+
### 🧱 Root Wrapping
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
class WrappedDto < Castkit::DataObject
|
340
|
+
root :user
|
341
|
+
string :name
|
342
|
+
end
|
343
|
+
|
344
|
+
WrappedDto.new(name: "Test").to_h
|
345
|
+
#=> { "user" => { "name" => "Test" } }
|
346
|
+
```
|
347
|
+
|
348
|
+
---
|
349
|
+
|
350
|
+
### 📦 Deserialization Helpers
|
351
|
+
|
352
|
+
You can deserialize using:
|
117
353
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
354
|
+
```ruby
|
355
|
+
UserDto.from_h(hash)
|
356
|
+
UserDto.deserialize(hash)
|
357
|
+
```
|
122
358
|
|
123
359
|
---
|
124
360
|
|
125
|
-
|
361
|
+
### 🔁 Conversion from/to Contract
|
126
362
|
|
127
363
|
```ruby
|
128
|
-
|
129
|
-
|
364
|
+
contract = UserDto.to_contract
|
365
|
+
UserDto.validate!(id: "123", name: "Alice")
|
366
|
+
|
367
|
+
from_contract = Castkit::DataObject.from_contract(contract)
|
368
|
+
```
|
369
|
+
|
370
|
+
---
|
371
|
+
|
372
|
+
### 🔄 Serializer Override
|
130
373
|
|
374
|
+
To override default serialization behavior:
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
class CustomSerializer < Castkit::Serializer
|
131
378
|
def call
|
132
|
-
|
379
|
+
{ payload: object.to_h }
|
133
380
|
end
|
134
381
|
end
|
135
382
|
|
136
|
-
class
|
137
|
-
string :
|
138
|
-
|
383
|
+
class MyDto < Castkit::DataObject
|
384
|
+
string :field
|
385
|
+
serializer CustomSerializer
|
386
|
+
end
|
387
|
+
```
|
388
|
+
|
389
|
+
---
|
390
|
+
|
391
|
+
### 🔍 Tracking Unknown Fields
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
dto = UserDto.new(name: "Alice", foo: "bar")
|
395
|
+
dto.unknown_attributes
|
396
|
+
#=> { foo: "bar" }
|
397
|
+
```
|
398
|
+
|
399
|
+
---
|
400
|
+
|
401
|
+
### 📤 Registering a Contract
|
402
|
+
|
403
|
+
```ruby
|
404
|
+
UserDto.register!(as: :User)
|
405
|
+
# Registers under Castkit::DataObjects::User
|
406
|
+
```
|
407
|
+
|
408
|
+
---
|
409
|
+
|
410
|
+
## Contracts
|
411
|
+
|
412
|
+
`Castkit::Contract` provides a lightweight mechanism for validating structured input without requiring a full data model. Ideal for validating service inputs, API payloads, or command parameters.
|
413
|
+
|
414
|
+
---
|
415
|
+
|
416
|
+
### 🛠 Defining Contracts
|
417
|
+
|
418
|
+
You can define a contract using the `.build` DSL:
|
419
|
+
|
420
|
+
```ruby
|
421
|
+
UserContract = Castkit::Contract.build(:user) do
|
422
|
+
string :id
|
423
|
+
string :email, required: false
|
424
|
+
end
|
425
|
+
```
|
426
|
+
|
427
|
+
Or subclass directly:
|
139
428
|
|
140
|
-
|
429
|
+
```ruby
|
430
|
+
class MyContract < Castkit::Contract::Generic
|
431
|
+
string :id
|
432
|
+
integer :count, required: false
|
141
433
|
end
|
142
434
|
```
|
143
435
|
|
144
436
|
---
|
145
437
|
|
146
|
-
|
438
|
+
### 🧪 Validation
|
147
439
|
|
148
440
|
```ruby
|
149
|
-
|
150
|
-
|
151
|
-
|
441
|
+
UserContract.validate(id: "123")
|
442
|
+
UserContract.validate!(id: "123")
|
443
|
+
```
|
152
444
|
|
153
|
-
Castkit
|
154
|
-
|
445
|
+
Returns a `Castkit::Contract::Result` with:
|
446
|
+
|
447
|
+
- `#success?` / `#failure?`
|
448
|
+
- `#errors` hash
|
449
|
+
- `#to_h` / `#to_s`
|
450
|
+
|
451
|
+
---
|
452
|
+
|
453
|
+
### ⚖️ Strict, Loose, and Warn Modes
|
454
|
+
|
455
|
+
```ruby
|
456
|
+
LooseContract = Castkit::Contract.build(:loose, strict: false) do
|
457
|
+
string :token
|
458
|
+
end
|
459
|
+
|
460
|
+
StrictContract = Castkit::Contract.build(:strict, allow_unknown: false, warn_on_unknown: true) do
|
461
|
+
string :id
|
462
|
+
end
|
463
|
+
```
|
464
|
+
|
465
|
+
---
|
466
|
+
|
467
|
+
### 🔄 Converting From DataObject
|
468
|
+
|
469
|
+
```ruby
|
470
|
+
class UserDto < Castkit::DataObject
|
471
|
+
string :id
|
472
|
+
string :email
|
155
473
|
end
|
474
|
+
|
475
|
+
UserContract = Castkit::Contract.from_dataobject(UserDto)
|
156
476
|
```
|
157
477
|
|
158
478
|
---
|
159
479
|
|
160
|
-
|
480
|
+
### ↔️ Converting Back to DTO
|
161
481
|
|
162
|
-
|
163
|
-
|
164
|
-
|
482
|
+
```ruby
|
483
|
+
UserDto = UserContract.to_dataobject
|
484
|
+
# or
|
485
|
+
UserDto = UserContract.dataobject
|
486
|
+
```
|
165
487
|
|
166
488
|
---
|
167
489
|
|
168
|
-
|
490
|
+
### 📤 Registering a Contract
|
491
|
+
|
492
|
+
```ruby
|
493
|
+
UserContract.register!(as: :UserInput)
|
494
|
+
# Registers under Castkit::Contracts::UserInput
|
495
|
+
```
|
496
|
+
|
497
|
+
---
|
498
|
+
|
499
|
+
### 🧱 Supported Options in Contract Attributes
|
500
|
+
|
501
|
+
Only a subset of options are supported:
|
502
|
+
|
503
|
+
- `required`
|
504
|
+
- `aliases`
|
505
|
+
- `min`, `max`, `format`
|
506
|
+
- `of` (for arrays)
|
507
|
+
- `validator`
|
508
|
+
- `unwrapped`, `prefix`
|
509
|
+
- `force_type`
|
510
|
+
|
511
|
+
---
|
512
|
+
|
513
|
+
### 🧩 Validating Nested DTOs
|
514
|
+
|
515
|
+
```ruby
|
516
|
+
class AddressDto < Castkit::DataObject
|
517
|
+
string :city
|
518
|
+
end
|
519
|
+
|
520
|
+
class UserDto < Castkit::DataObject
|
521
|
+
string :id
|
522
|
+
dataobject :address, of: AddressDto
|
523
|
+
end
|
524
|
+
|
525
|
+
UserContract = Castkit::Contract.from_dataobject(UserDto)
|
526
|
+
UserContract.validate!(id: "abc", address: { city: "Boston" })
|
527
|
+
```
|
528
|
+
|
529
|
+
---
|
530
|
+
|
531
|
+
## Advanced Usage (coming soon)
|
532
|
+
|
533
|
+
Castkit is designed to be modular and extendable. Future guides will cover:
|
534
|
+
|
535
|
+
- Custom serializers (`Castkit::Serializer`)
|
536
|
+
- Integration layers:
|
537
|
+
- `castkit-activerecord` for syncing with ActiveRecord models
|
538
|
+
- `castkit-msgpack` for binary encoding
|
539
|
+
- `castkit-oj` for high-performance JSON
|
540
|
+
- OpenAPI-compatible schema generation
|
541
|
+
- Declarative enums and union type helpers
|
542
|
+
- Circular reference detection in nested serialization
|
543
|
+
|
544
|
+
---
|
545
|
+
|
546
|
+
## Testing
|
547
|
+
|
548
|
+
You can test DTOs and Contracts by treating them like plain Ruby objects:
|
549
|
+
|
550
|
+
```ruby
|
551
|
+
dto = MyDto.new(name: "Alice")
|
552
|
+
expect(dto.name).to eq("Alice")
|
553
|
+
```
|
169
554
|
|
170
|
-
You can
|
555
|
+
You can also assert validation errors:
|
171
556
|
|
172
557
|
```ruby
|
173
|
-
|
174
|
-
|
558
|
+
expect {
|
559
|
+
MyDto.new(name: nil)
|
560
|
+
}.to raise_error(Castkit::AttributeError, /name is required/)
|
175
561
|
```
|
176
562
|
|
177
563
|
---
|
178
564
|
|
179
|
-
##
|
565
|
+
## Compatibility
|
180
566
|
|
181
567
|
- Ruby 2.7+
|
182
568
|
- Zero dependencies (uses core Ruby)
|
183
569
|
|
184
570
|
---
|
185
571
|
|
186
|
-
##
|
572
|
+
## License
|
187
573
|
|
188
574
|
MIT. See [LICENSE](LICENSE).
|
189
575
|
|
@@ -192,4 +578,3 @@ MIT. See [LICENSE](LICENSE).
|
|
192
578
|
## 🙏 Credits
|
193
579
|
|
194
580
|
Created with ❤️ by [Nathan Lucas](https://github.com/bnlucas)
|
195
|
-
Inspired by Java DTOs, dry-rb, and the need for clean, reliable data structures in APIs.
|