dynamicschema 1.0.1 → 2.1.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 +456 -173
- data/dynamicschema.gemspec +7 -2
- data/lib/dynamic_schema/buildable.rb +8 -8
- data/lib/dynamic_schema/builder.rb +50 -19
- data/lib/dynamic_schema/{resolver.rb → compiler.rb} +42 -43
- data/lib/dynamic_schema/{builder_methods/conversion.rb → converter.rb} +41 -18
- data/lib/dynamic_schema/receiver/base.rb +60 -0
- data/lib/dynamic_schema/receiver/object.rb +301 -0
- data/lib/dynamic_schema/receiver/value.rb +27 -0
- data/lib/dynamic_schema/struct.rb +203 -0
- data/lib/dynamic_schema/validator.rb +139 -0
- data/lib/dynamic_schema.rb +7 -2
- metadata +18 -11
- data/lib/dynamic_schema/builder_methods/validation.rb +0 -109
- data/lib/dynamic_schema/receiver.rb +0 -227
data/README.md
CHANGED
|
@@ -1,39 +1,34 @@
|
|
|
1
1
|
# DynamicSchema
|
|
2
2
|
|
|
3
|
-
The **DynamicSchema** gem provides
|
|
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
|
|
27
22
|
end
|
|
28
23
|
```
|
|
29
24
|
|
|
30
|
-
And then
|
|
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)
|
|
@@ -58,10 +56,11 @@ You can find a full OpenAI request example in the `/examples` folder of this rep
|
|
|
58
56
|
- [in Option (Values Only)](#in-option)
|
|
59
57
|
- [arguments Option](#arguments-option)
|
|
60
58
|
- [Class Schema](#class-schemas)
|
|
61
|
-
- [Definable](#definable)
|
|
62
|
-
- [Buildable](#buildable)
|
|
59
|
+
- [Definable](#definable)
|
|
60
|
+
- [Buildable](#buildable)
|
|
61
|
+
- [Struct](#struct)
|
|
63
62
|
- [Validation Methods](#validation-methods)
|
|
64
|
-
- [Validation Rules](#validation-rules)
|
|
63
|
+
- [Validation Rules](#validation-rules)
|
|
65
64
|
- [validate!](#validate)
|
|
66
65
|
- [validate](#validate-1)
|
|
67
66
|
- [valid?](#valid)
|
|
@@ -103,35 +102,181 @@ require 'dynamic_schema'
|
|
|
103
102
|
|
|
104
103
|
### Defining Schemas with **DynamicSchema**
|
|
105
104
|
|
|
106
|
-
DynamicSchema
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
DynamicSchema lets you define a DSL made of values, objects, and options, then reuse that DSL to build and validate Ruby Hashes.
|
|
106
|
+
|
|
107
|
+
You start by constructing a `DynamicSchema::Builder`. You can do this by calling:
|
|
108
|
+
- `DynamicSchema.define { … }`
|
|
109
|
+
- `DynamicSchema::Builder.new.define { … }`
|
|
110
|
+
|
|
111
|
+
In both cases, you pass a block that declares the schema values and objects with their options.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
schema = DynamicSchema.define do
|
|
115
|
+
# values with an optional default
|
|
116
|
+
api_key String
|
|
117
|
+
model String, default: 'gpt-4o'
|
|
118
|
+
|
|
119
|
+
# object with its own values
|
|
120
|
+
chat_options do
|
|
121
|
+
max_tokens Integer, default: 1024
|
|
122
|
+
temperature Float, in: 0..1
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
109
126
|
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
You can then:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# build without validation
|
|
131
|
+
built = schema.build do
|
|
132
|
+
api_key 'secret'
|
|
133
|
+
chat_options do
|
|
134
|
+
temperature 0.7
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# build with validation (raises on first error)
|
|
139
|
+
built_validated = schema.build! do
|
|
140
|
+
api_key 'secret'
|
|
141
|
+
chat_options do
|
|
142
|
+
temperature 0.7
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# validate an existing Hash (no building)
|
|
147
|
+
errors = schema.validate( { api_key: 'secret', chat_options: { temperature: 0.7 } } )
|
|
148
|
+
valid = schema.valid?( { api_key: 'secret', chat_options: { temperature: 0.7 } } )
|
|
149
|
+
```
|
|
112
150
|
|
|
113
|
-
|
|
114
|
-
using the DSL you've defined. The builder has a 'build' method which will construct a Hash without
|
|
115
|
-
validating the values. If you've specified that a value should be of a specific type and an
|
|
116
|
-
incompatible type was given that type will be in the Hash with no indication of that violation.
|
|
117
|
-
Alterativelly, you can call the `build!` method which will validate the Hash, raising an exception
|
|
118
|
-
if any of the schema criteria is violated.
|
|
151
|
+
#### Inheritance
|
|
119
152
|
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
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.
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
class BaseSettings
|
|
157
|
+
include DynamicSchema::Definable
|
|
158
|
+
schema do
|
|
159
|
+
api_key String, required: true
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# extend the base schema with additional fields
|
|
164
|
+
builder = DynamicSchema.define( inherit: BaseSettings.schema ) do
|
|
165
|
+
region Symbol, in: %i[us eu apac]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
settings = builder.build! do
|
|
169
|
+
api_key 'secret'
|
|
170
|
+
region :us
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
You can call `build`, `build!`, `validate`, `validate!`, and `valid?` on the builder as needed.
|
|
122
175
|
|
|
123
176
|
---
|
|
124
177
|
|
|
125
|
-
##
|
|
178
|
+
## Struct
|
|
179
|
+
|
|
180
|
+
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.
|
|
126
181
|
|
|
127
|
-
|
|
128
|
-
options or API paramters that you can define with specific types, defaults, and other options.
|
|
182
|
+
You create a struct class by passing the same schema shape you would give to a Builder. The schema can be provided as:
|
|
129
183
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
184
|
+
- a `Proc` that defines the schema
|
|
185
|
+
- a `DynamicSchema::Builder`
|
|
186
|
+
- a compiled `Hash` (advanced)
|
|
133
187
|
|
|
134
|
-
|
|
188
|
+
```ruby
|
|
189
|
+
require 'dynamic_schema'
|
|
190
|
+
|
|
191
|
+
# simple struct with typed fields
|
|
192
|
+
Person = DynamicSchema::Struct.define do
|
|
193
|
+
full_name String
|
|
194
|
+
age Integer
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
person = Person.build( full_name: 'Sam Lee', age: '42' )
|
|
198
|
+
person.age # => 42 (coerced using the same converters as Builder)
|
|
199
|
+
person.full_name = 'Samira Lee'
|
|
200
|
+
person.to_h # => { full_name: 'Samira Lee', age: 42 }
|
|
201
|
+
|
|
202
|
+
# nested object with its own accessors
|
|
203
|
+
Company = DynamicSchema::Struct.define do
|
|
204
|
+
employee do
|
|
205
|
+
full_name String
|
|
206
|
+
years_of_service Integer
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
acme = Company.build( employee: { full_name: 'Alex', years_of_service: 5 } )
|
|
211
|
+
acme.employee.full_name # => 'Alex'
|
|
212
|
+
acme.employee.years_of_service # => 5
|
|
213
|
+
|
|
214
|
+
# array of nested objects
|
|
215
|
+
Order = DynamicSchema::Struct.define do
|
|
216
|
+
items array: true do
|
|
217
|
+
name String
|
|
218
|
+
price Integer
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
order = Order.build( items: [ { name: 'Desk', price: 100 }, { name: 'Chair', price: 50 } ] )
|
|
223
|
+
order.items.map { | i | i.name } # => [ 'Desk', 'Chair' ]
|
|
224
|
+
|
|
225
|
+
# referencing another struct class
|
|
226
|
+
OrderItem = DynamicSchema::Struct.define do
|
|
227
|
+
name String
|
|
228
|
+
quantity Integer
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
OrderCollection = DynamicSchema::Struct.define do
|
|
232
|
+
order_number String
|
|
233
|
+
line_items OrderItem, array: true
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
collection = OrderCollection.new( {
|
|
237
|
+
order_number: 'A-100',
|
|
238
|
+
line_items: [ { name: 'Desk', quantity: 1 }, { name: 'Chair', quantity: 2 } ]
|
|
239
|
+
} )
|
|
240
|
+
collection.line_items[ 0 ].name # => 'Desk'
|
|
241
|
+
collection.line_items[ 1 ].quantity # => 2
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- defining
|
|
245
|
+
- `DynamicSchema::Struct.define` takes a block that looks exactly like a Builder schema.
|
|
246
|
+
- Use `array: true` to expose arrays of nested structs.
|
|
247
|
+
- You may reference another struct class as a value type; arrays of that type expose nested accessors for each element.
|
|
248
|
+
- building
|
|
249
|
+
- `StructClass.build( attributes )` constructs an instance and (optionally) coerces typed scalar fields using the same converters as the Builder.
|
|
250
|
+
- `StructClass.build!` additionally validates the instance just like `builder.build!`.
|
|
251
|
+
- accessing
|
|
252
|
+
- Use standard Ruby readers/writers: `instance.attribute`, `instance.attribute = value`.
|
|
253
|
+
- `#to_h` returns a deep Hash of the current values (nested structs become hashes).
|
|
254
|
+
|
|
255
|
+
You can also create a struct class from a builder or a compiled hash if you already have a schema elsewhere:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
builder = DynamicSchema.define do
|
|
259
|
+
name String
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
NameStruct = DynamicSchema::Struct.new( builder )
|
|
263
|
+
NameStruct.build( name: 'Taylor' ).name # => 'Taylor'
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
- validation
|
|
267
|
+
- struct instances include the same validation helpers as hashes built via a builder.
|
|
268
|
+
- `StructClass.build!` validates immediately and raises on the first error.
|
|
269
|
+
- instances respond to `#validate!`, `#validate`, and `#valid?` using the compiled schema.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Values
|
|
274
|
+
|
|
275
|
+
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.
|
|
276
|
+
|
|
277
|
+
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:
|
|
278
|
+
|
|
279
|
+
`name {type}, default: {value}, required: {true|false}, array: {true|false}, as: {name}, in: {Array|Range}`
|
|
135
280
|
|
|
136
281
|
#### example:
|
|
137
282
|
|
|
@@ -141,7 +286,7 @@ require 'dynamic_schema'
|
|
|
141
286
|
# define a schema structure with values
|
|
142
287
|
schema = DynamicSchema.define do
|
|
143
288
|
api_key
|
|
144
|
-
version
|
|
289
|
+
version String, default: '1.0'
|
|
145
290
|
end
|
|
146
291
|
|
|
147
292
|
# build the schema and set values
|
|
@@ -156,30 +301,26 @@ puts result[:version] # => "1.0"
|
|
|
156
301
|
|
|
157
302
|
- defining
|
|
158
303
|
- `api_key` defines a value named `api_key`. Any type can be used to assign the value.
|
|
159
|
-
- `version
|
|
160
|
-
- building
|
|
161
|
-
- `schema.build!`
|
|
304
|
+
- `version String, default: '1.0'` defines a value with a default.
|
|
305
|
+
- building
|
|
306
|
+
- `schema.build!` accepts both a Hash and a block where you can set the values.
|
|
162
307
|
- Inside the block, `api_key 'your-api-key'` sets the value of `api_key`.
|
|
163
|
-
- accessing
|
|
308
|
+
- accessing
|
|
164
309
|
- `result[:api_key]` retrieves the value of `api_key`.
|
|
165
|
-
- If a value has a default and you don't set it, the default value will be included in
|
|
166
|
-
resulting hash.
|
|
310
|
+
- If a value has a default and you don't set it, the default value will be included in resulting hash.
|
|
167
311
|
|
|
168
312
|
---
|
|
169
313
|
|
|
170
314
|
## Objects
|
|
171
315
|
|
|
172
|
-
A schema may be organized hierarchically, by creating collections of related values and
|
|
173
|
-
even other collections. These collections are called objects.
|
|
316
|
+
A schema may be organized hierarchically, by creating collections of related values and even other collections. These collections are called objects.
|
|
174
317
|
|
|
175
|
-
An *object* is defined in a similar manner to a value. Simply provide the name as though
|
|
176
|
-
calling a Ruby method, with a Hash of options and a block which encloses the child values
|
|
177
|
-
and objects:
|
|
318
|
+
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:
|
|
178
319
|
|
|
179
320
|
```
|
|
180
|
-
name arguments: [ {argument} ], default: {
|
|
321
|
+
name arguments: [ {argument} ], default: {value}, required: {true|false}, array: {true|false}, as: {name} do
|
|
181
322
|
# child values and objects can be defined here
|
|
182
|
-
end
|
|
323
|
+
end
|
|
183
324
|
```
|
|
184
325
|
|
|
185
326
|
Notice an *object* does not accept a type as it is always of type `Object`.
|
|
@@ -190,7 +331,7 @@ Notice an *object* does not accept a type as it is always of type `Object`.
|
|
|
190
331
|
require 'dynamic_schema'
|
|
191
332
|
|
|
192
333
|
schema = DynamicSchema.define do
|
|
193
|
-
api_key
|
|
334
|
+
api_key String
|
|
194
335
|
chat_options do
|
|
195
336
|
model String, default: 'claude-3'
|
|
196
337
|
max_tokens Integer, default: 1024
|
|
@@ -217,7 +358,7 @@ puts result[:chat_options][:stream] # => true
|
|
|
217
358
|
- defining
|
|
218
359
|
- `chat_options do ... end` defines an object named `chat_options`.
|
|
219
360
|
- Inside the object you can define values that belong to that object.
|
|
220
|
-
- building
|
|
361
|
+
- building
|
|
221
362
|
- In the build block, you can set values for values within objects by nesting blocks.
|
|
222
363
|
- `chat_options do ... end` allows you to set values inside the `chat_options` object.
|
|
223
364
|
- accessing
|
|
@@ -225,11 +366,9 @@ puts result[:chat_options][:stream] # => true
|
|
|
225
366
|
|
|
226
367
|
---
|
|
227
368
|
|
|
228
|
-
## Types
|
|
369
|
+
## Types
|
|
229
370
|
|
|
230
|
-
An *object* is always of type `Object`. A *value* can have no type or it can be of one or
|
|
231
|
-
more types. You specify the value type by providing an instance of a `Class` when defining
|
|
232
|
-
the value. If you want to specify multiple types simply provide an array of types.
|
|
371
|
+
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.
|
|
233
372
|
|
|
234
373
|
#### example:
|
|
235
374
|
|
|
@@ -238,37 +377,209 @@ require 'dynamic_schema'
|
|
|
238
377
|
|
|
239
378
|
schema = DynamicSchema.define do
|
|
240
379
|
typeless_value
|
|
241
|
-
symbol_value Symbol
|
|
380
|
+
symbol_value Symbol
|
|
242
381
|
boolean_value [ TrueClass, FalseClass ]
|
|
243
382
|
end
|
|
244
383
|
|
|
245
384
|
result = schema.build! do
|
|
246
385
|
typeless_value Struct.new(:name).new(name: 'Kristoph')
|
|
247
386
|
symbol_value "something"
|
|
248
|
-
boolean_value true
|
|
249
|
-
end
|
|
387
|
+
boolean_value true
|
|
388
|
+
end
|
|
250
389
|
|
|
251
390
|
puts result[:typeless_value].name # => "Kristoph"
|
|
252
391
|
puts result[:symbol_value] # => :something
|
|
253
|
-
puts result[:boolean_value] # => true
|
|
392
|
+
puts result[:boolean_value] # => true
|
|
254
393
|
```
|
|
255
394
|
|
|
256
395
|
- defining
|
|
257
|
-
- `typeless_value` defines a value that has no type and will accept an assignment of any type
|
|
258
|
-
- `symbol_value` defines a value that accepts symbols or types that can be coerced into
|
|
259
|
-
symbols, such as strings (see **Type Coercion**)
|
|
396
|
+
- `typeless_value` defines a value that has no type and will accept an assignment of any type
|
|
397
|
+
- `symbol_value` defines a value that accepts symbols or types that can be coerced into symbols, such as strings (see **Type Coercion**)
|
|
260
398
|
- `boolean_value` defines a value that can be either `true` or `false`
|
|
261
399
|
|
|
262
|
-
|
|
400
|
+
### Custom Types
|
|
401
|
+
|
|
402
|
+
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:
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
require 'dynamic_schema'
|
|
406
|
+
|
|
407
|
+
class Customer
|
|
408
|
+
attr_accessor :name, :email
|
|
409
|
+
end
|
|
263
410
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
411
|
+
schema = DynamicSchema.define do
|
|
412
|
+
customer Customer
|
|
413
|
+
end
|
|
267
414
|
|
|
268
|
-
|
|
415
|
+
# auto-instantiate and configure with a block
|
|
416
|
+
result = schema.build! do
|
|
417
|
+
customer do
|
|
418
|
+
name 'Alice'
|
|
419
|
+
email 'alice@example.com'
|
|
420
|
+
end
|
|
421
|
+
end
|
|
269
422
|
|
|
270
|
-
|
|
271
|
-
|
|
423
|
+
result[:customer].name # => 'Alice'
|
|
424
|
+
result[:customer].email # => 'alice@example.com'
|
|
425
|
+
|
|
426
|
+
# or provide an existing instance
|
|
427
|
+
existing = Customer.new
|
|
428
|
+
existing.name = 'Bob'
|
|
429
|
+
|
|
430
|
+
result = schema.build! do
|
|
431
|
+
customer existing do
|
|
432
|
+
email 'bob@example.com'
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
result[:customer].name # => 'Bob'
|
|
437
|
+
result[:customer].email # => 'bob@example.com'
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
When using a block with a custom type:
|
|
441
|
+
- If no instance is provided, DynamicSchema will call `YourClass.new` to create one
|
|
442
|
+
- Inside the block, method calls are translated to setter calls on the instance
|
|
443
|
+
- You can provide an existing instance and still use a block to configure it further
|
|
444
|
+
|
|
445
|
+
This is particularly useful when integrating with `DynamicSchema::Struct` or other custom classes that need to be configured within a schema.
|
|
446
|
+
|
|
447
|
+
### Multiple Types
|
|
448
|
+
|
|
449
|
+
You can specify multiple types for a value by providing an array of types. The value must match one of the listed types.
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
schema = DynamicSchema.define do
|
|
453
|
+
enabled [ TrueClass, FalseClass ]
|
|
454
|
+
identifier [ String, Integer ]
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
result = schema.build! do
|
|
458
|
+
enabled true
|
|
459
|
+
identifier 12345
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
result[ :enabled ] # => true
|
|
463
|
+
result[ :identifier ] # => 12345
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Validation ensures the value matches one of the specified types:
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
schema.valid?( { enabled: true } ) # => true
|
|
470
|
+
schema.valid?( { enabled: 'yes' } ) # => false (string not in types)
|
|
471
|
+
schema.valid?( { identifier: 'abc' } ) # => true
|
|
472
|
+
schema.valid?( { identifier: 123 } ) # => true
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Multiple Types with Nested Schema
|
|
476
|
+
|
|
477
|
+
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:
|
|
478
|
+
|
|
479
|
+
- **Hash values** (or blocks in Builder) are processed using the nested schema
|
|
480
|
+
- **Non-hash values** are validated against the scalar types in the array
|
|
481
|
+
|
|
482
|
+
This is useful for APIs where a field might be either a structured object or a simple value like a boolean.
|
|
483
|
+
|
|
484
|
+
#### Builder Example
|
|
485
|
+
|
|
486
|
+
```ruby
|
|
487
|
+
schema = DynamicSchema.define do
|
|
488
|
+
# 'data' can be either a nested object OR true/false
|
|
489
|
+
data [ Object, TrueClass, FalseClass ] do
|
|
490
|
+
name String
|
|
491
|
+
value Integer
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Using as a nested object (with block)
|
|
496
|
+
result = schema.build! do
|
|
497
|
+
data do
|
|
498
|
+
name 'example'
|
|
499
|
+
value 42
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
result[ :data ][ :name ] # => 'example'
|
|
503
|
+
|
|
504
|
+
# Using as a nested object (with hash)
|
|
505
|
+
result = schema.build! do
|
|
506
|
+
data( { name: 'from hash', value: 100 } )
|
|
507
|
+
end
|
|
508
|
+
result[ :data ][ :name ] # => 'from hash'
|
|
509
|
+
|
|
510
|
+
# Using as a boolean
|
|
511
|
+
result = schema.build! do
|
|
512
|
+
data true
|
|
513
|
+
end
|
|
514
|
+
result[ :data ] # => true
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
#### Struct Example
|
|
518
|
+
|
|
519
|
+
```ruby
|
|
520
|
+
Settings = DynamicSchema::Struct.define do
|
|
521
|
+
config [ Object, TrueClass, FalseClass ] do
|
|
522
|
+
host String
|
|
523
|
+
port Integer
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# With nested object - accessed via dot notation
|
|
528
|
+
settings = Settings.build( config: { host: 'localhost', port: 8080 } )
|
|
529
|
+
settings.config.host # => 'localhost'
|
|
530
|
+
settings.config.port # => 8080
|
|
531
|
+
|
|
532
|
+
# With boolean - returned as-is
|
|
533
|
+
settings = Settings.build( config: false )
|
|
534
|
+
settings.config # => false
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
#### Arrays with Multiple Types
|
|
538
|
+
|
|
539
|
+
Combine with `array: true` to create arrays where each element can be either a nested object or a scalar:
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
schema = DynamicSchema.define do
|
|
543
|
+
items [ Object, TrueClass, FalseClass ], array: true do
|
|
544
|
+
name String
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
result = schema.build! do
|
|
549
|
+
items( { name: 'first' } )
|
|
550
|
+
items true
|
|
551
|
+
items( { name: 'second' } )
|
|
552
|
+
items false
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
result[ :items ] # => [ { name: 'first' }, true, { name: 'second' }, false ]
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Validation
|
|
559
|
+
|
|
560
|
+
Validation checks that hash values conform to the nested schema and non-hash values match one of the scalar types:
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
schema = DynamicSchema.define do
|
|
564
|
+
data [ Object, TrueClass, FalseClass ] do
|
|
565
|
+
value Integer
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
schema.valid?( { data: { value: 42 } } ) # => true (valid nested object)
|
|
570
|
+
schema.valid?( { data: true } ) # => true (valid boolean)
|
|
571
|
+
schema.valid?( { data: 'invalid' } ) # => false (string not in types)
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Options
|
|
577
|
+
|
|
578
|
+
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.
|
|
579
|
+
|
|
580
|
+
### :default Option
|
|
581
|
+
|
|
582
|
+
The `:default` option allows you to specify a default value that will be used if no value is provided during build.
|
|
272
583
|
|
|
273
584
|
#### example:
|
|
274
585
|
|
|
@@ -285,9 +596,7 @@ puts result[:timeout] # => 30
|
|
|
285
596
|
|
|
286
597
|
### :required Option
|
|
287
598
|
|
|
288
|
-
The `:required` option ensures that a value must be provided when building the schema. If a
|
|
289
|
-
required value is missing when using `build!`, `validate`, or `validate!`,
|
|
290
|
-
a `DynamicSchema::RequiredOptionError` will be raised.
|
|
599
|
+
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.
|
|
291
600
|
|
|
292
601
|
#### example:
|
|
293
602
|
|
|
@@ -308,9 +617,7 @@ end
|
|
|
308
617
|
|
|
309
618
|
### :array Option
|
|
310
619
|
|
|
311
|
-
The `:array` option wraps the value or object in an array in the resulting Hash, even if only
|
|
312
|
-
one value is provided. This is particularly useful when dealing with APIs that expect array
|
|
313
|
-
inputs.
|
|
620
|
+
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.
|
|
314
621
|
|
|
315
622
|
#### example:
|
|
316
623
|
|
|
@@ -336,9 +643,7 @@ puts result[:message] # => [{ text: "Hello world", type: "plain" }]
|
|
|
336
643
|
|
|
337
644
|
### :as Option
|
|
338
645
|
|
|
339
|
-
The `:as` option allows you to use a different name in the DSL than what appears in the final
|
|
340
|
-
Hash. This is particularly useful when interfacing with APIs that have specific key
|
|
341
|
-
requirements.
|
|
646
|
+
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.
|
|
342
647
|
|
|
343
648
|
#### example:
|
|
344
649
|
|
|
@@ -358,8 +663,7 @@ puts result["Authorization"] # => "Bearer abc123"
|
|
|
358
663
|
|
|
359
664
|
### :in Option
|
|
360
665
|
|
|
361
|
-
The `:in` option provides validation for values, ensuring they fall within a specified Range or
|
|
362
|
-
are included in an Array of allowed values. This option is only available for values.
|
|
666
|
+
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.
|
|
363
667
|
|
|
364
668
|
#### example:
|
|
365
669
|
|
|
@@ -388,14 +692,11 @@ result = schema.build! do
|
|
|
388
692
|
end
|
|
389
693
|
```
|
|
390
694
|
|
|
391
|
-
### :arguments Option
|
|
695
|
+
### :arguments Option
|
|
392
696
|
|
|
393
|
-
The `:arguments` option allows objects to accept arguments when building. Any arguments provided
|
|
394
|
-
must appear when the object is built ( and so are implicitly 'required' ).
|
|
697
|
+
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').
|
|
395
698
|
|
|
396
|
-
If
|
|
397
|
-
block, the assignemnt in the block will take priority, followed by the attributes assigned and
|
|
398
|
-
finally the argument.
|
|
699
|
+
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.
|
|
399
700
|
|
|
400
701
|
#### example:
|
|
401
702
|
|
|
@@ -419,129 +720,113 @@ end
|
|
|
419
720
|
|
|
420
721
|
## Class Schemas
|
|
421
722
|
|
|
422
|
-
DynamicSchema provides a number of modules you can include into your own classes to simplify
|
|
423
|
-
their definition and construction.
|
|
723
|
+
DynamicSchema provides a number of modules you can include into your own classes to simplify their definition and construction.
|
|
424
724
|
|
|
425
|
-
### Definable
|
|
725
|
+
### Definable
|
|
426
726
|
|
|
427
|
-
The `Definable` module, when
|
|
428
|
-
methods.
|
|
727
|
+
The `Definable` module, when included in a class, will add the `schema` and the `builder` class methods.
|
|
429
728
|
|
|
430
|
-
By calling `schema` with a block you can define a schema for that specific class. You may also
|
|
431
|
-
retrieve the defined schema by calling 'schema' ( with or without a block ). The 'schema' method
|
|
432
|
-
may be called repeatedly to build up a schema with each call adding to the existing schema
|
|
433
|
-
( replacing values and objects of the same name if they appear in subsequent calls ).
|
|
729
|
+
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).
|
|
434
730
|
|
|
435
|
-
The `schema` method will integrate with a class hierarchy. By including Definable in a base class
|
|
436
|
-
you can call `schema` to define a schema for that base class and then in subsequent dervied classes
|
|
437
|
-
to augment it for those classes.
|
|
731
|
+
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.
|
|
438
732
|
|
|
439
|
-
The `builder` method will return a memoized builder of the schema defined by calls to the `schema`
|
|
440
|
-
method which can be used to build and validate schema conformant hashes.
|
|
733
|
+
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.
|
|
441
734
|
|
|
442
|
-
```ruby
|
|
443
|
-
class Setting
|
|
444
|
-
include DynamicSchema::Definable
|
|
445
|
-
schema do
|
|
446
|
-
name String
|
|
447
|
-
end
|
|
448
|
-
end
|
|
735
|
+
```ruby
|
|
736
|
+
class Setting
|
|
737
|
+
include DynamicSchema::Definable
|
|
738
|
+
schema do
|
|
739
|
+
name String
|
|
740
|
+
end
|
|
741
|
+
end
|
|
449
742
|
|
|
450
|
-
class
|
|
451
|
-
schema do
|
|
452
|
-
database do
|
|
743
|
+
class DatabaseSetting < Setting
|
|
744
|
+
schema do
|
|
745
|
+
database do
|
|
453
746
|
host String
|
|
454
|
-
port String
|
|
455
|
-
name String
|
|
456
|
-
end
|
|
457
|
-
end
|
|
747
|
+
port String
|
|
748
|
+
name String
|
|
749
|
+
end
|
|
750
|
+
end
|
|
458
751
|
|
|
459
|
-
def
|
|
460
|
-
# validate the attributes
|
|
752
|
+
def initialize( attributes = {} )
|
|
753
|
+
# validate the attributes
|
|
461
754
|
self.class.builder.validate!( attributes )
|
|
462
|
-
# retain them for future access
|
|
463
|
-
@attributes = attributes&.dup
|
|
755
|
+
# retain them for future access
|
|
756
|
+
@attributes = attributes&.dup
|
|
464
757
|
end
|
|
465
758
|
|
|
466
|
-
end
|
|
759
|
+
end
|
|
467
760
|
```
|
|
468
761
|
|
|
469
|
-
### Buildable
|
|
762
|
+
### Buildable
|
|
470
763
|
|
|
471
|
-
The `Buildable` module can be included in a class, in addition to `Definable` to
|
|
472
|
-
building that class using a schema assisted builder pattern. The `Buildable` module adds
|
|
473
|
-
`build!` and `build` methods to the class which can be used to build that class, with and
|
|
474
|
-
without validation respectivelly.
|
|
764
|
+
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.
|
|
475
765
|
|
|
476
|
-
These methods accept both a
|
|
477
|
-
that can be used to build the class instance. The attributes and block can be used simultanously.
|
|
766
|
+
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.
|
|
478
767
|
|
|
479
|
-
**Important** Note that `Buildable` requires a class method `builder` (
|
|
480
|
-
provides ) and an initializer that accepts a `Hash` of attributes.
|
|
768
|
+
**Important** Note that `Buildable` requires a class method `builder` (which `Definable` provides) and an initializer that accepts a `Hash` of attributes.
|
|
481
769
|
|
|
482
770
|
```ruby
|
|
483
|
-
class Setting
|
|
484
|
-
include DynamicSchema::Definable
|
|
771
|
+
class Setting
|
|
772
|
+
include DynamicSchema::Definable
|
|
485
773
|
include DynamicSchema::Buildable
|
|
486
|
-
schema do
|
|
487
|
-
name String
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
class
|
|
492
|
-
schema do
|
|
493
|
-
database do
|
|
494
|
-
adapter Symbol
|
|
774
|
+
schema do
|
|
775
|
+
name String
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
class DatabaseSetting < Setting
|
|
780
|
+
schema do
|
|
781
|
+
database do
|
|
782
|
+
adapter Symbol
|
|
495
783
|
host String
|
|
496
|
-
port String
|
|
497
|
-
name String
|
|
498
|
-
end
|
|
499
|
-
end
|
|
784
|
+
port String
|
|
785
|
+
name String
|
|
786
|
+
end
|
|
787
|
+
end
|
|
500
788
|
|
|
501
|
-
def
|
|
502
|
-
# validate the attributes
|
|
789
|
+
def initialize( attributes = {} )
|
|
790
|
+
# validate the attributes
|
|
503
791
|
self.class.builder.validate!( attributes )
|
|
504
|
-
# retain them for the future
|
|
505
|
-
@attributes = attributes&.dup
|
|
792
|
+
# retain them for the future
|
|
793
|
+
@attributes = attributes&.dup
|
|
506
794
|
end
|
|
507
|
-
end
|
|
795
|
+
end
|
|
508
796
|
|
|
509
|
-
database_settings =
|
|
510
|
-
database adapter: :pg do
|
|
797
|
+
database_settings = DatabaseSetting.build! name: 'settings.database' do
|
|
798
|
+
database adapter: :pg do
|
|
511
799
|
host "localhost"
|
|
512
800
|
port "127.0.0.1"
|
|
513
801
|
name "mydb"
|
|
514
|
-
end
|
|
802
|
+
end
|
|
515
803
|
end
|
|
516
804
|
```
|
|
517
805
|
|
|
518
806
|
## Validation
|
|
519
807
|
|
|
520
|
-
DynamicSchema provides three different methods for validating Hash structures against your
|
|
521
|
-
defined schema: `validate!`, `validate`, and `valid?`.
|
|
808
|
+
DynamicSchema provides three different methods for validating Hash structures against your defined schema: `validate!`, `validate`, and `valid?`.
|
|
522
809
|
|
|
523
|
-
These methods allow you to verify that your data conforms to your schema
|
|
524
|
-
including type constraints, required fields, and value ranges.
|
|
810
|
+
These methods allow you to verify that your data conforms to your schema requirements, including type constraints, required fields, and value ranges.
|
|
525
811
|
|
|
526
812
|
### Validation Rules
|
|
527
813
|
|
|
528
814
|
When validating, DynamicSchema checks:
|
|
529
815
|
|
|
530
|
-
1. **Required Fields**:
|
|
531
|
-
Any value or object marked as `required: true`
|
|
532
|
-
2. **Type Constraints**:
|
|
816
|
+
1. **Required Fields**:
|
|
817
|
+
Any value or object marked as `required: true` is present.
|
|
818
|
+
2. **Type Constraints**:
|
|
533
819
|
Any values match their specified types or can be coerced to the specified type.
|
|
534
820
|
3. **Value Ranges**:
|
|
535
821
|
Any values fall within their specified `:in` constraints.
|
|
536
|
-
4. **Objects**:
|
|
537
|
-
Any objects are recursively
|
|
538
|
-
5. **Arrays**:
|
|
822
|
+
4. **Objects**:
|
|
823
|
+
Any objects are recursively validated.
|
|
824
|
+
5. **Arrays**:
|
|
539
825
|
Any validation rules are applied to each element when `array: true`
|
|
540
826
|
|
|
541
827
|
### validate!
|
|
542
828
|
|
|
543
|
-
The `validate!` method performs strict validation and raises an exception when it encounters
|
|
544
|
-
the first validation error.
|
|
829
|
+
The `validate!` method performs strict validation and raises an exception when it encounters the first validation error.
|
|
545
830
|
|
|
546
831
|
#### example:
|
|
547
832
|
|
|
@@ -556,27 +841,26 @@ schema.validate!( { temperature: 0.5 } )
|
|
|
556
841
|
|
|
557
842
|
# this will raise DynamicSchema::IncompatibleTypeError
|
|
558
843
|
schema.validate!( {
|
|
559
|
-
api_key: ["not-a-string"],
|
|
844
|
+
api_key: ["not-a-string"],
|
|
560
845
|
temperature: 0.5
|
|
561
846
|
} )
|
|
562
847
|
|
|
563
848
|
# this will raise DynamicSchema::InOptionError
|
|
564
849
|
schema.validate!( {
|
|
565
850
|
api_key: "abc123",
|
|
566
|
-
temperature: 1.5
|
|
851
|
+
temperature: 1.5
|
|
567
852
|
} )
|
|
568
853
|
|
|
569
854
|
# this is valid and will not raise any errors
|
|
570
855
|
schema.validate!( {
|
|
571
|
-
api_key: 123,
|
|
856
|
+
api_key: 123,
|
|
572
857
|
temperature: 0.5
|
|
573
858
|
} )
|
|
574
859
|
```
|
|
575
860
|
|
|
576
861
|
### validate
|
|
577
862
|
|
|
578
|
-
The `validate` method performs validation but instead of raising exceptions, it collects and
|
|
579
|
-
returns an array of all validation errors encountered.
|
|
863
|
+
The `validate` method performs validation but instead of raising exceptions, it collects and returns an array of all validation errors encountered.
|
|
580
864
|
|
|
581
865
|
#### example:
|
|
582
866
|
|
|
@@ -631,8 +915,7 @@ schema.valid?({
|
|
|
631
915
|
DynamicSchema provides specific error types for different validation failures:
|
|
632
916
|
|
|
633
917
|
- `DynamicSchema::RequiredOptionError`: Raised when a required field is missing
|
|
634
|
-
- `DynamicSchema::IncompatibleTypeError`: Raised when a value's type doesn't match the schema
|
|
635
|
-
and cannot be coerced
|
|
918
|
+
- `DynamicSchema::IncompatibleTypeError`: Raised when a value's type doesn't match the schema and cannot be coerced
|
|
636
919
|
- `DynamicSchema::InOptionError`: Raised when a value falls outside its specified range/set
|
|
637
920
|
- `ArgumentError`: Raised when the provided values structure isn't a Hash
|
|
638
921
|
|
|
@@ -642,7 +925,7 @@ Each error includes helpful context about the validation failure, including the
|
|
|
642
925
|
|
|
643
926
|
## Contributing
|
|
644
927
|
|
|
645
|
-
Bug reports and pull requests are welcome on GitHub at [https://github.com/EndlessInternational/
|
|
928
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/EndlessInternational/dynamic_schema](https://github.com/EndlessInternational/dynamic_schema).
|
|
646
929
|
|
|
647
930
|
## License
|
|
648
931
|
|