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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +196 -219
  3. data/CHANGELOG.md +42 -0
  4. data/README.md +469 -84
  5. data/lib/castkit/attribute.rb +6 -24
  6. data/lib/castkit/castkit.rb +58 -10
  7. data/lib/castkit/configuration.rb +94 -47
  8. data/lib/castkit/contract/data_object.rb +62 -0
  9. data/lib/castkit/contract/generic.rb +168 -0
  10. data/lib/castkit/contract/result.rb +74 -0
  11. data/lib/castkit/contract/validator.rb +248 -0
  12. data/lib/castkit/contract.rb +67 -0
  13. data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
  14. data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
  15. data/lib/castkit/core/config.rb +74 -0
  16. data/lib/castkit/core/registerable.rb +59 -0
  17. data/lib/castkit/data_object.rb +45 -60
  18. data/lib/castkit/default_serializer.rb +85 -54
  19. data/lib/castkit/error.rb +15 -3
  20. data/lib/castkit/ext/attribute/access.rb +67 -0
  21. data/lib/castkit/ext/attribute/error_handling.rb +63 -0
  22. data/lib/castkit/ext/attribute/options.rb +142 -0
  23. data/lib/castkit/ext/attribute/validation.rb +85 -0
  24. data/lib/castkit/ext/data_object/contract.rb +96 -0
  25. data/lib/castkit/ext/data_object/deserialization.rb +167 -0
  26. data/lib/castkit/ext/data_object/serialization.rb +61 -0
  27. data/lib/castkit/inflector.rb +47 -0
  28. data/lib/castkit/types/boolean.rb +43 -0
  29. data/lib/castkit/types/collection.rb +24 -0
  30. data/lib/castkit/types/date.rb +34 -0
  31. data/lib/castkit/types/date_time.rb +34 -0
  32. data/lib/castkit/types/float.rb +46 -0
  33. data/lib/castkit/types/generic.rb +123 -0
  34. data/lib/castkit/types/integer.rb +46 -0
  35. data/lib/castkit/types/string.rb +44 -0
  36. data/lib/castkit/types.rb +15 -0
  37. data/lib/castkit/validators/base_validator.rb +39 -0
  38. data/lib/castkit/validators/numeric_validator.rb +2 -2
  39. data/lib/castkit/validators/string_validator.rb +3 -3
  40. data/lib/castkit/version.rb +1 -1
  41. data/lib/castkit.rb +2 -0
  42. metadata +29 -13
  43. data/lib/castkit/attribute_extensions/access.rb +0 -65
  44. data/lib/castkit/attribute_extensions/casting.rb +0 -147
  45. data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
  46. data/lib/castkit/attribute_extensions/options.rb +0 -131
  47. data/lib/castkit/attribute_extensions/serialization.rb +0 -89
  48. data/lib/castkit/attribute_extensions/validation.rb +0 -72
  49. data/lib/castkit/data_object_extensions/config.rb +0 -113
  50. data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
  51. data/lib/castkit/validators.rb +0 -4
data/README.md CHANGED
@@ -1,189 +1,575 @@
1
-
2
1
  # Castkit
3
2
 
4
- **Castkit** is a lightweight, type-safe Ruby DSL for defining and validating data objects.
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
- It’s inspired by DTO (data transfer object) patterns and brings clarity, safety, and structure to how you define and manipulate structured data in Ruby — with support for casting, validation, access control, serialization, and extensibility.
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
- ## Features
17
+ ## 🚀 Features
11
18
 
12
- - ✅ Declarative type-safe attribute definitions
13
- - 🔁 Built-in casting for primitive and custom types
14
- - 🔍 Pluggable validation (with per-type default validators)
15
- - 🔐 Attribute-level access control (`read`, `write`)
16
- - 📦 Serialization and deserialization with optional unwrapping and root keys
17
- - ♻️ Circular reference detection during serialization
18
- - 🔧 Configurable enforcement and custom validators
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
- ## 🔧 Installation
30
+ ## Configuration
23
31
 
24
- Add this line to your Gemfile:
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
- gem 'castkit'
35
+ Castkit.configure do |config|
36
+ config.enable_warnings = false
37
+ config.enforce_typing = true
38
+ end
28
39
  ```
29
40
 
30
- Then install:
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
- ```bash
33
- bundle install
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
- Or install manually:
72
+ #### Type Aliases
37
73
 
38
- ```bash
39
- gem install castkit
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
- ## 🚀 Quick Start
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
- unwrapped :profile, ProfileDto, prefix: "profile_"
215
+ class PageDto < Castkit::DataObject
216
+ dataobject :metadata, unwrapped: true, prefix: "meta"
53
217
  end
54
218
 
55
- user = UserDto.new(name: "Alice", age: 30, profile_name: "Dev")
56
- user.to_h
57
- # => { name: "Alice", age: 30, admin: false, profile_name: "Dev" }
219
+ # Serializes as:
220
+ # { "meta_locale": "en" }
221
+ ```
222
+
223
+ #### Composite Attributes
224
+
225
+ Composite fields are computed virtual attributes:
58
226
 
59
- user.to_json
60
- # => '{"name":"Alice","age":30,"admin":false,"profile_name":"Dev"}'
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
- ## 🧱 Defining Attributes
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
- | Type | Example |
68
- |-------------|--------------------------------|
69
- | `string` | `string :name` |
70
- | `integer` | `integer :age` |
71
- | `boolean` | `boolean :active` |
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
- ## 🧪 Validation
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
- Validators can be built-in or custom:
267
+ ### 🧪 Example
84
268
 
85
269
  ```ruby
86
- class ZipValidator
87
- def call(value, options:, context:)
88
- raise Castkit::AttributeError, "#{context} is not a valid ZIP" unless value =~ /^\d{5}$/
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
- class AddressDto < Castkit::DataObject
93
- string :zip, validator: ZipValidator.new
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
- You can also register per-type defaults globally:
304
+ ---
305
+
306
+ ### 🚀 Instantiation & Usage
98
307
 
99
308
  ```ruby
100
- Castkit.configuration.register_validator(:zip, ZipValidator.new)
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
- ## 🔐 Access Control
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 CredentialsDto < Castkit::DataObject
109
- string :username
110
- string :password, access: [:write] # only writeable, not serialized
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
- ## 🪄 Serialization Options
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
- - `ignore_nil: true` – skip nils
119
- - `ignore_blank: true` – skip `[]`, `{}`, `""`, etc.
120
- - `unwrapped: true, prefix: "foo_"` – flatten nested objects
121
- - `root "user"` – wrap in `{ "user": { ... } }`
354
+ ```ruby
355
+ UserDto.from_h(hash)
356
+ UserDto.deserialize(hash)
357
+ ```
122
358
 
123
359
  ---
124
360
 
125
- ## 🔄 Custom Serializers
361
+ ### 🔁 Conversion from/to Contract
126
362
 
127
363
  ```ruby
128
- class SimpleSerializer < Castkit::Serializer
129
- private
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
- obj.class.attributes.keys.map { |k| [k, obj.public_send(k)] }.to_h
379
+ { payload: object.to_h }
133
380
  end
134
381
  end
135
382
 
136
- class EventDto < Castkit::DataObject
137
- string :type
138
- string :timestamp
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
- serializer SimpleSerializer
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
- ## ⚙️ Configuration
438
+ ### 🧪 Validation
147
439
 
148
440
  ```ruby
149
- Castkit.configuration.enforce_array_of_type = true
150
- Castkit.configuration.enforce_boolean_casting = false
151
- Castkit.configuration.register_validator(:uuid, UuidValidator.new)
441
+ UserContract.validate(id: "123")
442
+ UserContract.validate!(id: "123")
443
+ ```
152
444
 
153
- Castkit.configure do |config|
154
- config.enforce_array_of_type = true
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
- ## 💥 Error Handling
480
+ ### ↔️ Converting Back to DTO
161
481
 
162
- - `Castkit::AttributeError` – invalid attribute value
163
- - `Castkit::DataObjectError` – object-level failure (e.g., unknown key)
164
- - `Castkit::SerializationError` – circular reference or serialization failure
482
+ ```ruby
483
+ UserDto = UserContract.to_dataobject
484
+ # or
485
+ UserDto = UserContract.dataobject
486
+ ```
165
487
 
166
488
  ---
167
489
 
168
- ## 🧪 Testing
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 test DTOs by instantiating them like POROs:
555
+ You can also assert validation errors:
171
556
 
172
557
  ```ruby
173
- dto = MyDto.new(input_data)
174
- expect(dto.name).to eq("expected")
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
- ## 📦 Compatibility
565
+ ## Compatibility
180
566
 
181
567
  - Ruby 2.7+
182
568
  - Zero dependencies (uses core Ruby)
183
569
 
184
570
  ---
185
571
 
186
- ## 📃 License
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.