dynamicschema 2.0.0 → 2.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/README.md +380 -171
- data/dynamicschema.gemspec +2 -2
- data/lib/dynamic_schema/builder.rb +12 -6
- data/lib/dynamic_schema/compiler.rb +14 -9
- data/lib/dynamic_schema/receiver/object.rb +257 -200
- data/lib/dynamic_schema/struct.rb +71 -20
- data/lib/dynamic_schema/validator.rb +39 -5
- metadata +6 -9
data/README.md
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
# DynamicSchema
|
|
2
2
|
|
|
3
|
-
The **DynamicSchema** gem provides an elegant and expressive way to define domain-specific
|
|
4
|
-
language (DSL) schemas, making it effortless to build and validate complex Ruby `Hash`
|
|
5
|
-
constructs.
|
|
3
|
+
The **DynamicSchema** gem provides an elegant and expressive way to define domain-specific language (DSL) schemas, making it effortless to build and validate complex Ruby `Hash` constructs.
|
|
6
4
|
|
|
7
|
-
This is particularly useful when dealing with intricate configuration or interfacing with
|
|
8
|
-
external APIs, where data structures need to adhere to specific formats and validations.
|
|
9
|
-
By allowing default values, type constraints, nested schemas, and transformations,
|
|
10
|
-
DynamicSchema ensures that your data structures are both robust and flexible.
|
|
5
|
+
This is particularly useful when dealing with intricate configuration or interfacing with external APIs, where data structures need to adhere to specific formats and validations. By allowing default values, type constraints, nested schemas, and transformations, DynamicSchema ensures that your data structures are both robust and flexible.
|
|
11
6
|
|
|
12
7
|
You can trivially define a custom schema:
|
|
13
8
|
|
|
14
9
|
```ruby
|
|
15
|
-
openai_request_schema = DynamicSchema.define do
|
|
10
|
+
openai_request_schema = DynamicSchema.define do
|
|
16
11
|
model String, default: 'gpt-4o'
|
|
17
12
|
max_tokens Integer, default: 1024
|
|
18
13
|
temperature Float, in: 0..1
|
|
19
14
|
|
|
20
|
-
message arguments: [ :role ], as: :messages, array: true do
|
|
15
|
+
message arguments: [ :role ], as: :messages, array: true do
|
|
21
16
|
role Symbol, in: [ :system, :user, :assistant ]
|
|
22
|
-
content array: true do
|
|
23
|
-
type Symbol, default: :text
|
|
17
|
+
content array: true do
|
|
18
|
+
type Symbol, default: :text
|
|
24
19
|
text String
|
|
25
20
|
end
|
|
26
21
|
end
|
|
@@ -30,10 +25,10 @@ end
|
|
|
30
25
|
And then repeatedly use that schema to elegantly build a schema-conformant `Hash`:
|
|
31
26
|
```ruby
|
|
32
27
|
request = openai_request_schema.build {
|
|
33
|
-
message :system do
|
|
28
|
+
message :system do
|
|
34
29
|
content text: "You are a helpful assistant that talks like a pirate."
|
|
35
30
|
end
|
|
36
|
-
message :user do
|
|
31
|
+
message :user do
|
|
37
32
|
content text: ARGV[0] || "say hello!"
|
|
38
33
|
end
|
|
39
34
|
}
|
|
@@ -50,6 +45,9 @@ You can find a full OpenAI request example in the `/examples` folder of this rep
|
|
|
50
45
|
- [Values](#values)
|
|
51
46
|
- [Objects](#objects)
|
|
52
47
|
- [Types](#types)
|
|
48
|
+
- [Custom Types](#custom-types)
|
|
49
|
+
- [Multiple Types](#multiple-types)
|
|
50
|
+
- [Multiple Types with Nested Schema](#multiple-types-with-nested-schema)
|
|
53
51
|
- [Options](#options)
|
|
54
52
|
- [default Option](#default-option)
|
|
55
53
|
- [required Option](#required-option)
|
|
@@ -57,12 +55,13 @@ You can find a full OpenAI request example in the `/examples` folder of this rep
|
|
|
57
55
|
- [as Option](#as-option)
|
|
58
56
|
- [in Option (Values Only)](#in-option)
|
|
59
57
|
- [arguments Option](#arguments-option)
|
|
58
|
+
- [normalize Option](#normalize-option)
|
|
60
59
|
- [Class Schema](#class-schemas)
|
|
61
|
-
- [Definable](#definable)
|
|
62
|
-
- [Buildable](#buildable)
|
|
60
|
+
- [Definable](#definable)
|
|
61
|
+
- [Buildable](#buildable)
|
|
63
62
|
- [Struct](#struct)
|
|
64
63
|
- [Validation Methods](#validation-methods)
|
|
65
|
-
- [Validation Rules](#validation-rules)
|
|
64
|
+
- [Validation Rules](#validation-rules)
|
|
66
65
|
- [validate!](#validate)
|
|
67
66
|
- [validate](#validate-1)
|
|
68
67
|
- [valid?](#valid)
|
|
@@ -104,12 +103,11 @@ require 'dynamic_schema'
|
|
|
104
103
|
|
|
105
104
|
### Defining Schemas with **DynamicSchema**
|
|
106
105
|
|
|
107
|
-
DynamicSchema lets you define a DSL made of values, objects, and options, then reuse that DSL to
|
|
108
|
-
build and validate Ruby Hashes.
|
|
106
|
+
DynamicSchema lets you define a DSL made of values, objects, and options, then reuse that DSL to build and validate Ruby Hashes.
|
|
109
107
|
|
|
110
|
-
You start by constructing a `DynamicSchema::Builder`. You can do this by calling:
|
|
111
|
-
- `DynamicSchema.define { … }`
|
|
112
|
-
- `DynamicSchema::Builder.new.define { … }`
|
|
108
|
+
You start by constructing a `DynamicSchema::Builder`. You can do this by calling:
|
|
109
|
+
- `DynamicSchema.define { … }`
|
|
110
|
+
- `DynamicSchema::Builder.new.define { … }`
|
|
113
111
|
|
|
114
112
|
In both cases, you pass a block that declares the schema values and objects with their options.
|
|
115
113
|
|
|
@@ -153,9 +151,7 @@ valid = schema.valid?( { api_key: 'secret', chat_options: { temperature: 0.7 }
|
|
|
153
151
|
|
|
154
152
|
#### Inheritance
|
|
155
153
|
|
|
156
|
-
You can extend an existing schema using the `inherit:` option. Pass a Proc that describes
|
|
157
|
-
the parent schema—typically from a class that includes `DynamicSchema::Definable` via
|
|
158
|
-
its `schema` method.
|
|
154
|
+
You can extend an existing schema using the `inherit:` option. Pass a Proc that describes the parent schema—typically from a class that includes `DynamicSchema::Definable` via its `schema` method.
|
|
159
155
|
|
|
160
156
|
```ruby
|
|
161
157
|
class BaseSettings
|
|
@@ -182,13 +178,9 @@ You can call `build`, `build!`, `validate`, `validate!`, and `valid?` on the bui
|
|
|
182
178
|
|
|
183
179
|
## Struct
|
|
184
180
|
|
|
185
|
-
In addition to building plain Ruby `Hash` values, DynamicSchema can generate lightweight
|
|
186
|
-
Ruby classes from a schema. A `DynamicSchema::Struct` exposes readers and writers for the
|
|
187
|
-
fields you define, and transparently wraps nested objects so that you can access them with
|
|
188
|
-
dot-style accessors rather than deep hash indexing.
|
|
181
|
+
In addition to building plain Ruby `Hash` values, DynamicSchema can generate lightweight Ruby classes from a schema. A `DynamicSchema::Struct` exposes readers and writers for the fields you define, and transparently wraps nested objects so that you can access them with dot-style accessors rather than deep hash indexing.
|
|
189
182
|
|
|
190
|
-
You create a struct class by passing the same schema shape you would give to a Builder. The
|
|
191
|
-
schema can be provided as:
|
|
183
|
+
You create a struct class by passing the same schema shape you would give to a Builder. The schema can be provided as:
|
|
192
184
|
|
|
193
185
|
- a `Proc` that defines the schema
|
|
194
186
|
- a `DynamicSchema::Builder`
|
|
@@ -253,18 +245,15 @@ collection.line_items[ 1 ].quantity # => 2
|
|
|
253
245
|
- defining
|
|
254
246
|
- `DynamicSchema::Struct.define` takes a block that looks exactly like a Builder schema.
|
|
255
247
|
- Use `array: true` to expose arrays of nested structs.
|
|
256
|
-
- You may reference another struct class as a value type; arrays of that type expose
|
|
257
|
-
nested accessors for each element.
|
|
248
|
+
- You may reference another struct class as a value type; arrays of that type expose nested accessors for each element.
|
|
258
249
|
- building
|
|
259
|
-
- `StructClass.build( attributes )` constructs an instance and (optionally) coerces typed
|
|
260
|
-
scalar fields using the same converters as the Builder.
|
|
250
|
+
- `StructClass.build( attributes )` constructs an instance and (optionally) coerces typed scalar fields using the same converters as the Builder.
|
|
261
251
|
- `StructClass.build!` additionally validates the instance just like `builder.build!`.
|
|
262
252
|
- accessing
|
|
263
253
|
- Use standard Ruby readers/writers: `instance.attribute`, `instance.attribute = value`.
|
|
264
254
|
- `#to_h` returns a deep Hash of the current values (nested structs become hashes).
|
|
265
255
|
|
|
266
|
-
You can also create a struct class from a builder or a compiled hash if you already have a
|
|
267
|
-
schema elsewhere:
|
|
256
|
+
You can also create a struct class from a builder or a compiled hash if you already have a schema elsewhere:
|
|
268
257
|
|
|
269
258
|
```ruby
|
|
270
259
|
builder = DynamicSchema.define do
|
|
@@ -282,16 +271,13 @@ NameStruct.build( name: 'Taylor' ).name # => 'Taylor'
|
|
|
282
271
|
|
|
283
272
|
---
|
|
284
273
|
|
|
285
|
-
## Values
|
|
274
|
+
## Values
|
|
286
275
|
|
|
287
|
-
A *value* is a basic building block of your schema. Values represent individual settings,
|
|
288
|
-
options or API parameters that you can define with specific types, defaults, and other options.
|
|
276
|
+
A *value* is a basic building block of your schema. Values represent individual settings, options or API parameters that you can define with specific types, defaults, and other options.
|
|
289
277
|
|
|
290
|
-
When defining a value, you provide the name as though you were calling a Ruby method, with
|
|
291
|
-
arguments that include an optional type (which can be a `Class`, `Module` or an `Array` of these)
|
|
292
|
-
as well as a `Hash` of options, all of which are optional:
|
|
278
|
+
When defining a value, you provide the name as though you were calling a Ruby method, with arguments that include an optional type (which can be a `Class`, `Module` or an `Array` of these) as well as a `Hash` of options, all of which are optional:
|
|
293
279
|
|
|
294
|
-
`name {type} default: {
|
|
280
|
+
`name {type}, default: {value}, required: {true|false}, array: {true|false}, as: {name}, in: {Array|Range}`
|
|
295
281
|
|
|
296
282
|
#### example:
|
|
297
283
|
|
|
@@ -301,7 +287,7 @@ require 'dynamic_schema'
|
|
|
301
287
|
# define a schema structure with values
|
|
302
288
|
schema = DynamicSchema.define do
|
|
303
289
|
api_key
|
|
304
|
-
version
|
|
290
|
+
version String, default: '1.0'
|
|
305
291
|
end
|
|
306
292
|
|
|
307
293
|
# build the schema and set values
|
|
@@ -316,30 +302,26 @@ puts result[:version] # => "1.0"
|
|
|
316
302
|
|
|
317
303
|
- defining
|
|
318
304
|
- `api_key` defines a value named `api_key`. Any type can be used to assign the value.
|
|
319
|
-
- `version
|
|
320
|
-
- building
|
|
305
|
+
- `version String, default: '1.0'` defines a value with a default.
|
|
306
|
+
- building
|
|
321
307
|
- `schema.build!` accepts both a Hash and a block where you can set the values.
|
|
322
308
|
- Inside the block, `api_key 'your-api-key'` sets the value of `api_key`.
|
|
323
|
-
- accessing
|
|
309
|
+
- accessing
|
|
324
310
|
- `result[:api_key]` retrieves the value of `api_key`.
|
|
325
|
-
- If a value has a default and you don't set it, the default value will be included in
|
|
326
|
-
resulting hash.
|
|
311
|
+
- If a value has a default and you don't set it, the default value will be included in resulting hash.
|
|
327
312
|
|
|
328
313
|
---
|
|
329
314
|
|
|
330
315
|
## Objects
|
|
331
316
|
|
|
332
|
-
A schema may be organized hierarchically, by creating collections of related values and
|
|
333
|
-
even other collections. These collections are called objects.
|
|
317
|
+
A schema may be organized hierarchically, by creating collections of related values and even other collections. These collections are called objects.
|
|
334
318
|
|
|
335
|
-
An *object* is defined in a similar manner to a value. Simply provide the name as though
|
|
336
|
-
calling a Ruby method, with a Hash of options and a block which encloses the child values
|
|
337
|
-
and objects:
|
|
319
|
+
An *object* is defined in a similar manner to a value. Simply provide the name as though calling a Ruby method, with a Hash of options and a block which encloses the child values and objects:
|
|
338
320
|
|
|
339
321
|
```
|
|
340
|
-
name arguments: [ {argument} ], default: {
|
|
322
|
+
name arguments: [ {argument} ], default: {value}, required: {true|false}, array: {true|false}, as: {name} do
|
|
341
323
|
# child values and objects can be defined here
|
|
342
|
-
end
|
|
324
|
+
end
|
|
343
325
|
```
|
|
344
326
|
|
|
345
327
|
Notice an *object* does not accept a type as it is always of type `Object`.
|
|
@@ -350,7 +332,7 @@ Notice an *object* does not accept a type as it is always of type `Object`.
|
|
|
350
332
|
require 'dynamic_schema'
|
|
351
333
|
|
|
352
334
|
schema = DynamicSchema.define do
|
|
353
|
-
api_key
|
|
335
|
+
api_key String
|
|
354
336
|
chat_options do
|
|
355
337
|
model String, default: 'claude-3'
|
|
356
338
|
max_tokens Integer, default: 1024
|
|
@@ -377,7 +359,7 @@ puts result[:chat_options][:stream] # => true
|
|
|
377
359
|
- defining
|
|
378
360
|
- `chat_options do ... end` defines an object named `chat_options`.
|
|
379
361
|
- Inside the object you can define values that belong to that object.
|
|
380
|
-
- building
|
|
362
|
+
- building
|
|
381
363
|
- In the build block, you can set values for values within objects by nesting blocks.
|
|
382
364
|
- `chat_options do ... end` allows you to set values inside the `chat_options` object.
|
|
383
365
|
- accessing
|
|
@@ -385,11 +367,9 @@ puts result[:chat_options][:stream] # => true
|
|
|
385
367
|
|
|
386
368
|
---
|
|
387
369
|
|
|
388
|
-
## Types
|
|
370
|
+
## Types
|
|
389
371
|
|
|
390
|
-
An *object* is always of type `Object`. A *value* can have no type or it can be of one or
|
|
391
|
-
more types. You specify the value type by providing an instance of a `Class` when defining
|
|
392
|
-
the value. If you want to specify multiple types simply provide an array of types.
|
|
372
|
+
An *object* is always of type `Object`. A *value* can have no type or it can be of one or more types. You specify the value type by providing an instance of a `Class` when defining the value. If you want to specify multiple types simply provide an array of types.
|
|
393
373
|
|
|
394
374
|
#### example:
|
|
395
375
|
|
|
@@ -398,37 +378,209 @@ require 'dynamic_schema'
|
|
|
398
378
|
|
|
399
379
|
schema = DynamicSchema.define do
|
|
400
380
|
typeless_value
|
|
401
|
-
symbol_value Symbol
|
|
381
|
+
symbol_value Symbol
|
|
402
382
|
boolean_value [ TrueClass, FalseClass ]
|
|
403
383
|
end
|
|
404
384
|
|
|
405
385
|
result = schema.build! do
|
|
406
386
|
typeless_value Struct.new(:name).new(name: 'Kristoph')
|
|
407
387
|
symbol_value "something"
|
|
408
|
-
boolean_value true
|
|
409
|
-
end
|
|
388
|
+
boolean_value true
|
|
389
|
+
end
|
|
410
390
|
|
|
411
391
|
puts result[:typeless_value].name # => "Kristoph"
|
|
412
392
|
puts result[:symbol_value] # => :something
|
|
413
|
-
puts result[:boolean_value] # => true
|
|
393
|
+
puts result[:boolean_value] # => true
|
|
414
394
|
```
|
|
415
395
|
|
|
416
396
|
- defining
|
|
417
|
-
- `typeless_value` defines a value that has no type and will accept an assignment of any type
|
|
418
|
-
- `symbol_value` defines a value that accepts symbols or types that can be coerced into
|
|
419
|
-
symbols, such as strings (see **Type Coercion**)
|
|
397
|
+
- `typeless_value` defines a value that has no type and will accept an assignment of any type
|
|
398
|
+
- `symbol_value` defines a value that accepts symbols or types that can be coerced into symbols, such as strings (see **Type Coercion**)
|
|
420
399
|
- `boolean_value` defines a value that can be either `true` or `false`
|
|
421
400
|
|
|
422
|
-
|
|
401
|
+
### Custom Types
|
|
402
|
+
|
|
403
|
+
You can use any Ruby class as a value type, not just the built-in types. When a custom class is specified as the type, DynamicSchema will validate that values are instances of that class. You can also configure custom class instances using blocks:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
require 'dynamic_schema'
|
|
407
|
+
|
|
408
|
+
class Customer
|
|
409
|
+
attr_accessor :name, :email
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
schema = DynamicSchema.define do
|
|
413
|
+
customer Customer
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# auto-instantiate and configure with a block
|
|
417
|
+
result = schema.build! do
|
|
418
|
+
customer do
|
|
419
|
+
name 'Alice'
|
|
420
|
+
email 'alice@example.com'
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
result[:customer].name # => 'Alice'
|
|
425
|
+
result[:customer].email # => 'alice@example.com'
|
|
426
|
+
|
|
427
|
+
# or provide an existing instance
|
|
428
|
+
existing = Customer.new
|
|
429
|
+
existing.name = 'Bob'
|
|
430
|
+
|
|
431
|
+
result = schema.build! do
|
|
432
|
+
customer existing do
|
|
433
|
+
email 'bob@example.com'
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
result[:customer].name # => 'Bob'
|
|
438
|
+
result[:customer].email # => 'bob@example.com'
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
When using a block with a custom type:
|
|
442
|
+
- If no instance is provided, DynamicSchema will call `YourClass.new` to create one
|
|
443
|
+
- Inside the block, method calls are translated to setter calls on the instance
|
|
444
|
+
- You can provide an existing instance and still use a block to configure it further
|
|
445
|
+
|
|
446
|
+
This is particularly useful when integrating with `DynamicSchema::Struct` or other custom classes that need to be configured within a schema.
|
|
447
|
+
|
|
448
|
+
### Multiple Types
|
|
449
|
+
|
|
450
|
+
You can specify multiple types for a value by providing an array of types. The value must match one of the listed types.
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
schema = DynamicSchema.define do
|
|
454
|
+
enabled [ TrueClass, FalseClass ]
|
|
455
|
+
identifier [ String, Integer ]
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
result = schema.build! do
|
|
459
|
+
enabled true
|
|
460
|
+
identifier 12345
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
result[ :enabled ] # => true
|
|
464
|
+
result[ :identifier ] # => 12345
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Validation ensures the value matches one of the specified types:
|
|
423
468
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
469
|
+
```ruby
|
|
470
|
+
schema.valid?( { enabled: true } ) # => true
|
|
471
|
+
schema.valid?( { enabled: 'yes' } ) # => false (string not in types)
|
|
472
|
+
schema.valid?( { identifier: 'abc' } ) # => true
|
|
473
|
+
schema.valid?( { identifier: 123 } ) # => true
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Multiple Types with Nested Schema
|
|
477
|
+
|
|
478
|
+
When you combine multiple types with a block, you create a field that can be either a nested object (defined by the block) or one of the scalar types. The decision is made at runtime:
|
|
479
|
+
|
|
480
|
+
- **Hash values** (or blocks in Builder) are processed using the nested schema
|
|
481
|
+
- **Non-hash values** are validated against the scalar types in the array
|
|
482
|
+
|
|
483
|
+
This is useful for APIs where a field might be either a structured object or a simple value like a boolean.
|
|
484
|
+
|
|
485
|
+
#### Builder Example
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
schema = DynamicSchema.define do
|
|
489
|
+
# 'data' can be either a nested object OR true/false
|
|
490
|
+
data [ Object, TrueClass, FalseClass ] do
|
|
491
|
+
name String
|
|
492
|
+
value Integer
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Using as a nested object (with block)
|
|
497
|
+
result = schema.build! do
|
|
498
|
+
data do
|
|
499
|
+
name 'example'
|
|
500
|
+
value 42
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
result[ :data ][ :name ] # => 'example'
|
|
504
|
+
|
|
505
|
+
# Using as a nested object (with hash)
|
|
506
|
+
result = schema.build! do
|
|
507
|
+
data( { name: 'from hash', value: 100 } )
|
|
508
|
+
end
|
|
509
|
+
result[ :data ][ :name ] # => 'from hash'
|
|
510
|
+
|
|
511
|
+
# Using as a boolean
|
|
512
|
+
result = schema.build! do
|
|
513
|
+
data true
|
|
514
|
+
end
|
|
515
|
+
result[ :data ] # => true
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### Struct Example
|
|
519
|
+
|
|
520
|
+
```ruby
|
|
521
|
+
Settings = DynamicSchema::Struct.define do
|
|
522
|
+
config [ Object, TrueClass, FalseClass ] do
|
|
523
|
+
host String
|
|
524
|
+
port Integer
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# With nested object - accessed via dot notation
|
|
529
|
+
settings = Settings.build( config: { host: 'localhost', port: 8080 } )
|
|
530
|
+
settings.config.host # => 'localhost'
|
|
531
|
+
settings.config.port # => 8080
|
|
532
|
+
|
|
533
|
+
# With boolean - returned as-is
|
|
534
|
+
settings = Settings.build( config: false )
|
|
535
|
+
settings.config # => false
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### Arrays with Multiple Types
|
|
427
539
|
|
|
428
|
-
|
|
540
|
+
Combine with `array: true` to create arrays where each element can be either a nested object or a scalar:
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
schema = DynamicSchema.define do
|
|
544
|
+
items [ Object, TrueClass, FalseClass ], array: true do
|
|
545
|
+
name String
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
result = schema.build! do
|
|
550
|
+
items( { name: 'first' } )
|
|
551
|
+
items true
|
|
552
|
+
items( { name: 'second' } )
|
|
553
|
+
items false
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
result[ :items ] # => [ { name: 'first' }, true, { name: 'second' }, false ]
|
|
557
|
+
```
|
|
429
558
|
|
|
430
|
-
|
|
431
|
-
|
|
559
|
+
#### Validation
|
|
560
|
+
|
|
561
|
+
Validation checks that hash values conform to the nested schema and non-hash values match one of the scalar types:
|
|
562
|
+
|
|
563
|
+
```ruby
|
|
564
|
+
schema = DynamicSchema.define do
|
|
565
|
+
data [ Object, TrueClass, FalseClass ] do
|
|
566
|
+
value Integer
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
schema.valid?( { data: { value: 42 } } ) # => true (valid nested object)
|
|
571
|
+
schema.valid?( { data: true } ) # => true (valid boolean)
|
|
572
|
+
schema.valid?( { data: 'invalid' } ) # => false (string not in types)
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Options
|
|
578
|
+
|
|
579
|
+
Both *values* and *objects* can be customized through *options*. The options for both values and objects include `default`, `required`, `as` and `array`. In addition values support the `in` criteria option while objects support the `arguments` option.
|
|
580
|
+
|
|
581
|
+
### :default Option
|
|
582
|
+
|
|
583
|
+
The `:default` option allows you to specify a default value that will be used if no value is provided during build.
|
|
432
584
|
|
|
433
585
|
#### example:
|
|
434
586
|
|
|
@@ -445,9 +597,7 @@ puts result[:timeout] # => 30
|
|
|
445
597
|
|
|
446
598
|
### :required Option
|
|
447
599
|
|
|
448
|
-
The `:required` option ensures that a value must be provided when building the schema. If a
|
|
449
|
-
required value is missing when using `build!`, `validate`, or `validate!`,
|
|
450
|
-
a `DynamicSchema::RequiredOptionError` will be raised.
|
|
600
|
+
The `:required` option ensures that a value must be provided when building the schema. If a required value is missing when using `build!`, `validate`, or `validate!`, a `DynamicSchema::RequiredOptionError` will be raised.
|
|
451
601
|
|
|
452
602
|
#### example:
|
|
453
603
|
|
|
@@ -468,9 +618,7 @@ end
|
|
|
468
618
|
|
|
469
619
|
### :array Option
|
|
470
620
|
|
|
471
|
-
The `:array` option wraps the value or object in an array in the resulting Hash, even if only
|
|
472
|
-
one value is provided. This is particularly useful when dealing with APIs that expect array
|
|
473
|
-
inputs.
|
|
621
|
+
The `:array` option wraps the value or object in an array in the resulting Hash, even if only one value is provided. This is particularly useful when dealing with APIs that expect array inputs.
|
|
474
622
|
|
|
475
623
|
#### example:
|
|
476
624
|
|
|
@@ -496,9 +644,7 @@ puts result[:message] # => [{ text: "Hello world", type: "plain" }]
|
|
|
496
644
|
|
|
497
645
|
### :as Option
|
|
498
646
|
|
|
499
|
-
The `:as` option allows you to use a different name in the DSL than what appears in the final
|
|
500
|
-
Hash. This is particularly useful when interfacing with APIs that have specific key
|
|
501
|
-
requirements.
|
|
647
|
+
The `:as` option allows you to use a different name in the DSL than what appears in the final Hash. This is particularly useful when interfacing with APIs that have specific key requirements.
|
|
502
648
|
|
|
503
649
|
#### example:
|
|
504
650
|
|
|
@@ -518,8 +664,7 @@ puts result["Authorization"] # => "Bearer abc123"
|
|
|
518
664
|
|
|
519
665
|
### :in Option
|
|
520
666
|
|
|
521
|
-
The `:in` option provides validation for values, ensuring they fall within a specified Range or
|
|
522
|
-
are included in an Array of allowed values. This option is only available for values.
|
|
667
|
+
The `:in` option provides validation for values, ensuring they fall within a specified Range or are included in an Array of allowed values. This option is only available for values.
|
|
523
668
|
|
|
524
669
|
#### example:
|
|
525
670
|
|
|
@@ -550,12 +695,9 @@ end
|
|
|
550
695
|
|
|
551
696
|
### :arguments Option
|
|
552
697
|
|
|
553
|
-
The `:arguments` option allows objects to accept arguments when building. Any arguments provided
|
|
554
|
-
must appear when the object is built ( and so are implicitly 'required' ).
|
|
698
|
+
The `:arguments` option allows objects to accept arguments when building. Any arguments provided must appear when the object is built (and so are implicitly 'required').
|
|
555
699
|
|
|
556
|
-
If
|
|
557
|
-
block, the assignemnt in the block will take priority, followed by the attributes assigned and
|
|
558
|
-
finally the argument.
|
|
700
|
+
If an argument is provided, the same argument appears in the attributes hash, or in the object block, the assignment in the block will take priority, followed by the attributes assigned and finally the argument.
|
|
559
701
|
|
|
560
702
|
#### example:
|
|
561
703
|
|
|
@@ -577,131 +719,200 @@ result = schema.build! do
|
|
|
577
719
|
end
|
|
578
720
|
```
|
|
579
721
|
|
|
722
|
+
### :normalize Option
|
|
723
|
+
|
|
724
|
+
The `:normalize` option allows you to transform values immediately upon assignment. It accepts a lambda or Proc that receives the value and returns the transformed result. When used with arrays, the normalizer is called for each individual item, not the array itself.
|
|
725
|
+
|
|
726
|
+
#### example:
|
|
727
|
+
|
|
728
|
+
```ruby
|
|
729
|
+
schema = DynamicSchema.define do
|
|
730
|
+
name String, normalize: ->( v ) { v.strip.downcase }
|
|
731
|
+
tags String, array: true, normalize: ->( v ) { v.upcase }
|
|
732
|
+
score Integer, normalize: ->( v ) { v.clamp( 0, 100 ) }
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
result = schema.build! do
|
|
736
|
+
name ' Alice '
|
|
737
|
+
tags [ 'important', 'urgent' ]
|
|
738
|
+
score 150
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
puts result[:name] # => "alice"
|
|
742
|
+
puts result[:tags] # => ["IMPORTANT", "URGENT"]
|
|
743
|
+
puts result[:score] # => 100
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
The normalizer is applied after type coercion, so you receive the properly typed value:
|
|
747
|
+
|
|
748
|
+
```ruby
|
|
749
|
+
schema = DynamicSchema.define do
|
|
750
|
+
count Integer, normalize: ->( v ) { v * 2 }
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
result = schema.build! do
|
|
754
|
+
count '5' # coerced to Integer first, then normalized
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
puts result[:count] # => 10
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
You can also use normalize with objects to transform the entire nested structure:
|
|
761
|
+
|
|
762
|
+
```ruby
|
|
763
|
+
CustomUser = Struct.new( :name, :email )
|
|
764
|
+
|
|
765
|
+
schema = DynamicSchema.define do
|
|
766
|
+
user normalize: ->( v ) { CustomUser.new( v.to_h[:name], v.to_h[:email] ) } do
|
|
767
|
+
name String
|
|
768
|
+
email String
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
result = schema.build! do
|
|
773
|
+
user do
|
|
774
|
+
name 'Alice'
|
|
775
|
+
email 'alice@example.com'
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
result[:user].class # => CustomUser
|
|
780
|
+
result[:user].name # => "Alice"
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
The normalize option works with Struct as well:
|
|
784
|
+
|
|
785
|
+
```ruby
|
|
786
|
+
Person = DynamicSchema::Struct.define do
|
|
787
|
+
name String, normalize: ->( v ) { v.strip.capitalize }
|
|
788
|
+
tags String, array: true, normalize: ->( v ) { v.downcase }
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
person = Person.build( name: ' alice ', tags: [ 'ADMIN', 'USER' ] )
|
|
792
|
+
person.name # => "Alice"
|
|
793
|
+
person.tags # => ["admin", "user"]
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Note that any exceptions raised within a normalize lambda are not captured and will propagate to the caller:
|
|
797
|
+
|
|
798
|
+
```ruby
|
|
799
|
+
schema = DynamicSchema.define do
|
|
800
|
+
value Integer, normalize: ->( v ) { raise "invalid value" if v < 0; v }
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# This will raise RuntimeError: "invalid value"
|
|
804
|
+
schema.build! { value -1 }
|
|
805
|
+
```
|
|
806
|
+
|
|
580
807
|
## Class Schemas
|
|
581
808
|
|
|
582
|
-
DynamicSchema provides a number of modules you can include into your own classes to simplify
|
|
583
|
-
their definition and construction.
|
|
809
|
+
DynamicSchema provides a number of modules you can include into your own classes to simplify their definition and construction.
|
|
584
810
|
|
|
585
|
-
### Definable
|
|
811
|
+
### Definable
|
|
586
812
|
|
|
587
|
-
The `Definable` module, when included in a class, will add the `schema` and the `builder` class
|
|
588
|
-
methods.
|
|
813
|
+
The `Definable` module, when included in a class, will add the `schema` and the `builder` class methods.
|
|
589
814
|
|
|
590
|
-
By calling `schema` with a block you can define a schema for that specific class. You may also
|
|
591
|
-
retrieve the defined schema by calling 'schema' ( with or without a block ). The 'schema' method
|
|
592
|
-
may be called repeatedly to build up a schema with each call adding to the existing schema
|
|
593
|
-
( replacing values and objects of the same name if they appear in subsequent calls ).
|
|
815
|
+
By calling `schema` with a block you can define a schema for that specific class. You may also retrieve the defined schema by calling 'schema' (with or without a block). The 'schema' method may be called repeatedly to build up a schema with each call adding to the existing schema (replacing values and objects of the same name if they appear in subsequent calls).
|
|
594
816
|
|
|
595
|
-
The `schema` method will integrate with a class hierarchy. By including Definable in a base class
|
|
596
|
-
you can call `schema` to define a schema for that base class and then in subsequent derived classes
|
|
597
|
-
to augment it for those classes.
|
|
817
|
+
The `schema` method will integrate with a class hierarchy. By including Definable in a base class you can call `schema` to define a schema for that base class and then in subsequent derived classes to augment it for those classes.
|
|
598
818
|
|
|
599
|
-
The `builder` method will return a memoized builder of the schema defined by calls to the `schema`
|
|
600
|
-
method which can be used to build and validate schema conformant hashes.
|
|
819
|
+
The `builder` method will return a memoized builder of the schema defined by calls to the `schema` method which can be used to build and validate schema conformant hashes.
|
|
601
820
|
|
|
602
|
-
```ruby
|
|
603
|
-
class Setting
|
|
604
|
-
include DynamicSchema::Definable
|
|
605
|
-
schema do
|
|
606
|
-
name String
|
|
607
|
-
end
|
|
608
|
-
end
|
|
821
|
+
```ruby
|
|
822
|
+
class Setting
|
|
823
|
+
include DynamicSchema::Definable
|
|
824
|
+
schema do
|
|
825
|
+
name String
|
|
826
|
+
end
|
|
827
|
+
end
|
|
609
828
|
|
|
610
|
-
class
|
|
611
|
-
schema do
|
|
612
|
-
database do
|
|
829
|
+
class DatabaseSetting < Setting
|
|
830
|
+
schema do
|
|
831
|
+
database do
|
|
613
832
|
host String
|
|
614
|
-
port String
|
|
615
|
-
name String
|
|
616
|
-
end
|
|
617
|
-
end
|
|
833
|
+
port String
|
|
834
|
+
name String
|
|
835
|
+
end
|
|
836
|
+
end
|
|
618
837
|
|
|
619
838
|
def initialize( attributes = {} )
|
|
620
|
-
# validate the attributes
|
|
839
|
+
# validate the attributes
|
|
621
840
|
self.class.builder.validate!( attributes )
|
|
622
|
-
# retain them for future access
|
|
623
|
-
@attributes = attributes&.dup
|
|
841
|
+
# retain them for future access
|
|
842
|
+
@attributes = attributes&.dup
|
|
624
843
|
end
|
|
625
844
|
|
|
626
|
-
end
|
|
845
|
+
end
|
|
627
846
|
```
|
|
628
847
|
|
|
629
|
-
### Buildable
|
|
848
|
+
### Buildable
|
|
630
849
|
|
|
631
|
-
The `Buildable` module can be included in a class, in addition to `Definable`, to facilitate
|
|
632
|
-
building that class using a schema assisted builder pattern. The `Buildable` module adds
|
|
633
|
-
`build!` and `build` methods to the class which can be used to build that class, with and
|
|
634
|
-
without validation respectively.
|
|
850
|
+
The `Buildable` module can be included in a class, in addition to `Definable`, to facilitate building that class using a schema assisted builder pattern. The `Buildable` module adds `build!` and `build` methods to the class which can be used to build that class, with and without validation respectively.
|
|
635
851
|
|
|
636
|
-
These methods accept both a Hash with attributes that follow the schema, as well as a block
|
|
637
|
-
that can be used to build the class instance. The attributes and block can be used simultaneously.
|
|
852
|
+
These methods accept both a Hash with attributes that follow the schema, as well as a block that can be used to build the class instance. The attributes and block can be used simultaneously.
|
|
638
853
|
|
|
639
|
-
**Important** Note that `Buildable` requires a class method `builder` (
|
|
640
|
-
provides ) and an initializer that accepts a `Hash` of attributes.
|
|
854
|
+
**Important** Note that `Buildable` requires a class method `builder` (which `Definable` provides) and an initializer that accepts a `Hash` of attributes.
|
|
641
855
|
|
|
642
856
|
```ruby
|
|
643
|
-
class Setting
|
|
644
|
-
include DynamicSchema::Definable
|
|
857
|
+
class Setting
|
|
858
|
+
include DynamicSchema::Definable
|
|
645
859
|
include DynamicSchema::Buildable
|
|
646
|
-
schema do
|
|
647
|
-
name String
|
|
648
|
-
end
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
class DatabaseSetting < Setting
|
|
652
|
-
schema do
|
|
653
|
-
database do
|
|
860
|
+
schema do
|
|
861
|
+
name String
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
class DatabaseSetting < Setting
|
|
866
|
+
schema do
|
|
867
|
+
database do
|
|
654
868
|
adapter Symbol
|
|
655
869
|
host String
|
|
656
|
-
port String
|
|
657
|
-
name String
|
|
658
|
-
end
|
|
659
|
-
end
|
|
870
|
+
port String
|
|
871
|
+
name String
|
|
872
|
+
end
|
|
873
|
+
end
|
|
660
874
|
|
|
661
875
|
def initialize( attributes = {} )
|
|
662
|
-
# validate the attributes
|
|
876
|
+
# validate the attributes
|
|
663
877
|
self.class.builder.validate!( attributes )
|
|
664
|
-
# retain them for the future
|
|
665
|
-
@attributes = attributes&.dup
|
|
878
|
+
# retain them for the future
|
|
879
|
+
@attributes = attributes&.dup
|
|
666
880
|
end
|
|
667
|
-
end
|
|
881
|
+
end
|
|
668
882
|
|
|
669
|
-
database_settings = DatabaseSetting.build! name: 'settings.database' do
|
|
670
|
-
database adapter: :pg do
|
|
883
|
+
database_settings = DatabaseSetting.build! name: 'settings.database' do
|
|
884
|
+
database adapter: :pg do
|
|
671
885
|
host "localhost"
|
|
672
886
|
port "127.0.0.1"
|
|
673
887
|
name "mydb"
|
|
674
|
-
end
|
|
888
|
+
end
|
|
675
889
|
end
|
|
676
890
|
```
|
|
677
891
|
|
|
678
892
|
## Validation
|
|
679
893
|
|
|
680
|
-
DynamicSchema provides three different methods for validating Hash structures against your
|
|
681
|
-
defined schema: `validate!`, `validate`, and `valid?`.
|
|
894
|
+
DynamicSchema provides three different methods for validating Hash structures against your defined schema: `validate!`, `validate`, and `valid?`.
|
|
682
895
|
|
|
683
|
-
These methods allow you to verify that your data conforms to your schema requirements,
|
|
684
|
-
including type constraints, required fields, and value ranges.
|
|
896
|
+
These methods allow you to verify that your data conforms to your schema requirements, including type constraints, required fields, and value ranges.
|
|
685
897
|
|
|
686
898
|
### Validation Rules
|
|
687
899
|
|
|
688
900
|
When validating, DynamicSchema checks:
|
|
689
901
|
|
|
690
|
-
1. **Required Fields**:
|
|
902
|
+
1. **Required Fields**:
|
|
691
903
|
Any value or object marked as `required: true` is present.
|
|
692
|
-
2. **Type Constraints**:
|
|
904
|
+
2. **Type Constraints**:
|
|
693
905
|
Any values match their specified types or can be coerced to the specified type.
|
|
694
906
|
3. **Value Ranges**:
|
|
695
907
|
Any values fall within their specified `:in` constraints.
|
|
696
|
-
4. **Objects**:
|
|
908
|
+
4. **Objects**:
|
|
697
909
|
Any objects are recursively validated.
|
|
698
|
-
5. **Arrays**:
|
|
910
|
+
5. **Arrays**:
|
|
699
911
|
Any validation rules are applied to each element when `array: true`
|
|
700
912
|
|
|
701
913
|
### validate!
|
|
702
914
|
|
|
703
|
-
The `validate!` method performs strict validation and raises an exception when it encounters
|
|
704
|
-
the first validation error.
|
|
915
|
+
The `validate!` method performs strict validation and raises an exception when it encounters the first validation error.
|
|
705
916
|
|
|
706
917
|
#### example:
|
|
707
918
|
|
|
@@ -716,27 +927,26 @@ schema.validate!( { temperature: 0.5 } )
|
|
|
716
927
|
|
|
717
928
|
# this will raise DynamicSchema::IncompatibleTypeError
|
|
718
929
|
schema.validate!( {
|
|
719
|
-
api_key: ["not-a-string"],
|
|
930
|
+
api_key: ["not-a-string"],
|
|
720
931
|
temperature: 0.5
|
|
721
932
|
} )
|
|
722
933
|
|
|
723
934
|
# this will raise DynamicSchema::InOptionError
|
|
724
935
|
schema.validate!( {
|
|
725
936
|
api_key: "abc123",
|
|
726
|
-
temperature: 1.5
|
|
937
|
+
temperature: 1.5
|
|
727
938
|
} )
|
|
728
939
|
|
|
729
940
|
# this is valid and will not raise any errors
|
|
730
941
|
schema.validate!( {
|
|
731
|
-
api_key: 123,
|
|
942
|
+
api_key: 123,
|
|
732
943
|
temperature: 0.5
|
|
733
944
|
} )
|
|
734
945
|
```
|
|
735
946
|
|
|
736
947
|
### validate
|
|
737
948
|
|
|
738
|
-
The `validate` method performs validation but instead of raising exceptions, it collects and
|
|
739
|
-
returns an array of all validation errors encountered.
|
|
949
|
+
The `validate` method performs validation but instead of raising exceptions, it collects and returns an array of all validation errors encountered.
|
|
740
950
|
|
|
741
951
|
#### example:
|
|
742
952
|
|
|
@@ -791,8 +1001,7 @@ schema.valid?({
|
|
|
791
1001
|
DynamicSchema provides specific error types for different validation failures:
|
|
792
1002
|
|
|
793
1003
|
- `DynamicSchema::RequiredOptionError`: Raised when a required field is missing
|
|
794
|
-
- `DynamicSchema::IncompatibleTypeError`: Raised when a value's type doesn't match the schema
|
|
795
|
-
and cannot be coerced
|
|
1004
|
+
- `DynamicSchema::IncompatibleTypeError`: Raised when a value's type doesn't match the schema and cannot be coerced
|
|
796
1005
|
- `DynamicSchema::InOptionError`: Raised when a value falls outside its specified range/set
|
|
797
1006
|
- `ArgumentError`: Raised when the provided values structure isn't a Hash
|
|
798
1007
|
|