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.
- checksums.yaml +4 -4
- data/.rspec_status +195 -219
- data/CHANGELOG.md +42 -0
- data/README.md +744 -83
- data/castkit.gemspec +1 -0
- data/lib/castkit/attribute.rb +6 -24
- data/lib/castkit/castkit.rb +61 -10
- data/lib/castkit/cli/generate.rb +98 -0
- data/lib/castkit/cli/list.rb +200 -0
- data/lib/castkit/cli/main.rb +43 -0
- data/lib/castkit/cli.rb +24 -0
- data/lib/castkit/configuration.rb +116 -46
- data/lib/castkit/contract/base.rb +168 -0
- data/lib/castkit/contract/data_object.rb +62 -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 +56 -67
- 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/plugins.rb +86 -0
- data/lib/castkit/ext/data_object/serialization.rb +61 -0
- data/lib/castkit/inflector.rb +47 -0
- data/lib/castkit/plugins.rb +82 -0
- data/lib/castkit/serializers/base.rb +94 -0
- data/lib/castkit/serializers/default_serializer.rb +156 -0
- data/lib/castkit/types/base.rb +122 -0
- data/lib/castkit/types/boolean.rb +47 -0
- data/lib/castkit/types/collection.rb +35 -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/integer.rb +46 -0
- data/lib/castkit/types/string.rb +44 -0
- data/lib/castkit/types.rb +15 -0
- data/lib/castkit/validators/base.rb +59 -0
- data/lib/castkit/validators/boolean_validator.rb +39 -0
- data/lib/castkit/validators/collection_validator.rb +29 -0
- data/lib/castkit/validators/float_validator.rb +31 -0
- data/lib/castkit/validators/integer_validator.rb +31 -0
- data/lib/castkit/validators/numeric_validator.rb +2 -2
- data/lib/castkit/validators/string_validator.rb +3 -4
- data/lib/castkit/version.rb +1 -1
- data/lib/castkit.rb +2 -0
- data/lib/generators/base.rb +97 -0
- data/lib/generators/contract.rb +68 -0
- data/lib/generators/data_object.rb +48 -0
- data/lib/generators/plugin.rb +25 -0
- data/lib/generators/serializer.rb +28 -0
- data/lib/generators/templates/contract.rb.tt +24 -0
- data/lib/generators/templates/contract_spec.rb.tt +76 -0
- data/lib/generators/templates/data_object.rb.tt +15 -0
- data/lib/generators/templates/data_object_spec.rb.tt +36 -0
- data/lib/generators/templates/plugin.rb.tt +37 -0
- data/lib/generators/templates/plugin_spec.rb.tt +18 -0
- data/lib/generators/templates/serializer.rb.tt +24 -0
- data/lib/generators/templates/serializer_spec.rb.tt +14 -0
- data/lib/generators/templates/type.rb.tt +55 -0
- data/lib/generators/templates/type_spec.rb.tt +42 -0
- data/lib/generators/templates/validator.rb.tt +26 -0
- data/lib/generators/templates/validator_spec.rb.tt +23 -0
- data/lib/generators/type.rb +29 -0
- data/lib/generators/validator.rb +41 -0
- metadata +74 -15
- 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/default_serializer.rb +0 -123
- data/lib/castkit/serializer.rb +0 -92
- data/lib/castkit/validators.rb +0 -4
data/README.md
CHANGED
@@ -1,189 +1,851 @@
|
|
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:
|
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
|
-
|
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
|
+
- [Plugins](#plugins)
|
25
|
+
- [Castkit CLI](#castkit-cli)
|
26
|
+
- [Testing](#testing)
|
27
|
+
- [Compatibility](#compatibility)
|
28
|
+
- [License](#license)
|
19
29
|
|
20
30
|
---
|
21
31
|
|
22
|
-
##
|
32
|
+
## Configuration
|
23
33
|
|
24
|
-
|
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
|
-
|
37
|
+
Castkit.configure do |config|
|
38
|
+
config.enable_warnings = false
|
39
|
+
config.enforce_typing = true
|
40
|
+
end
|
28
41
|
```
|
29
42
|
|
30
|
-
|
43
|
+
### ⚙️ Available Settings
|
31
44
|
|
32
|
-
|
33
|
-
|
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
|
-
|
74
|
+
#### Type Aliases
|
37
75
|
|
38
|
-
|
39
|
-
|
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
|
-
##
|
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
|
-
|
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
|
-
|
56
|
-
user.to_h
|
57
|
-
# => { name: "Alice", age: 30, admin: false, profile_name: "Dev" }
|
196
|
+
This is equivalent to:
|
58
197
|
|
59
|
-
|
60
|
-
|
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
|
-
|
210
|
+
### 🧬 Unwrapped & Composite
|
66
211
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
243
|
+
### 🔍 Transient Attributes
|
82
244
|
|
83
|
-
|
245
|
+
Transient fields are excluded from serialization and can be defined in two ways:
|
84
246
|
|
85
247
|
```ruby
|
86
|
-
class
|
87
|
-
|
88
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
330
|
+
To build a relaxed version dynamically:
|
98
331
|
|
99
332
|
```ruby
|
100
|
-
|
333
|
+
LooseClone = MyDto.relaxed(warn_on_unknown: true)
|
101
334
|
```
|
102
335
|
|
103
336
|
---
|
104
337
|
|
105
|
-
|
338
|
+
### 🧱 Root Wrapping
|
106
339
|
|
107
340
|
```ruby
|
108
|
-
class
|
109
|
-
|
110
|
-
string :
|
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
|
-
|
352
|
+
### 📦 Deserialization Helpers
|
353
|
+
|
354
|
+
You can deserialize using:
|
117
355
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
356
|
+
```ruby
|
357
|
+
UserDto.from_h(hash)
|
358
|
+
UserDto.deserialize(hash)
|
359
|
+
```
|
122
360
|
|
123
361
|
---
|
124
362
|
|
125
|
-
|
363
|
+
### 🔁 Conversion from/to Contract
|
126
364
|
|
127
365
|
```ruby
|
128
|
-
|
129
|
-
|
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
|
-
|
382
|
+
{ payload: object.to_h }
|
133
383
|
end
|
134
384
|
end
|
135
385
|
|
136
|
-
class
|
137
|
-
string :
|
138
|
-
|
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
|
-
|
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
|
-
|
442
|
+
### 🧪 Validation
|
147
443
|
|
148
444
|
```ruby
|
149
|
-
|
150
|
-
|
151
|
-
|
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.
|
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
|
-
|
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
|
-
|
163
|
-
|
164
|
-
|
599
|
+
```ruby
|
600
|
+
Castkit::Plugins.activate(MyDto, :oj)
|
601
|
+
```
|
165
602
|
|
166
603
|
---
|
167
604
|
|
168
|
-
|
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
|
831
|
+
You can also assert validation errors:
|
171
832
|
|
172
833
|
```ruby
|
173
|
-
|
174
|
-
|
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
|
-
##
|
841
|
+
## Compatibility
|
180
842
|
|
181
843
|
- Ruby 2.7+
|
182
844
|
- Zero dependencies (uses core Ruby)
|
183
845
|
|
184
846
|
---
|
185
847
|
|
186
|
-
##
|
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.
|