castkit 0.1.2 → 0.3.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +195 -219
  3. data/CHANGELOG.md +42 -0
  4. data/README.md +744 -83
  5. data/castkit.gemspec +1 -0
  6. data/lib/castkit/attribute.rb +6 -24
  7. data/lib/castkit/castkit.rb +61 -10
  8. data/lib/castkit/cli/generate.rb +98 -0
  9. data/lib/castkit/cli/list.rb +200 -0
  10. data/lib/castkit/cli/main.rb +43 -0
  11. data/lib/castkit/cli.rb +24 -0
  12. data/lib/castkit/configuration.rb +116 -46
  13. data/lib/castkit/contract/base.rb +168 -0
  14. data/lib/castkit/contract/data_object.rb +62 -0
  15. data/lib/castkit/contract/result.rb +74 -0
  16. data/lib/castkit/contract/validator.rb +248 -0
  17. data/lib/castkit/contract.rb +67 -0
  18. data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
  19. data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
  20. data/lib/castkit/core/config.rb +74 -0
  21. data/lib/castkit/core/registerable.rb +59 -0
  22. data/lib/castkit/data_object.rb +56 -67
  23. data/lib/castkit/error.rb +15 -3
  24. data/lib/castkit/ext/attribute/access.rb +67 -0
  25. data/lib/castkit/ext/attribute/error_handling.rb +63 -0
  26. data/lib/castkit/ext/attribute/options.rb +142 -0
  27. data/lib/castkit/ext/attribute/validation.rb +85 -0
  28. data/lib/castkit/ext/data_object/contract.rb +96 -0
  29. data/lib/castkit/ext/data_object/deserialization.rb +167 -0
  30. data/lib/castkit/ext/data_object/plugins.rb +86 -0
  31. data/lib/castkit/ext/data_object/serialization.rb +61 -0
  32. data/lib/castkit/inflector.rb +47 -0
  33. data/lib/castkit/plugins.rb +82 -0
  34. data/lib/castkit/serializers/base.rb +94 -0
  35. data/lib/castkit/serializers/default_serializer.rb +156 -0
  36. data/lib/castkit/types/base.rb +122 -0
  37. data/lib/castkit/types/boolean.rb +47 -0
  38. data/lib/castkit/types/collection.rb +35 -0
  39. data/lib/castkit/types/date.rb +34 -0
  40. data/lib/castkit/types/date_time.rb +34 -0
  41. data/lib/castkit/types/float.rb +46 -0
  42. data/lib/castkit/types/integer.rb +46 -0
  43. data/lib/castkit/types/string.rb +44 -0
  44. data/lib/castkit/types.rb +15 -0
  45. data/lib/castkit/validators/base.rb +59 -0
  46. data/lib/castkit/validators/boolean_validator.rb +39 -0
  47. data/lib/castkit/validators/collection_validator.rb +29 -0
  48. data/lib/castkit/validators/float_validator.rb +31 -0
  49. data/lib/castkit/validators/integer_validator.rb +31 -0
  50. data/lib/castkit/validators/numeric_validator.rb +2 -2
  51. data/lib/castkit/validators/string_validator.rb +3 -4
  52. data/lib/castkit/version.rb +1 -1
  53. data/lib/castkit.rb +2 -0
  54. data/lib/generators/base.rb +97 -0
  55. data/lib/generators/contract.rb +68 -0
  56. data/lib/generators/data_object.rb +48 -0
  57. data/lib/generators/plugin.rb +25 -0
  58. data/lib/generators/serializer.rb +28 -0
  59. data/lib/generators/templates/contract.rb.tt +24 -0
  60. data/lib/generators/templates/contract_spec.rb.tt +76 -0
  61. data/lib/generators/templates/data_object.rb.tt +15 -0
  62. data/lib/generators/templates/data_object_spec.rb.tt +36 -0
  63. data/lib/generators/templates/plugin.rb.tt +37 -0
  64. data/lib/generators/templates/plugin_spec.rb.tt +18 -0
  65. data/lib/generators/templates/serializer.rb.tt +24 -0
  66. data/lib/generators/templates/serializer_spec.rb.tt +14 -0
  67. data/lib/generators/templates/type.rb.tt +55 -0
  68. data/lib/generators/templates/type_spec.rb.tt +42 -0
  69. data/lib/generators/templates/validator.rb.tt +26 -0
  70. data/lib/generators/templates/validator_spec.rb.tt +23 -0
  71. data/lib/generators/type.rb +29 -0
  72. data/lib/generators/validator.rb +41 -0
  73. metadata +74 -15
  74. data/lib/castkit/attribute_extensions/access.rb +0 -65
  75. data/lib/castkit/attribute_extensions/casting.rb +0 -147
  76. data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
  77. data/lib/castkit/attribute_extensions/options.rb +0 -131
  78. data/lib/castkit/attribute_extensions/serialization.rb +0 -89
  79. data/lib/castkit/attribute_extensions/validation.rb +0 -72
  80. data/lib/castkit/data_object_extensions/config.rb +0 -113
  81. data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
  82. data/lib/castkit/default_serializer.rb +0 -123
  83. data/lib/castkit/serializer.rb +0 -92
  84. data/lib/castkit/validators.rb +0 -4
data/README.md CHANGED
@@ -1,189 +1,851 @@
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:
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.
5
12
 
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.
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
+ - [Plugins](#plugins)
25
+ - [Castkit CLI](#castkit-cli)
26
+ - [Testing](#testing)
27
+ - [Compatibility](#compatibility)
28
+ - [License](#license)
19
29
 
20
30
  ---
21
31
 
22
- ## 🔧 Installation
32
+ ## Configuration
23
33
 
24
- Add this line to your Gemfile:
34
+ 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
35
 
26
36
  ```ruby
27
- gem 'castkit'
37
+ Castkit.configure do |config|
38
+ config.enable_warnings = false
39
+ config.enforce_typing = true
40
+ end
28
41
  ```
29
42
 
30
- Then install:
43
+ ### ⚙️ Available Settings
31
44
 
32
- ```bash
33
- bundle install
45
+ | Option | Type | Default | Description |
46
+ |----------------------------|---------|---------|-------------|
47
+ | `enable_warnings` | Boolean | `true` | Enables runtime warnings for misconfigurations. |
48
+ | `enforce_typing` | Boolean | `true` | Raises if type mismatch during load (e.g., `true` vs. `"true"`). |
49
+ | `enforce_attribute_access` | Boolean | `true` | Raises if an unknown access level is defined. |
50
+ | `enforce_unwrapped_prefix` | Boolean | `true` | Requires `unwrapped: true` when using attribute prefixes. |
51
+ | `enforce_array_options` | Boolean | `true` | Raises if an array attribute is missing the `of:` option. |
52
+ | `raise_type_errors` | Boolean | `true` | Raises if an unregistered or invalid type is used. |
53
+ | `strict_by_default` | Boolean | `true` | Applies `strict: true` by default to all DTOs and Contracts. |
54
+
55
+ ### 🔧 Type System
56
+
57
+ Castkit comes with built-in support for primitive types and allows registration of custom ones:
58
+
59
+ #### Default types
60
+
61
+ ```ruby
62
+ {
63
+ array: Castkit::Types::Collection,
64
+ boolean: Castkit::Types::Boolean,
65
+ date: Castkit::Types::Date,
66
+ datetime: Castkit::Types::DateTime,
67
+ float: Castkit::Types::Float,
68
+ hash: Castkit::Types::Base,
69
+ integer: Castkit::Types::Integer,
70
+ string: Castkit::Types::String
71
+ }
34
72
  ```
35
73
 
36
- Or install manually:
74
+ #### Type Aliases
37
75
 
38
- ```bash
39
- gem install castkit
76
+ | Alias | Canonical |
77
+ |------------|-----------|
78
+ | `collection` | `array` |
79
+ | `bool` | `boolean` |
80
+ | `int` | `integer` |
81
+ | `map` | `hash` |
82
+ | `number` | `float` |
83
+ | `str` | `string` |
84
+ | `timestamp` | `datetime`|
85
+ | `uuid` | `string` |
86
+
87
+ #### Registering Custom Types
88
+
89
+ ```ruby
90
+ Castkit.configure do |config|
91
+ config.register_type(:mytype, MyTypeClass, aliases: [:custom])
92
+ end
40
93
  ```
41
94
 
42
95
  ---
43
96
 
44
- ## 🚀 Quick Start
97
+ ## Attribute DSL
98
+
99
+ 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
100
 
46
101
  ```ruby
47
102
  class UserDto < Castkit::DataObject
48
- string :name
49
- integer :age, required: false
103
+ string :name, required: true
50
104
  boolean :admin, default: false
105
+ array :tags, of: :string, ignore_nil: true
106
+ end
107
+ ```
108
+
109
+ ---
110
+
111
+ ### 🧠 Supported Types
112
+
113
+ Castkit supports a strict set of primitive types defined in `Castkit::Configuration::DEFAULT_TYPES` and aliased in `TYPE_ALIASES`.
114
+
115
+ #### Canonical Types:
116
+ - `:array`
117
+ - `:boolean`
118
+ - `:date`
119
+ - `:datetime`
120
+ - `:float`
121
+ - `:hash`
122
+ - `:integer`
123
+ - `:string`
124
+
125
+ #### Type Aliases:
126
+
127
+ Castkit provides shorthand aliases for common primitive types:
128
+
129
+ | Alias | Canonical | Description |
130
+ |--------------|-------------|-------------------------------------|
131
+ | `collection` | `array` | Alias for arrays |
132
+ | `bool` | `boolean` | Alias for true/false types |
133
+ | `int` | `integer` | Alias for integer values |
134
+ | `map` | `hash` | Alias for hashes (key-value pairs) |
135
+ | `number` | `float` | Alias for numeric values |
136
+ | `str` | `string` | Alias for strings |
137
+ | `timestamp` | `datetime` | Alias for date-time values |
138
+ | `uuid` | `string` | Commonly used for identifiers |
139
+
140
+ No other types are supported unless explicitly registered via `Castkit.configuration.register_type`.
141
+
142
+ ---
143
+
144
+
145
+ ### ⚙️ Attribute Options
146
+
147
+ | Option | Type | Default | Description |
148
+ |-------------------|------------|----------------|-------------|
149
+ | `required` | Boolean | `true` | Whether the field is required on initialization. |
150
+ | `default` | Object/Proc| `nil` | Default value or lambda called at runtime. |
151
+ | `access` | Array<Symbol> | `[:read, :write]` | Controls read/write visibility. |
152
+ | `ignore_nil` | Boolean | `false` | Exclude `nil` values from serialization. |
153
+ | `ignore_blank` | Boolean | `false` | Exclude empty strings, arrays, and hashes. |
154
+ | `ignore` | Boolean | `false` | Fully ignore the field (no serialization/deserialization). |
155
+ | `composite` | Boolean | `false` | Used for computed, virtual fields. |
156
+ | `transient` | Boolean | `false` | Excluded from serialized output. |
157
+ | `unwrapped` | Boolean | `false` | Merges nested DataObject fields into parent. |
158
+ | `prefix` | String | `nil` | Used with `unwrapped` to prefix keys. |
159
+ | `aliases` | Array<Symbol> | `[]` | Accept alternative keys during deserialization. |
160
+ | `of:` | Symbol | `nil` | Required for `:array` attributes. |
161
+ | `validator:` | Proc | `nil` | Optional callable that validates the value. |
162
+
163
+ ---
164
+
165
+ ### 🔒 Access Control
166
+
167
+ Access determines when the field is considered readable/writable.
168
+
169
+ ```ruby
170
+ string :email, access: [:read]
171
+ string :password, access: [:write]
172
+ ```
173
+
174
+ ---
175
+
176
+ ### 🧩 Attribute Grouping
177
+
178
+ Castkit supports grouping attributes using `required` and `optional` blocks to reduce repetition and improve clarity when defining large DTOs.
51
179
 
52
- unwrapped :profile, ProfileDto, prefix: "profile_"
180
+ #### Example
181
+
182
+ ```ruby
183
+ class UserDto < Castkit::DataObject
184
+ required do
185
+ string :id
186
+ string :name
187
+ end
188
+
189
+ optional do
190
+ integer :age
191
+ boolean :admin
192
+ end
53
193
  end
194
+ ```
54
195
 
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" }
196
+ This is equivalent to:
58
197
 
59
- user.to_json
60
- # => '{"name":"Alice","age":30,"admin":false,"profile_name":"Dev"}'
198
+ ```ruby
199
+ class UserDto < Castkit::DataObject
200
+ string :id # required: true
201
+ string :name # required: true
202
+ integer :age, required: false
203
+ boolean :admin, required: false
204
+ end
61
205
  ```
206
+ Grouped declarations are especially useful when your DTO has many optional fields or a mix of required/optional fields across different types.
62
207
 
63
208
  ---
64
209
 
65
- ## 🧱 Defining Attributes
210
+ ### 🧬 Unwrapped & Composite
66
211
 
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` |
212
+ ```ruby
213
+ class Metadata < Castkit::DataObject
214
+ string :locale
215
+ end
216
+
217
+ class PageDto < Castkit::DataObject
218
+ dataobject :metadata, unwrapped: true, prefix: "meta"
219
+ end
220
+
221
+ # Serializes as:
222
+ # { "meta_locale": "en" }
223
+ ```
224
+
225
+ #### Composite Attributes
226
+
227
+ Composite fields are computed virtual attributes:
228
+
229
+ ```ruby
230
+ class ProductDto < Castkit::DataObject
231
+ string :name, required: true
232
+ string :sku, access: [:read]
233
+ float :price, default: 0.0
234
+
235
+ composite :description, :string do
236
+ "#{name}: #{sku} - #{price}"
237
+ end
238
+ end
239
+ ```
78
240
 
79
241
  ---
80
242
 
81
- ## 🧪 Validation
243
+ ### 🔍 Transient Attributes
82
244
 
83
- Validators can be built-in or custom:
245
+ Transient fields are excluded from serialization and can be defined in two ways:
84
246
 
85
247
  ```ruby
86
- class ZipValidator
87
- def call(value, options:, context:)
88
- raise Castkit::AttributeError, "#{context} is not a valid ZIP" unless value =~ /^\d{5}$/
248
+ class ProductDto < Castkit::DataObject
249
+ string :id, transient: true
250
+
251
+ transient do
252
+ string :internal_token
89
253
  end
90
254
  end
255
+ ```
91
256
 
92
- class AddressDto < Castkit::DataObject
93
- string :zip, validator: ZipValidator.new
257
+ ---
258
+
259
+ ### 🪞 Aliases and Key Paths
260
+
261
+ ```ruby
262
+ string :email, aliases: ["emailAddress", "user.email"]
263
+
264
+ dto.load({ "emailAddress" => "foo@bar.com" })
265
+ ```
266
+
267
+ ---
268
+
269
+ ### 🧪 Example
270
+
271
+ ```ruby
272
+ class ProductDto < Castkit::DataObject
273
+ string :name, required: true
274
+ float :price, default: 0.0, validator: ->(v) { raise "too low" if v < 0 }
275
+ array :tags, of: :string, ignore_blank: true
276
+ string :sku, access: [:read]
277
+
278
+ composite :description, :string do
279
+ "#{name}: #{sku} - #{price}"
280
+ end
281
+
282
+ transient do
283
+ string :id
284
+ end
285
+ end
286
+ ```
287
+
288
+ ---
289
+
290
+ ## DataObjects
291
+
292
+ `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.
293
+
294
+ ---
295
+
296
+ ### ✍️ Defining a DTO
297
+
298
+ ```ruby
299
+ class UserDto < Castkit::DataObject
300
+ string :id
301
+ string :name
302
+ integer :age, required: false
303
+ end
304
+ ```
305
+
306
+ ---
307
+
308
+ ### 🚀 Instantiation & Usage
309
+
310
+ ```ruby
311
+ user = UserDto.new(name: "Alice", age: 30)
312
+ user.to_h #=> { name: "Alice", age: 30 }
313
+ user.to_json #=> '{"name":"Alice","age":30}'
314
+ ```
315
+
316
+ ---
317
+
318
+ ### ⚖️ Strict Mode vs. Unknown Key Handling
319
+
320
+ By default, Castkit operates in strict mode and raises if unknown keys are passed. You can override this:
321
+
322
+ ```ruby
323
+ class LooseDto < Castkit::DataObject
324
+ strict false
325
+ ignore_unknown true # equivalent to strict false
326
+ warn_on_unknown true # emits a warning instead of raising
94
327
  end
95
328
  ```
96
329
 
97
- You can also register per-type defaults globally:
330
+ To build a relaxed version dynamically:
98
331
 
99
332
  ```ruby
100
- Castkit.configuration.register_validator(:zip, ZipValidator.new)
333
+ LooseClone = MyDto.relaxed(warn_on_unknown: true)
101
334
  ```
102
335
 
103
336
  ---
104
337
 
105
- ## 🔐 Access Control
338
+ ### 🧱 Root Wrapping
106
339
 
107
340
  ```ruby
108
- class CredentialsDto < Castkit::DataObject
109
- string :username
110
- string :password, access: [:write] # only writeable, not serialized
341
+ class WrappedDto < Castkit::DataObject
342
+ root :user
343
+ string :name
111
344
  end
345
+
346
+ WrappedDto.new(name: "Test").to_h
347
+ #=> { "user" => { "name" => "Test" } }
112
348
  ```
113
349
 
114
350
  ---
115
351
 
116
- ## 🪄 Serialization Options
352
+ ### 📦 Deserialization Helpers
353
+
354
+ You can deserialize using:
117
355
 
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": { ... } }`
356
+ ```ruby
357
+ UserDto.from_h(hash)
358
+ UserDto.deserialize(hash)
359
+ ```
122
360
 
123
361
  ---
124
362
 
125
- ## 🔄 Custom Serializers
363
+ ### 🔁 Conversion from/to Contract
126
364
 
127
365
  ```ruby
128
- class SimpleSerializer < Castkit::Serializer
129
- private
366
+ contract = UserDto.to_contract
367
+ UserDto.validate!(id: "123", name: "Alice")
368
+
369
+ from_contract = Castkit::DataObject.from_contract(contract)
370
+ ```
371
+
372
+ ---
373
+
374
+ ### 🔄 Serializer Override
130
375
 
376
+ To override default serialization behavior:
377
+
378
+ ```ruby
379
+
380
+ class CustomSerializer < Castkit::Serializers::Base
131
381
  def call
132
- obj.class.attributes.keys.map { |k| [k, obj.public_send(k)] }.to_h
382
+ { payload: object.to_h }
133
383
  end
134
384
  end
135
385
 
136
- class EventDto < Castkit::DataObject
137
- string :type
138
- string :timestamp
386
+ class MyDto < Castkit::DataObject
387
+ string :field
388
+ serializer CustomSerializer
389
+ end
390
+ ```
391
+
392
+ ---
393
+
394
+ ### 🔍 Tracking Unknown Fields
395
+
396
+ ```ruby
397
+ dto = UserDto.new(name: "Alice", foo: "bar")
398
+ dto.unknown_attributes
399
+ #=> { foo: "bar" }
400
+ ```
139
401
 
140
- serializer SimpleSerializer
402
+ ---
403
+
404
+ ### 📤 Registering a Contract
405
+
406
+ ```ruby
407
+ UserDto.register!(as: :User)
408
+ # Registers under Castkit::DataObjects::User
409
+ ```
410
+
411
+ ---
412
+
413
+ ## Contracts
414
+
415
+ `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.
416
+
417
+ ---
418
+
419
+ ### 🛠 Defining Contracts
420
+
421
+ You can define a contract using the `.build` DSL:
422
+
423
+ ```ruby
424
+ UserContract = Castkit::Contract.build(:user) do
425
+ string :id
426
+ string :email, required: false
427
+ end
428
+ ```
429
+
430
+ Or subclass directly:
431
+
432
+ ```ruby
433
+
434
+ class MyContract < Castkit::Contract::Base
435
+ string :id
436
+ integer :count, required: false
141
437
  end
142
438
  ```
143
439
 
144
440
  ---
145
441
 
146
- ## ⚙️ Configuration
442
+ ### 🧪 Validation
147
443
 
148
444
  ```ruby
149
- Castkit.configuration.enforce_array_of_type = true
150
- Castkit.configuration.enforce_boolean_casting = false
151
- Castkit.configuration.register_validator(:uuid, UuidValidator.new)
445
+ UserContract.validate(id: "123")
446
+ UserContract.validate!(id: "123")
447
+ ```
448
+
449
+ Returns a `Castkit::Contract::Result` with:
450
+
451
+ - `#success?` / `#failure?`
452
+ - `#errors` hash
453
+ - `#to_h` / `#to_s`
454
+
455
+ ---
456
+
457
+ ### ⚖️ Strict, Loose, and Warn Modes
458
+
459
+ ```ruby
460
+ LooseContract = Castkit::Contract.build(:loose, strict: false) do
461
+ string :token
462
+ end
463
+
464
+ StrictContract = Castkit::Contract.build(:strict, allow_unknown: false, warn_on_unknown: true) do
465
+ string :id
466
+ end
467
+ ```
468
+
469
+ ---
470
+
471
+ ### 🔄 Converting From DataObject
472
+
473
+ ```ruby
474
+ class UserDto < Castkit::DataObject
475
+ string :id
476
+ string :email
477
+ end
478
+
479
+ UserContract = Castkit::Contract.from_dataobject(UserDto)
480
+ ```
481
+
482
+ ---
483
+
484
+ ### ↔️ Converting Back to DTO
485
+
486
+ ```ruby
487
+ UserDto = UserContract.to_dataobject
488
+ # or
489
+ UserDto = UserContract.dataobject
490
+ ```
491
+
492
+ ---
493
+
494
+ ### 📤 Registering a Contract
495
+
496
+ ```ruby
497
+ UserContract.register!(as: :UserInput)
498
+ # Registers under Castkit::Contracts::UserInput
499
+ ```
500
+
501
+ ---
502
+
503
+ ### 🧱 Supported Options in Contract Attributes
504
+
505
+ Only a subset of options are supported:
506
+
507
+ - `required`
508
+ - `aliases`
509
+ - `min`, `max`, `format`
510
+ - `of` (for arrays)
511
+ - `validator`
512
+ - `unwrapped`, `prefix`
513
+ - `force_type`
514
+
515
+ ---
516
+
517
+ ### 🧩 Validating Nested DTOs
518
+
519
+ ```ruby
520
+ class AddressDto < Castkit::DataObject
521
+ string :city
522
+ end
523
+
524
+ class UserDto < Castkit::DataObject
525
+ string :id
526
+ dataobject :address, of: AddressDto
527
+ end
528
+
529
+ UserContract = Castkit::Contract.from_dataobject(UserDto)
530
+ UserContract.validate!(id: "abc", address: { city: "Boston" })
531
+ ```
532
+
533
+ ---
534
+
535
+ ## Advanced Usage (coming soon)
536
+
537
+ Castkit is designed to be modular and extendable. Future guides will cover:
538
+
539
+ - Custom serializers (`Castkit::Serializers::Base`)
540
+ - Integration layers:
541
+ - `castkit-activerecord` for syncing with ActiveRecord models
542
+ - `castkit-msgpack` for binary encoding
543
+ - `castkit-oj` for high-performance JSON
544
+ - OpenAPI-compatible schema generation
545
+ - Declarative enums and union type helpers
546
+ - Circular reference detection in nested serialization
547
+
548
+ ---
549
+
550
+ ## Plugins
551
+
552
+ Castkit supports modular extensions through a lightweight plugin system. Plugins can modify or extend the behavior of `Castkit::DataObject` classes, such as adding serialization support, transformation helpers, or framework integrations.
553
+
554
+ Plugins are just Ruby modules and can be registered and activated globally or per-class.
555
+
556
+ ---
557
+
558
+ ### 📦 Activating Plugins
559
+
560
+ Plugins can be activated on any DataObject or at runtime:
561
+
562
+ ```ruby
563
+ module MyPlugin
564
+ def self.setup!(klass)
565
+ # Optional: called after inclusion
566
+ klass.string :plugin_id
567
+ end
568
+
569
+ def plugin_feature
570
+ "Enabled!"
571
+ end
572
+ end
152
573
 
153
574
  Castkit.configure do |config|
154
- config.enforce_array_of_type = true
575
+ config.register_plugin(:my_plugin, MyPlugin)
576
+ end
577
+
578
+ class MyDto < Castkit::DataObject
579
+ Castkit::Plugins.activate(self, :my_plugin)
155
580
  end
156
581
  ```
157
582
 
583
+ This includes the `MyPlugin` module into `MyDto` and calls `MyPlugin.setup!(MyDto)` if defined.
584
+
158
585
  ---
159
586
 
160
- ## 💥 Error Handling
587
+ ### 🧩 Registering Plugins
588
+
589
+ Plugins must be registered before use:
590
+
591
+ ```ruby
592
+ Castkit.configure do |config|
593
+ config.register_plugin(:oj, Castkit::Plugins::Oj)
594
+ end
595
+ ```
596
+
597
+ You can then activate them:
161
598
 
162
- - `Castkit::AttributeError` – invalid attribute value
163
- - `Castkit::DataObjectError` – object-level failure (e.g., unknown key)
164
- - `Castkit::SerializationError` – circular reference or serialization failure
599
+ ```ruby
600
+ Castkit::Plugins.activate(MyDto, :oj)
601
+ ```
165
602
 
166
603
  ---
167
604
 
168
- ## 🧪 Testing
605
+ ### 🧰 Plugin API
606
+
607
+ | Method | Description |
608
+ |------------------------------|-------------|
609
+ | `Castkit::Plugins.register(:name, mod)` | Registers a plugin under a custom name. |
610
+ | `Castkit::Plugins.activate(klass, *names)` | Includes one or more plugins into a class. |
611
+ | `Castkit::Plugins.lookup!(:name)` | Looks up the plugin by name or constant. |
612
+
613
+ ---
614
+
615
+ ### 📁 Plugin Structure
616
+
617
+ Castkit looks for plugins under the `Castkit::Plugins` namespace by default:
618
+
619
+ ```ruby
620
+ module Castkit
621
+ module Plugins
622
+ module Oj
623
+ def self.setup!(klass)
624
+ klass.include SerializationSupport
625
+ end
626
+ end
627
+ end
628
+ end
629
+ ```
630
+
631
+ To activate this:
632
+
633
+ ```ruby
634
+ Castkit::Plugins.activate(MyDto, :oj)
635
+ ```
636
+
637
+ You can also manually register plugins not under this namespace.
638
+
639
+ ---
640
+
641
+ ### ✅ Example Use Case
642
+
643
+ ```ruby
644
+ module Castkit
645
+ module Plugins
646
+ module Timestamps
647
+ def self.setup!(klass)
648
+ klass.datetime :created_at
649
+ klass.datetime :updated_at
650
+ end
651
+ end
652
+ end
653
+ end
654
+
655
+ Castkit::Plugins.activate(UserDto, :timestamps)
656
+ ```
657
+
658
+ This approach allows reusable, modular feature sets across DTOs with clean setup behavior.
659
+
660
+ ---
661
+
662
+ ## Castkit CLI
663
+
664
+ Castkit includes a command-line interface to help scaffold and inspect DTO components with ease.
665
+
666
+ The CLI is structured around two primary commands:
667
+
668
+ - `castkit generate` — scaffolds boilerplate for Castkit components.
669
+ - `castkit list` — introspects and displays registered or defined components.
670
+
671
+ ---
672
+
673
+ ## ✨ Generate Commands
674
+
675
+ The `castkit generate` command provides subcommands for creating files for all core Castkit component types.
676
+
677
+ ### 🧱 DataObject
678
+
679
+ ```bash
680
+ castkit generate dataobject User name:string age:integer
681
+ ```
682
+
683
+ Creates:
684
+
685
+ - `lib/castkit/data_objects/user.rb`
686
+ - `spec/castkit/data_objects/user_spec.rb`
687
+
688
+ ### 📄 Contract
689
+
690
+ ```bash
691
+ castkit generate contract UserInput id:string email:string
692
+ ```
693
+
694
+ Creates:
695
+
696
+ - `lib/castkit/contracts/user_input.rb`
697
+ - `spec/castkit/contracts/user_input_spec.rb`
698
+
699
+ ### 🔌 Plugin
700
+
701
+ ```bash
702
+ castkit generate plugin Oj
703
+ ```
704
+
705
+ Creates:
706
+
707
+ - `lib/castkit/plugins/oj.rb`
708
+ - `spec/castkit/plugins/oj_spec.rb`
709
+
710
+ ### 🧪 Validator
711
+
712
+ ```bash
713
+ castkit generate validator Money
714
+ ```
715
+
716
+ Creates:
717
+
718
+ - `lib/castkit/validators/money.rb`
719
+ - `spec/castkit/validators/money_spec.rb`
720
+
721
+ ### 🧬 Type
722
+
723
+ ```bash
724
+ castkit generate type money
725
+ ```
726
+
727
+ Creates:
728
+
729
+ - `lib/castkit/types/money.rb`
730
+ - `spec/castkit/types/money_spec.rb`
731
+
732
+ ### 📦 Serializer
733
+
734
+ ```bash
735
+ castkit generate serializer Json
736
+ ```
737
+
738
+ Creates:
739
+
740
+ - `lib/castkit/serializers/json.rb`
741
+ - `spec/castkit/serializers/json_spec.rb`
742
+
743
+ You can disable test generation with `--no-spec`.
744
+
745
+ ---
746
+
747
+ ## 📋 List Commands
748
+
749
+ The `castkit list` command provides an interface to view internal Castkit definitions or project-registered components.
750
+
751
+ ### 🧾 List Types
752
+
753
+ ```bash
754
+ castkit list types
755
+ ```
756
+
757
+ Displays a grouped list of:
758
+
759
+ - Native types (defined by Castkit)
760
+ - Custom types (registered via `Castkit.configure`)
761
+
762
+ Example:
763
+
764
+ ```bash
765
+ Native Types:
766
+ Castkit::Types::String - :string, :str, :uuid
767
+
768
+ Custom Types:
769
+ MyApp::Types::Money - :money
770
+ ```
771
+
772
+ ### 🔍 List Validators
773
+
774
+ ```bash
775
+ castkit list validators
776
+ ```
777
+
778
+ Displays all validator classes defined in `lib/castkit/validators` or custom-defined under `Castkit::Validators`.
779
+
780
+ Castkit validators are tagged `[Castkit]`, and others as `[Custom]`.
781
+
782
+ ### 📑 List Contracts
783
+
784
+ ```bash
785
+ castkit list contracts
786
+ ```
787
+
788
+ Lists all contracts in the `Castkit::Contracts` namespace and related files.
789
+
790
+ ### 📦 List DataObjects
791
+
792
+ ```bash
793
+ castkit list dataobjects
794
+ ```
795
+
796
+ Lists all DTOs in the `Castkit::DataObjects` namespace.
797
+
798
+ ### 🧪 List Serializers
799
+
800
+ ```bash
801
+ castkit list serializers
802
+ ```
803
+
804
+ Lists all serializer classes and their source origin.
805
+
806
+ ---
807
+
808
+ ## 🧰 Example Usage
809
+
810
+ ```bash
811
+ castkit generate dataobject Product name:string price:float
812
+ castkit generate contract ProductInput name:string
813
+
814
+ castkit list types
815
+ castkit list validators
816
+ ```
817
+
818
+ The CLI is designed to provide a familiar Rails-like generator experience, tailored for Castkit’s data-first architecture.
819
+
820
+ ---
821
+
822
+ ## Testing
823
+
824
+ You can test DTOs and Contracts by treating them like plain Ruby objects:
825
+
826
+ ```ruby
827
+ dto = MyDto.new(name: "Alice")
828
+ expect(dto.name).to eq("Alice")
829
+ ```
169
830
 
170
- You can test DTOs by instantiating them like POROs:
831
+ You can also assert validation errors:
171
832
 
172
833
  ```ruby
173
- dto = MyDto.new(input_data)
174
- expect(dto.name).to eq("expected")
834
+ expect {
835
+ MyDto.new(name: nil)
836
+ }.to raise_error(Castkit::AttributeError, /name is required/)
175
837
  ```
176
838
 
177
839
  ---
178
840
 
179
- ## 📦 Compatibility
841
+ ## Compatibility
180
842
 
181
843
  - Ruby 2.7+
182
844
  - Zero dependencies (uses core Ruby)
183
845
 
184
846
  ---
185
847
 
186
- ## 📃 License
848
+ ## License
187
849
 
188
850
  MIT. See [LICENSE](LICENSE).
189
851
 
@@ -192,4 +854,3 @@ MIT. See [LICENSE](LICENSE).
192
854
  ## 🙏 Credits
193
855
 
194
856
  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.