schematist 1.0.0 → 1.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 +108 -21
- data/lib/schematist/dsl/complex_types.rb +18 -16
- data/lib/schematist/dsl/conditional_builder.rb +47 -11
- data/lib/schematist/dsl/conditional_context.rb +9 -6
- data/lib/schematist/dsl/conditionals.rb +6 -3
- data/lib/schematist/dsl/primitive_types.rb +10 -10
- data/lib/schematist/dsl/schema_builders.rb +35 -26
- data/lib/schematist/dsl/utilities.rb +21 -10
- data/lib/schematist/json_output.rb +1 -8
- data/lib/schematist/schema.rb +29 -0
- data/lib/schematist/validator.rb +4 -9
- data/lib/schematist/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2acb271d17c754d0d444729086bc552a07c3bb3fd2e66af289bde8d9d5db5b22
|
|
4
|
+
data.tar.gz: 4469284644cddf477d91afee10b70960673546c3ad11e0b7554f0b5b09f4e5ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4c561f755bf2d2a0b33a144817239f585aaad38f7274149597e8a3a129021a8ec1a41606ad51469509152c32210596031b7edfe108a00946d493b30e244566b
|
|
7
|
+
data.tar.gz: eb97a91efb9754f9fa0d652273a340e5cb8048f45f8b9271ee2537e5196372d3e3d13432ada2615b684fe78aa02b957ee58bb165a10f6f2c7d0c7a3f5e4e7695
|
data/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
[](https://codecov.io/gh/crmne/schematist)
|
|
6
6
|
[](https://github.com/rubocop/rubocop)
|
|
7
7
|
|
|
8
|
-
A
|
|
8
|
+
A general purpose JSON Schema DSL for Ruby with a clean, Rails-inspired API. Emits Draft 2020-12 schemas and depends on nothing.
|
|
9
|
+
|
|
10
|
+
**Formerly `RubyLLM::Schema`.** Trapping a general purpose JSON Schema DSL inside another gem's namespace was a disservice to anyone looking for one, so 1.0 gave it its own name. See [Migrating from ruby_llm-schema](#migrating-from-ruby_llm-schema).
|
|
9
11
|
|
|
10
12
|
Originally created by [Daniel Friis](https://github.com/danielfriis).
|
|
11
13
|
|
|
@@ -69,9 +71,9 @@ chat = RubyLLM.chat
|
|
|
69
71
|
response = chat.with_schema(PersonSchema)
|
|
70
72
|
.ask("Generate a person named Alice who is 30 years old and lives in New York")
|
|
71
73
|
|
|
72
|
-
#
|
|
73
|
-
puts response.content # => {"name"
|
|
74
|
-
puts response.
|
|
74
|
+
# Content stays the raw JSON string, and #parsed gives you the Hash
|
|
75
|
+
puts response.content # => "{\"name\":\"Alice\",\"age\":30}"
|
|
76
|
+
puts response.parsed # => {"name" => "Alice", "age" => 30}
|
|
75
77
|
```
|
|
76
78
|
|
|
77
79
|
### RubyLLM tools
|
|
@@ -85,8 +87,8 @@ class SearchParams < Schematist::Schema
|
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
class SearchDocuments < RubyLLM::Tool
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
description "Searches internal documents"
|
|
91
|
+
parameters SearchParams
|
|
90
92
|
|
|
91
93
|
def execute(query:, limit: 10)
|
|
92
94
|
DocumentSearch.call(query:, limit:)
|
|
@@ -94,13 +96,13 @@ class SearchDocuments < RubyLLM::Tool
|
|
|
94
96
|
end
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
For tool-specific
|
|
99
|
+
For tool-specific arguments, define the schema inline with `parameters do ... end`.
|
|
98
100
|
|
|
99
101
|
```ruby
|
|
100
102
|
class Weather < RubyLLM::Tool
|
|
101
|
-
|
|
103
|
+
description "Gets current weather"
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
parameters do
|
|
104
106
|
string :city, description: "City name"
|
|
105
107
|
string :units, enum: %w[celsius fahrenheit], required: false
|
|
106
108
|
end
|
|
@@ -300,6 +302,7 @@ Number and integer types support the following properties:
|
|
|
300
302
|
|
|
301
303
|
- `enum`: an array of allowed numeric values (e.g. `enum: [0, 1, 2]`)
|
|
302
304
|
- `const`: the single allowed value (e.g. `const: 1`)
|
|
305
|
+
- `format`: a format string (e.g. `format: "int64"`)
|
|
303
306
|
- `multiple_of`: a multiple of the number (e.g. `multiple_of: 0.01`)
|
|
304
307
|
- `minimum`: the minimum value of the number (e.g. `minimum: 0`)
|
|
305
308
|
- `maximum`: the maximum value of the number (e.g. `maximum: 100`)
|
|
@@ -318,17 +321,19 @@ integer :level, enum: [0, 1, 2]
|
|
|
318
321
|
```ruby
|
|
319
322
|
boolean :is_active
|
|
320
323
|
boolean :accepted_terms, const: true
|
|
324
|
+
boolean :flag, enum: [true]
|
|
321
325
|
```
|
|
322
326
|
|
|
323
|
-
|
|
327
|
+
Booleans support `const` and `enum`.
|
|
324
328
|
|
|
325
329
|
### Null
|
|
326
330
|
|
|
327
331
|
```ruby
|
|
328
332
|
null :placeholder
|
|
333
|
+
null :nothing, enum: [nil]
|
|
329
334
|
```
|
|
330
335
|
|
|
331
|
-
|
|
336
|
+
Nulls support `enum`.
|
|
332
337
|
|
|
333
338
|
### Arrays
|
|
334
339
|
|
|
@@ -361,7 +366,7 @@ end
|
|
|
361
366
|
|
|
362
367
|
### Tuples
|
|
363
368
|
|
|
364
|
-
A tuple is
|
|
369
|
+
A tuple is an array where each position has its own schema. It emits `prefixItems`, and by default it is exactly as long as its prefix.
|
|
365
370
|
|
|
366
371
|
```ruby
|
|
367
372
|
tuple :coordinates do
|
|
@@ -370,6 +375,20 @@ tuple :coordinates do
|
|
|
370
375
|
end
|
|
371
376
|
```
|
|
372
377
|
|
|
378
|
+
Give it somewhere for the rest to go and it stops being fixed length. `of:` types the tail, `unevaluated_items:` closes it off after the prefix, and an explicit `max_items:` sets its own bound.
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
tuple :event, of: :string do # prefixItems, then strings
|
|
382
|
+
string
|
|
383
|
+
integer
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
tuple :pair, unevaluated_items: false do
|
|
387
|
+
string
|
|
388
|
+
string
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
373
392
|
### Objects
|
|
374
393
|
|
|
375
394
|
Objects types expect a block with the properties of the object.
|
|
@@ -482,7 +501,7 @@ array :values, of: :integer, unevaluated_items: false
|
|
|
482
501
|
|
|
483
502
|
### Runtime Values
|
|
484
503
|
|
|
485
|
-
Any schema value can be a proc, resolved when the schema is rendered.
|
|
504
|
+
Any schema value can be a proc, resolved when the schema is rendered. One schema class then produces a different document per instance, which is what you want when an enum comes from the database.
|
|
486
505
|
|
|
487
506
|
```ruby
|
|
488
507
|
class RoleSchema < Schematist::Schema
|
|
@@ -523,6 +542,50 @@ any_of :value do
|
|
|
523
542
|
end
|
|
524
543
|
```
|
|
525
544
|
|
|
545
|
+
### Schemas That Aren't Objects
|
|
546
|
+
|
|
547
|
+
A type with a name declares a property. Without a name it declares what the schema itself is. That is how a root, or a definition, becomes something other than an object.
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
class Tags < Schematist::Schema
|
|
551
|
+
array of: :string, unique: true # the whole schema is an array
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
class Id < Schematist::Schema
|
|
555
|
+
one_of do # the whole schema is a choice
|
|
556
|
+
string
|
|
557
|
+
integer
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
class Person < Schematist::Schema
|
|
562
|
+
raw({ "$ref" => "https://example.com/person.json" })
|
|
563
|
+
end
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
The same rule applies inside `define`, so a definition can be any schema:
|
|
567
|
+
|
|
568
|
+
```ruby
|
|
569
|
+
define :status do
|
|
570
|
+
string enum: %w[draft sent] # a reusable string
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
define :address do
|
|
574
|
+
string :street # named, so an object with properties
|
|
575
|
+
end
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Objects also take their keywords at the root:
|
|
579
|
+
|
|
580
|
+
```ruby
|
|
581
|
+
class Metadata < Schematist::Schema
|
|
582
|
+
string :name
|
|
583
|
+
min_properties 1
|
|
584
|
+
max_properties 10
|
|
585
|
+
unevaluated_properties false
|
|
586
|
+
end
|
|
587
|
+
```
|
|
588
|
+
|
|
526
589
|
### Schema Definitions and References
|
|
527
590
|
|
|
528
591
|
You can define sub-schemas and reference them in other schemas, or reference the root schema to generate recursive schemas.
|
|
@@ -657,7 +720,7 @@ class PaymentSchema < Schematist::Schema
|
|
|
657
720
|
end
|
|
658
721
|
```
|
|
659
722
|
|
|
660
|
-
Use a `dependent` block when you also need validations
|
|
723
|
+
Use a `dependent` block when you also need validations. This upgrades the output to `dependentSchemas`:
|
|
661
724
|
|
|
662
725
|
```ruby
|
|
663
726
|
dependent :credit_card do
|
|
@@ -703,6 +766,32 @@ end
|
|
|
703
766
|
|
|
704
767
|
Conditions propagate through nested schemas via `of:`.
|
|
705
768
|
|
|
769
|
+
A branch is a schema, so anything you can write in a schema you can write in a branch. `requires` and `validates` stay as shorthands for the two common cases.
|
|
770
|
+
|
|
771
|
+
```ruby
|
|
772
|
+
given kind: "business" do
|
|
773
|
+
requires :vat_id
|
|
774
|
+
|
|
775
|
+
object :tax_details do
|
|
776
|
+
string :vat_number
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
array :filings, of: :string
|
|
780
|
+
|
|
781
|
+
otherwise do
|
|
782
|
+
validates :vat_id, type: :string
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
`given` matches on property values. Pass a schema explicitly when the condition is something else:
|
|
788
|
+
|
|
789
|
+
```ruby
|
|
790
|
+
given({ required: %w[tax_id] }) do
|
|
791
|
+
requires :summary
|
|
792
|
+
end
|
|
793
|
+
```
|
|
794
|
+
|
|
706
795
|
## JSON Output
|
|
707
796
|
|
|
708
797
|
`to_json_schema` returns a Draft 2020-12 JSON Schema document with string keys, ready to hand to any JSON Schema validator.
|
|
@@ -722,13 +811,11 @@ schema.to_json_schema
|
|
|
722
811
|
puts schema.to_json # Pretty JSON string of the same document
|
|
723
812
|
```
|
|
724
813
|
|
|
725
|
-
The schema name maps to `title`. Provider-only keys are not part of the document
|
|
814
|
+
The schema name maps to `title`. Provider-only keys are not part of the document. `strict` was an OpenAI `response_format` flag rather than a JSON Schema keyword, so it has been removed. Set it where you build the request.
|
|
726
815
|
|
|
727
816
|
### Migrating from ruby_llm-schema
|
|
728
817
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
Update the gem, then the constants:
|
|
818
|
+
`RubyLLM::Schema` is now `Schematist`. Update the gem, then the constants:
|
|
732
819
|
|
|
733
820
|
```ruby
|
|
734
821
|
gem 'schematist' # was: gem 'ruby_llm-schema'
|
|
@@ -739,13 +826,13 @@ end
|
|
|
739
826
|
include Schematist::Helpers # was: RubyLLM::Helpers
|
|
740
827
|
```
|
|
741
828
|
|
|
742
|
-
Errors moved up a level with the rename
|
|
829
|
+
Errors moved up a level with the rename: `Schematist::ValidationError`, not `RubyLLM::Schema::ValidationError`. `strict` is gone; see below.
|
|
743
830
|
|
|
744
831
|
### Migrating from the provider envelope
|
|
745
832
|
|
|
746
|
-
`to_json_schema`
|
|
833
|
+
`to_json_schema` returns the schema document itself. It used to return `{name:, description:, schema:, strict:}`, the shape OpenAI's `response_format` expects, and building that belongs in whatever talks to the provider.
|
|
747
834
|
|
|
748
|
-
If you were reaching into `[:schema]` to get at the document, drop the digging
|
|
835
|
+
If you were reaching into `[:schema]` to get at the document, drop the digging. Note the keys are strings, not symbols:
|
|
749
836
|
|
|
750
837
|
```ruby
|
|
751
838
|
schema.to_json_schema[:schema][:properties] # before
|
|
@@ -3,37 +3,39 @@
|
|
|
3
3
|
module Schematist
|
|
4
4
|
module DSL
|
|
5
5
|
module ComplexTypes
|
|
6
|
-
def object(name, description: nil, required: true, requires: nil, **options, &block)
|
|
7
|
-
|
|
6
|
+
def object(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
7
|
+
add_property_or_self(name, object_schema(description: description, **options, &block), required: required, requires: requires)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def array(name, description: nil, required: true, requires: nil, **options, &block)
|
|
11
|
-
|
|
10
|
+
def array(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
11
|
+
add_property_or_self(name, array_schema(description: description, **options, &block), required: required, requires: requires)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def tuple(name, description: nil, required: true, requires: nil, **options, &block)
|
|
15
|
-
|
|
14
|
+
def tuple(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
15
|
+
add_property_or_self(name, tuple_schema(description: description, **options, &block), required: required, requires: requires)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def any_of(name, description: nil, required: true, requires: nil, **options, &block)
|
|
19
|
-
|
|
18
|
+
def any_of(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
19
|
+
add_property_or_self(name, any_of_schema(description: description, **options, &block), required: required, requires: requires)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def one_of(name, description: nil, required: true, requires: nil, **options, &block)
|
|
23
|
-
|
|
22
|
+
def one_of(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
23
|
+
add_property_or_self(name, one_of_schema(description: description, **options, &block), required: required, requires: requires)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def all_of(name, description: nil, required: true, requires: nil, **options, &block)
|
|
27
|
-
|
|
26
|
+
def all_of(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
27
|
+
add_property_or_self(name, all_of_schema(description: description, **options, &block), required: required, requires: requires)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def none_of(name, description: nil, required: true, requires: nil, **options, &block)
|
|
31
|
-
|
|
30
|
+
def none_of(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
31
|
+
add_property_or_self(name, none_of_schema(description: description, **options, &block), required: required, requires: requires)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Emits a schema fragment verbatim, for the corners of JSON Schema this DSL does not cover
|
|
35
|
-
def raw(name, schema, required: true, requires: nil)
|
|
36
|
-
|
|
35
|
+
def raw(name, schema = nil, required: true, requires: nil)
|
|
36
|
+
return self_schema(name) && nil if schema.nil? && name.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
add_property_or_self(name, schema, required: required, requires: requires)
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def optional(name, description: nil, &block)
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Schematist
|
|
4
4
|
module DSL
|
|
5
|
+
# A branch of a conditional. Backed by a schema class, so anything you can write in a schema
|
|
6
|
+
# you can write in a branch: nested objects, arrays, composition, references, raw fragments.
|
|
7
|
+
# `requires` and `validates` stay as shorthands for the two common cases.
|
|
5
8
|
class ConditionalBuilder
|
|
6
|
-
def requires(*fields)
|
|
7
|
-
required.concat(fields.map(&:to_s))
|
|
8
|
-
end
|
|
9
|
-
|
|
10
9
|
VALIDATES_KEY_MAP = {
|
|
11
10
|
type: :type,
|
|
12
11
|
const: :const,
|
|
@@ -19,6 +18,15 @@ module Schematist
|
|
|
19
18
|
maximum: :maximum
|
|
20
19
|
}.freeze
|
|
21
20
|
|
|
21
|
+
def initialize
|
|
22
|
+
@schema_class = Class.new(Schema)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Requires properties without saying anything else about them
|
|
26
|
+
def requires(*fields)
|
|
27
|
+
required.concat(fields.map(&:to_s))
|
|
28
|
+
end
|
|
29
|
+
|
|
22
30
|
def validates(field, **options)
|
|
23
31
|
constraints = {}
|
|
24
32
|
|
|
@@ -38,28 +46,56 @@ module Schematist
|
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def to_schema
|
|
41
|
-
|
|
49
|
+
return @schema_class.self_schema.dup if @schema_class.self_schema
|
|
42
50
|
|
|
43
|
-
schema
|
|
44
|
-
schema[:properties] =
|
|
51
|
+
schema = {}
|
|
52
|
+
schema[:properties] = branch_properties if branch_properties.any?
|
|
53
|
+
schema[:required] = branch_required if branch_required.any?
|
|
54
|
+
schema[:additionalProperties] = @schema_class.additional_properties if additional_properties_set?
|
|
45
55
|
|
|
46
|
-
schema
|
|
56
|
+
@schema_class.send(:merge_schema_keywords, schema, @schema_class)
|
|
47
57
|
end
|
|
48
58
|
|
|
49
59
|
def empty?
|
|
50
|
-
|
|
60
|
+
to_schema.empty?
|
|
51
61
|
end
|
|
52
62
|
|
|
53
63
|
def required_fields
|
|
54
|
-
|
|
64
|
+
branch_required
|
|
55
65
|
end
|
|
56
66
|
|
|
67
|
+
# A branch that only lists required properties becomes dependentRequired rather than
|
|
68
|
+
# dependentSchemas, which is the smaller thing to say when it is all you mean.
|
|
57
69
|
def validations_empty?
|
|
58
|
-
|
|
70
|
+
to_schema.keys == [:required]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Everything else is the ordinary schema DSL, evaluated against the branch's schema class
|
|
74
|
+
def method_missing(name, ...)
|
|
75
|
+
return super unless @schema_class.respond_to?(name)
|
|
76
|
+
|
|
77
|
+
@schema_class.public_send(name, ...)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def respond_to_missing?(name, include_private = false)
|
|
81
|
+
@schema_class.respond_to?(name) || super
|
|
59
82
|
end
|
|
60
83
|
|
|
61
84
|
private
|
|
62
85
|
|
|
86
|
+
def branch_properties
|
|
87
|
+
declared = @schema_class.properties.transform_keys(&:to_s)
|
|
88
|
+
validations.merge(declared)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def branch_required
|
|
92
|
+
(@schema_class.required_properties.map(&:to_s) + required).uniq
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def additional_properties_set?
|
|
96
|
+
@schema_class.instance_variable_defined?(:@additional_properties)
|
|
97
|
+
end
|
|
98
|
+
|
|
63
99
|
def required
|
|
64
100
|
@required ||= []
|
|
65
101
|
end
|
|
@@ -8,16 +8,19 @@ module Schematist
|
|
|
8
8
|
@else_builder = else_builder
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def
|
|
12
|
-
@
|
|
11
|
+
def otherwise(&block)
|
|
12
|
+
@else_builder.instance_eval(&block)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# Everything else describes the then branch
|
|
16
|
+
def method_missing(name, ...)
|
|
17
|
+
return super unless @then_builder.respond_to?(name)
|
|
18
|
+
|
|
19
|
+
@then_builder.public_send(name, ...)
|
|
17
20
|
end
|
|
18
21
|
|
|
19
|
-
def
|
|
20
|
-
@
|
|
22
|
+
def respond_to_missing?(name, include_private = false)
|
|
23
|
+
@then_builder.respond_to?(name) || super
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
end
|
|
@@ -18,10 +18,13 @@ module Schematist
|
|
|
18
18
|
dependencies[property.to_s] = builder
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
# Matches on property values, or on any schema when one is passed explicitly:
|
|
22
|
+
# given(status: "shipped") { ... }
|
|
23
|
+
# given({required: %w[tax_id]}) { ... }
|
|
24
|
+
def given(condition = nil, **properties, &block)
|
|
25
|
+
raise ArgumentError, "given requires a condition" if condition.nil? && properties.empty?
|
|
23
26
|
|
|
24
|
-
if_schema = {
|
|
27
|
+
if_schema = condition || {
|
|
25
28
|
properties: properties.transform_keys(&:to_s).transform_values { |v| coerce_condition(v) },
|
|
26
29
|
required: properties.keys.map(&:to_s)
|
|
27
30
|
}
|
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
module Schematist
|
|
4
4
|
module DSL
|
|
5
5
|
module PrimitiveTypes
|
|
6
|
-
def string(name, description: nil, required: true, requires: nil, **options, &block)
|
|
7
|
-
|
|
6
|
+
def string(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
7
|
+
add_property_or_self(name, string_schema(description: description, **options, &block), required: required, requires: requires)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def number(name, description: nil, required: true, requires: nil, **options, &block)
|
|
11
|
-
|
|
10
|
+
def number(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
11
|
+
add_property_or_self(name, number_schema(description: description, **options, &block), required: required, requires: requires)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def integer(name, description: nil, required: true, requires: nil, **options, &block)
|
|
15
|
-
|
|
14
|
+
def integer(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
15
|
+
add_property_or_self(name, integer_schema(description: description, **options, &block), required: required, requires: requires)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def boolean(name, description: nil, required: true, requires: nil, **options, &block)
|
|
19
|
-
|
|
18
|
+
def boolean(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
19
|
+
add_property_or_self(name, boolean_schema(description: description, **options, &block), required: required, requires: requires)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def null(name, description: nil, required: true, requires: nil, **options, &block)
|
|
23
|
-
|
|
22
|
+
def null(name = nil, description: nil, required: true, requires: nil, **options, &block)
|
|
23
|
+
add_property_or_self(name, null_schema(description: description, **options, &block), required: required, requires: requires)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -23,12 +23,13 @@ module Schematist
|
|
|
23
23
|
}.compact, annotations, schema_block)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def number_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, multiple_of: nil, enum: nil, const: nil, **annotations, &block)
|
|
26
|
+
def number_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, multiple_of: nil, enum: nil, const: nil, format: nil, **annotations, &block)
|
|
27
27
|
schema_block = collect_schema_block(&block) if block
|
|
28
28
|
|
|
29
29
|
annotate({
|
|
30
30
|
type: "number",
|
|
31
31
|
description: description,
|
|
32
|
+
format: format,
|
|
32
33
|
minimum: minimum,
|
|
33
34
|
maximum: maximum,
|
|
34
35
|
exclusiveMinimum: greater_than,
|
|
@@ -39,12 +40,13 @@ module Schematist
|
|
|
39
40
|
}.compact, annotations, schema_block)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
def integer_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, multiple_of: nil, enum: nil, const: nil, **annotations, &block)
|
|
43
|
+
def integer_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, multiple_of: nil, enum: nil, const: nil, format: nil, **annotations, &block)
|
|
43
44
|
schema_block = collect_schema_block(&block) if block
|
|
44
45
|
|
|
45
46
|
annotate({
|
|
46
47
|
type: "integer",
|
|
47
48
|
description: description,
|
|
49
|
+
format: format,
|
|
48
50
|
minimum: minimum,
|
|
49
51
|
maximum: maximum,
|
|
50
52
|
exclusiveMinimum: greater_than,
|
|
@@ -55,16 +57,16 @@ module Schematist
|
|
|
55
57
|
}.compact, annotations, schema_block)
|
|
56
58
|
end
|
|
57
59
|
|
|
58
|
-
def boolean_schema(description: nil, const: nil, **annotations, &block)
|
|
60
|
+
def boolean_schema(description: nil, enum: nil, const: nil, **annotations, &block)
|
|
59
61
|
schema_block = collect_schema_block(&block) if block
|
|
60
62
|
|
|
61
|
-
annotate({type: "boolean", description: description, const: const}.compact, annotations, schema_block)
|
|
63
|
+
annotate({type: "boolean", description: description, enum: enum, const: const}.compact, annotations, schema_block)
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
def null_schema(description: nil, **annotations, &block)
|
|
66
|
+
def null_schema(description: nil, enum: nil, **annotations, &block)
|
|
65
67
|
schema_block = collect_schema_block(&block) if block
|
|
66
68
|
|
|
67
|
-
annotate({type: "null", description: description}.compact, annotations, schema_block)
|
|
69
|
+
annotate({type: "null", description: description, enum: enum}.compact, annotations, schema_block)
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
def object_schema(description: nil, of: nil, reference: nil, min_properties: nil, max_properties: nil, unevaluated_properties: nil, **annotations, &block)
|
|
@@ -96,57 +98,67 @@ module Schematist
|
|
|
96
98
|
}.compact, annotations, schema_block)
|
|
97
99
|
end
|
|
98
100
|
|
|
99
|
-
def tuple_schema(description: nil, **annotations, &block)
|
|
101
|
+
def tuple_schema(description: nil, of: nil, min_items: nil, max_items: nil, unevaluated_items: nil, **annotations, &block)
|
|
100
102
|
schema_block = collect_schema_block(&block)
|
|
101
103
|
schemas = schema_block.schemas
|
|
104
|
+
tail = determine_array_items(of) if of
|
|
105
|
+
|
|
106
|
+
# A tuple is exactly its prefix unless you give it somewhere for the rest to go
|
|
107
|
+
closed = tail.nil? && unevaluated_items.nil? && max_items.nil?
|
|
102
108
|
|
|
103
109
|
annotate({
|
|
104
110
|
type: "array",
|
|
105
111
|
description: description,
|
|
106
112
|
prefixItems: schemas,
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
items: tail,
|
|
114
|
+
minItems: min_items || schemas.length,
|
|
115
|
+
maxItems: closed ? schemas.length : max_items,
|
|
116
|
+
unevaluatedItems: unevaluated_items
|
|
109
117
|
}.compact, annotations, schema_block)
|
|
110
118
|
end
|
|
111
119
|
|
|
112
|
-
def any_of_schema(description: nil, unevaluated_properties: nil, **annotations, &block)
|
|
120
|
+
def any_of_schema(description: nil, unevaluated_properties: nil, unevaluated_items: nil, **annotations, &block)
|
|
113
121
|
schema_block = collect_schema_block(&block)
|
|
114
122
|
|
|
115
123
|
annotate({
|
|
116
124
|
description: description,
|
|
117
125
|
anyOf: schema_block.schemas,
|
|
118
|
-
unevaluatedProperties: unevaluated_properties
|
|
126
|
+
unevaluatedProperties: unevaluated_properties,
|
|
127
|
+
unevaluatedItems: unevaluated_items
|
|
119
128
|
}.compact, annotations, schema_block)
|
|
120
129
|
end
|
|
121
130
|
|
|
122
|
-
def one_of_schema(description: nil, unevaluated_properties: nil, **annotations, &block)
|
|
131
|
+
def one_of_schema(description: nil, unevaluated_properties: nil, unevaluated_items: nil, **annotations, &block)
|
|
123
132
|
schema_block = collect_schema_block(&block)
|
|
124
133
|
|
|
125
134
|
annotate({
|
|
126
135
|
description: description,
|
|
127
136
|
oneOf: schema_block.schemas,
|
|
128
|
-
unevaluatedProperties: unevaluated_properties
|
|
137
|
+
unevaluatedProperties: unevaluated_properties,
|
|
138
|
+
unevaluatedItems: unevaluated_items
|
|
129
139
|
}.compact, annotations, schema_block)
|
|
130
140
|
end
|
|
131
141
|
|
|
132
|
-
def all_of_schema(description: nil, unevaluated_properties: nil, **annotations, &block)
|
|
142
|
+
def all_of_schema(description: nil, unevaluated_properties: nil, unevaluated_items: nil, **annotations, &block)
|
|
133
143
|
schema_block = collect_schema_block(&block)
|
|
134
144
|
|
|
135
145
|
annotate({
|
|
136
146
|
description: description,
|
|
137
147
|
allOf: schema_block.schemas,
|
|
138
|
-
unevaluatedProperties: unevaluated_properties
|
|
148
|
+
unevaluatedProperties: unevaluated_properties,
|
|
149
|
+
unevaluatedItems: unevaluated_items
|
|
139
150
|
}.compact, annotations, schema_block)
|
|
140
151
|
end
|
|
141
152
|
|
|
142
|
-
def none_of_schema(description: nil, unevaluated_properties: nil, **annotations, &block)
|
|
153
|
+
def none_of_schema(description: nil, unevaluated_properties: nil, unevaluated_items: nil, **annotations, &block)
|
|
143
154
|
schema_block = collect_schema_block(&block)
|
|
144
155
|
schemas = schema_block.schemas
|
|
145
156
|
|
|
146
157
|
annotate({
|
|
147
158
|
description: description,
|
|
148
159
|
not: schemas.size == 1 ? schemas.first : {anyOf: schemas},
|
|
149
|
-
unevaluatedProperties: unevaluated_properties
|
|
160
|
+
unevaluatedProperties: unevaluated_properties,
|
|
161
|
+
unevaluatedItems: unevaluated_items
|
|
150
162
|
}.compact, annotations, schema_block)
|
|
151
163
|
end
|
|
152
164
|
|
|
@@ -162,6 +174,8 @@ module Schematist
|
|
|
162
174
|
end
|
|
163
175
|
|
|
164
176
|
def build_object_schema(description, &block)
|
|
177
|
+
raise InvalidObjectTypeError, "An object needs a block or an `of:` schema to describe it." unless block
|
|
178
|
+
|
|
165
179
|
sub_schema = Class.new(Schema)
|
|
166
180
|
result = sub_schema.class_eval(&block)
|
|
167
181
|
|
|
@@ -173,15 +187,10 @@ module Schematist
|
|
|
173
187
|
schema_class_to_inline_schema(result).merge(description ? {description: description} : {})
|
|
174
188
|
# Block didn't return reference or schema, so we build an inline object schema
|
|
175
189
|
else
|
|
176
|
-
schema =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
additionalProperties: sub_schema.additional_properties,
|
|
181
|
-
description: description
|
|
182
|
-
}.compact
|
|
183
|
-
|
|
184
|
-
merge_schema_keywords(schema, sub_schema)
|
|
190
|
+
schema = schema_for(sub_schema)
|
|
191
|
+
# The keyword wins over an annotation set inside the block
|
|
192
|
+
schema[:description] = description if description
|
|
193
|
+
schema
|
|
185
194
|
end
|
|
186
195
|
end
|
|
187
196
|
|
|
@@ -8,16 +8,7 @@ module Schematist
|
|
|
8
8
|
sub_schema = Class.new(Schema)
|
|
9
9
|
sub_schema.class_eval(&)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: sub_schema.properties,
|
|
14
|
-
required: sub_schema.required_properties,
|
|
15
|
-
additionalProperties: sub_schema.additional_properties
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
merge_schema_keywords(schema, sub_schema)
|
|
19
|
-
|
|
20
|
-
definitions[name] = schema
|
|
11
|
+
definitions[name] = schema_for(sub_schema)
|
|
21
12
|
end
|
|
22
13
|
|
|
23
14
|
def object_keywords
|
|
@@ -47,6 +38,26 @@ module Schematist
|
|
|
47
38
|
|
|
48
39
|
private
|
|
49
40
|
|
|
41
|
+
# What a schema class means as a schema: whatever it declared itself to be, or an object
|
|
42
|
+
# built from its properties.
|
|
43
|
+
def schema_for(schema_class)
|
|
44
|
+
schema = schema_class.self_schema&.dup || {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: schema_class.properties,
|
|
47
|
+
required: schema_class.required_properties,
|
|
48
|
+
additionalProperties: schema_class.additional_properties
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
merge_schema_keywords(schema, schema_class)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Declares a property when given a name, and what this schema is when not.
|
|
55
|
+
def add_property_or_self(name, definition, required:, requires: nil)
|
|
56
|
+
return self_schema(definition) && nil if name.nil?
|
|
57
|
+
|
|
58
|
+
add_property(name, definition, required: required, requires: requires)
|
|
59
|
+
end
|
|
60
|
+
|
|
50
61
|
# Merges everything a schema class collects beyond its properties: annotations, core keywords,
|
|
51
62
|
# key constraints, conditionals. Annotations are defaults, so an option passed to the enclosing
|
|
52
63
|
# builder wins over them.
|
|
@@ -27,18 +27,11 @@ module Schematist
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
29
|
def schema_body
|
|
30
|
-
schema_hash =
|
|
31
|
-
type: "object",
|
|
32
|
-
properties: self.class.properties,
|
|
33
|
-
required: self.class.required_properties,
|
|
34
|
-
additionalProperties: self.class.additional_properties
|
|
35
|
-
}
|
|
30
|
+
schema_hash = self.class.send(:schema_for, self.class)
|
|
36
31
|
|
|
37
32
|
# Only include $defs if there are definitions
|
|
38
33
|
schema_hash["$defs"] = self.class.definitions unless self.class.definitions.empty?
|
|
39
34
|
|
|
40
|
-
self.class.send(:merge_schema_keywords, schema_hash, self.class)
|
|
41
|
-
|
|
42
35
|
schema_hash
|
|
43
36
|
end
|
|
44
37
|
|
data/lib/schematist/schema.rb
CHANGED
|
@@ -91,6 +91,29 @@ module Schematist
|
|
|
91
91
|
core_keyword(:vocabulary, *args)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
# The schema this class is, when it is not an object built from properties. Set by
|
|
95
|
+
# calling a type without a name: `string enum: %w[a b]` rather than `string :status`.
|
|
96
|
+
def self_schema(definition = nil)
|
|
97
|
+
@self_schema = definition if definition
|
|
98
|
+
@self_schema
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def min_properties(value = nil)
|
|
102
|
+
object_keyword(:minProperties, value)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def max_properties(value = nil)
|
|
106
|
+
object_keyword(:maxProperties, value)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def unevaluated_properties(value = nil)
|
|
110
|
+
object_keyword(:unevaluatedProperties, value)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def unevaluated_items(value = nil)
|
|
114
|
+
object_keyword(:unevaluatedItems, value)
|
|
115
|
+
end
|
|
116
|
+
|
|
94
117
|
def additional_properties(value = nil)
|
|
95
118
|
return @additional_properties ||= false if value.nil?
|
|
96
119
|
|
|
@@ -109,6 +132,12 @@ module Schematist
|
|
|
109
132
|
|
|
110
133
|
private
|
|
111
134
|
|
|
135
|
+
def object_keyword(keyword, value)
|
|
136
|
+
return object_keywords[keyword] if value.nil?
|
|
137
|
+
|
|
138
|
+
object_keywords[keyword] = value
|
|
139
|
+
end
|
|
140
|
+
|
|
112
141
|
def annotation(name, *args)
|
|
113
142
|
read_or_write(annotations, ANNOTATIONS.fetch(name), *args)
|
|
114
143
|
end
|
data/lib/schematist/validator.rb
CHANGED
|
@@ -49,15 +49,10 @@ module Schematist
|
|
|
49
49
|
# Mark node with temporary mark
|
|
50
50
|
marks[node] = GRAY
|
|
51
51
|
|
|
52
|
-
# Visit all adjacent nodes (dependencies)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
references = extract_references(property)
|
|
57
|
-
references.each do |adjacent_node|
|
|
58
|
-
visit(adjacent_node, definitions, marks)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
52
|
+
# Visit all adjacent nodes (dependencies). A definition is any schema, not only an
|
|
53
|
+
# object with properties, so the whole thing is searched for references.
|
|
54
|
+
extract_references(definitions[node]).each do |adjacent_node|
|
|
55
|
+
visit(adjacent_node, definitions, marks)
|
|
61
56
|
end
|
|
62
57
|
|
|
63
58
|
# Mark node with permanent mark
|
data/lib/schematist/version.rb
CHANGED