jsonschema_rs 0.58.4 → 0.58.5
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/CHANGELOG.md +9 -1
- data/Cargo.toml +3 -3
- data/README.md +97 -65
- data/ext/jsonschema/Cargo.lock +40 -17
- data/ext/jsonschema/Cargo.toml +3 -3
- data/lib/jsonschema/version.rb +1 -1
- data/sig/jsonschema.rbs +18 -9
- data/src/canonical.rs +1 -1
- data/src/lib.rs +9 -10
- data/src/options.rs +23 -22
- data/src/registry.rs +6 -7
- data/src/retriever.rs +50 -0
- data/src/ser.rs +1 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c602494a0fa886d7e8ea3b14a5a707014b7723234371424cdb2315aa0a1d451f
|
|
4
|
+
data.tar.gz: 1634e82743fe6cd6c4634809a38dccc8bfbbf488e8e55a4038359ad8b0630399
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 105326b4a1e627bdc426e746b7bf5e2e1521aee3a6fe98739735799d4b6b7f5e49652d5c00beff8c8b66cee37b4ed7619692f8cb3318aa42dff77727f0b4d05a
|
|
7
|
+
data.tar.gz: 336eda3d631ac7f02af86b94488a49d7c720051360328f5d1e269a75b3144200f07de18e507be070248a04d509909334b66f5310bc9cf5c2bb7f413ed6a81cc2
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.58.5] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `canonicalize` keeping a reference to a cycle of bare references under `not` or `oneOf` instead of treating it as accepting every value.
|
|
10
|
+
- Validator construction holding the GVL while it retrieves external resources over HTTP or from files.
|
|
11
|
+
|
|
5
12
|
## [0.58.4] - 2026-10-01
|
|
6
13
|
|
|
7
14
|
### Fixed
|
|
@@ -637,7 +644,8 @@
|
|
|
637
644
|
|
|
638
645
|
- Initial public release
|
|
639
646
|
|
|
640
|
-
[Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.58.
|
|
647
|
+
[Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.58.5...HEAD
|
|
648
|
+
[0.58.5]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.58.4...ruby-v0.58.5
|
|
641
649
|
[0.58.4]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.58.3...ruby-v0.58.4
|
|
642
650
|
[0.58.3]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.58.2...ruby-v0.58.3
|
|
643
651
|
[0.58.2]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.58.1...ruby-v0.58.2
|
data/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "jsonschema-rb"
|
|
3
|
-
version = "0.58.
|
|
3
|
+
version = "0.58.5"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -14,8 +14,8 @@ crate-type = ["cdylib"]
|
|
|
14
14
|
|
|
15
15
|
[dependencies]
|
|
16
16
|
strum = "0.28.0"
|
|
17
|
-
jsonschema = { version = "0.58.
|
|
18
|
-
magnus = { version = "0.
|
|
17
|
+
jsonschema = { version = "0.58.5", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
|
|
18
|
+
magnus = { version = "0.9", features = ["rb-sys"] }
|
|
19
19
|
rb-sys = "0.9"
|
|
20
20
|
serde = { workspace = true }
|
|
21
21
|
serde_json = { workspace = true, features = ["arbitrary_precision"] }
|
data/README.md
CHANGED
|
@@ -45,29 +45,29 @@ end
|
|
|
45
45
|
|
|
46
46
|
## Highlights
|
|
47
47
|
|
|
48
|
-
- 📚
|
|
49
|
-
- 🌐 Remote reference fetching (network/file)
|
|
48
|
+
- 📚 Drafts 4, 6, 7, 2019-09 and 2020-12
|
|
50
49
|
- 🔧 Custom keywords and format validators
|
|
51
|
-
-
|
|
50
|
+
- ⚡ Compile-time validators for your own native extensions, via the Rust crate
|
|
51
|
+
- 🌐 `$ref` resolution over HTTP and from files
|
|
52
52
|
- 📦 Schema bundling into Compound Schema Documents, and `$ref` dereferencing
|
|
53
|
+
- 🎨 Structured Output v1 reports (flag/list/hierarchical)
|
|
54
|
+
- ✨ Meta-schema validation for schema documents, including custom metaschemas
|
|
53
55
|
- 🧮 Experimental schema canonicalization
|
|
54
56
|
- ♦️ Supports Ruby 3.2, 3.4 and 4.0
|
|
55
57
|
|
|
56
58
|
### Supported drafts
|
|
57
59
|
|
|
58
|
-
The following drafts are supported:
|
|
59
|
-
|
|
60
60
|
- [](https://bowtie.report/#/implementations/rust-jsonschema)
|
|
61
61
|
- [](https://bowtie.report/#/implementations/rust-jsonschema)
|
|
62
62
|
- [](https://bowtie.report/#/implementations/rust-jsonschema)
|
|
63
63
|
- [](https://bowtie.report/#/implementations/rust-jsonschema)
|
|
64
64
|
- [](https://bowtie.report/#/implementations/rust-jsonschema)
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Per-draft compliance results are on the [Bowtie Report](https://bowtie.report/#/implementations/rust-jsonschema).
|
|
67
67
|
|
|
68
68
|
## Playground
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
Try schemas in the browser with the WebAssembly [playground](https://jsonschema.dygalo.dev/).
|
|
71
71
|
|
|
72
72
|
## Installation
|
|
73
73
|
|
|
@@ -77,13 +77,13 @@ Add to your Gemfile:
|
|
|
77
77
|
gem 'jsonschema_rs'
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Pre-built native gems
|
|
80
|
+
Pre-built native gems:
|
|
81
81
|
|
|
82
82
|
- **Linux**: `x86_64`, `aarch64` (glibc and musl)
|
|
83
83
|
- **macOS**: `x86_64`, `arm64`
|
|
84
84
|
- **Windows**: `x64` (mingw-ucrt)
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
On other platforms, `gem install` compiles from source and needs:
|
|
87
87
|
- Ruby 3.2+
|
|
88
88
|
- Rust toolchain ([rustup](https://rustup.rs/))
|
|
89
89
|
|
|
@@ -91,8 +91,8 @@ If no pre-built gem is available for your platform, it will be compiled from sou
|
|
|
91
91
|
|
|
92
92
|
### Reusable validators
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
`validator_for`
|
|
94
|
+
Build a validator once to check many instances against one schema.
|
|
95
|
+
`validator_for` detects the draft from `$schema` and falls back to Draft 2020-12:
|
|
96
96
|
|
|
97
97
|
```ruby
|
|
98
98
|
validator = JSONSchema.validator_for({
|
|
@@ -108,7 +108,7 @@ validator.valid?({ "name" => "Alice", "age" => 30 }) # => true
|
|
|
108
108
|
validator.valid?({ "age" => 30 }) # => false
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
To pick the draft yourself, use a draft-specific class:
|
|
112
112
|
|
|
113
113
|
```ruby
|
|
114
114
|
validator = JSONSchema::Draft7Validator.new(schema)
|
|
@@ -119,6 +119,8 @@ validator = JSONSchema::Draft7Validator.new(schema)
|
|
|
119
119
|
|
|
120
120
|
### Custom format validators
|
|
121
121
|
|
|
122
|
+
Pass a callable that takes a `String` and returns a boolean via `formats`. Drafts 2019-09 and 2020-12 treat `format` as an annotation, so set `validate_formats: true` to get it checked:
|
|
123
|
+
|
|
122
124
|
```ruby
|
|
123
125
|
phone_format = ->(value) { value.match?(/^\+?[1-9]\d{1,14}$/) }
|
|
124
126
|
|
|
@@ -150,10 +152,10 @@ validator = JSONSchema.validator_for(
|
|
|
150
152
|
```
|
|
151
153
|
|
|
152
154
|
Each custom keyword class must implement:
|
|
153
|
-
- `initialize(parent_schema, value, schema_path)
|
|
154
|
-
- `validate(instance)
|
|
155
|
+
- `initialize(parent_schema, value, schema_path)`: called once when the validator is built
|
|
156
|
+
- `validate(instance)`: raise to reject the value, return to accept it
|
|
155
157
|
|
|
156
|
-
|
|
158
|
+
To report several problems from one keyword, also implement `iter_errors(instance)` returning an array of exceptions. `each_error` yields each of them. Without it, `each_error` yields the single error from `validate`:
|
|
157
159
|
|
|
158
160
|
```ruby
|
|
159
161
|
class AllPositive
|
|
@@ -172,7 +174,7 @@ class AllPositive
|
|
|
172
174
|
end
|
|
173
175
|
```
|
|
174
176
|
|
|
175
|
-
|
|
177
|
+
The resulting `ValidationError` keeps the raised exception as `cause`, for both `validate` and `iter_errors`:
|
|
176
178
|
|
|
177
179
|
```ruby
|
|
178
180
|
begin
|
|
@@ -185,7 +187,7 @@ end
|
|
|
185
187
|
|
|
186
188
|
### Structured evaluation output
|
|
187
189
|
|
|
188
|
-
|
|
190
|
+
`evaluate` returns the [JSON Schema Output v1](https://json-schema.org/draft/2020-12/json-schema-core#name-output-formatting) formats instead of a boolean:
|
|
189
191
|
|
|
190
192
|
```ruby
|
|
191
193
|
schema = {
|
|
@@ -203,14 +205,14 @@ evaluation = validator.evaluate({ "age" => "not_an_integer" })
|
|
|
203
205
|
evaluation.valid? # => false
|
|
204
206
|
```
|
|
205
207
|
|
|
206
|
-
**Flag output
|
|
208
|
+
**Flag output**: valid or invalid, nothing else:
|
|
207
209
|
|
|
208
210
|
```ruby
|
|
209
211
|
evaluation.flag
|
|
210
212
|
# => {valid: false}
|
|
211
213
|
```
|
|
212
214
|
|
|
213
|
-
**List output
|
|
215
|
+
**List output**: every evaluation node in a flat list:
|
|
214
216
|
|
|
215
217
|
```ruby
|
|
216
218
|
evaluation.list
|
|
@@ -231,7 +233,7 @@ evaluation.list
|
|
|
231
233
|
# ]}
|
|
232
234
|
```
|
|
233
235
|
|
|
234
|
-
**Hierarchical output
|
|
236
|
+
**Hierarchical output**: the same nodes nested by schema structure:
|
|
235
237
|
|
|
236
238
|
```ruby
|
|
237
239
|
evaluation.hierarchical
|
|
@@ -255,7 +257,7 @@ evaluation.hierarchical
|
|
|
255
257
|
# ]}
|
|
256
258
|
```
|
|
257
259
|
|
|
258
|
-
**Collected errors
|
|
260
|
+
**Collected errors**: every error across all nodes:
|
|
259
261
|
|
|
260
262
|
```ruby
|
|
261
263
|
evaluation.errors
|
|
@@ -266,8 +268,8 @@ evaluation.errors
|
|
|
266
268
|
# error: "\"not_an_integer\" is not of type \"integer\""}]
|
|
267
269
|
```
|
|
268
270
|
|
|
269
|
-
**Collected annotations
|
|
270
|
-
|
|
271
|
+
**Collected annotations**: annotations from the nodes that passed.
|
|
272
|
+
A failed node reports its annotations as `droppedAnnotations` in the list and hierarchical output instead.
|
|
271
273
|
|
|
272
274
|
```ruby
|
|
273
275
|
valid_eval = validator.evaluate({ "name" => "Alice", "age" => 30 })
|
|
@@ -278,7 +280,7 @@ valid_eval.annotations
|
|
|
278
280
|
|
|
279
281
|
### Canonical JSON serialization
|
|
280
282
|
|
|
281
|
-
|
|
283
|
+
`Canonical::JSON.to_string` serializes equal JSON values to the same string, regardless of key order:
|
|
282
284
|
|
|
283
285
|
```ruby
|
|
284
286
|
schema_a = { "type" => "object", "properties" => { "b" => { "type" => "integer" }, "a" => { "type" => "string" } } }
|
|
@@ -290,13 +292,13 @@ dump_b = JSONSchema::Canonical::JSON.to_string(schema_b)
|
|
|
290
292
|
dump_a == dump_b # => true
|
|
291
293
|
```
|
|
292
294
|
|
|
293
|
-
|
|
295
|
+
Use it to deduplicate schemas that differ only in key order.
|
|
294
296
|
|
|
295
297
|
## Schema Canonicalization
|
|
296
298
|
|
|
297
299
|
> **Experimental**: the canonicalization API may change in minor releases.
|
|
298
300
|
|
|
299
|
-
`JSONSchema.canonicalize` reduces a schema to a normal form
|
|
301
|
+
`JSONSchema.canonicalize` reduces a schema to a normal form that accepts the same values. Schemas that accept the same values reduce to equal `CanonicalSchema` objects, and a schema proven to accept nothing reduces to `false`:
|
|
300
302
|
|
|
301
303
|
```ruby
|
|
302
304
|
canonical = JSONSchema.canonicalize(
|
|
@@ -315,7 +317,9 @@ JSONSchema.canonicalize({ "type" => "integer", "minimum" => 10, "maximum" => 5 }
|
|
|
315
317
|
# => :no, which is JSONSchema::Canonical::Satisfiability::NO
|
|
316
318
|
```
|
|
317
319
|
|
|
318
|
-
A canonical
|
|
320
|
+
A schema the canonical form cannot model exactly comes back unchanged, with `kind == :raw`. Its `view.reason` names what stopped the run, and `view.pointer` the subschema at fault when a single one is.
|
|
321
|
+
|
|
322
|
+
Canonical schemas combine like sets of values. Every emitted schema carries `$schema`; the comments below leave it out:
|
|
319
323
|
|
|
320
324
|
```ruby
|
|
321
325
|
positive = JSONSchema.canonicalize({ "type" => "integer", "minimum" => 0 })
|
|
@@ -333,19 +337,19 @@ positive.union(bounded).to_json_schema
|
|
|
333
337
|
positive.subtract(bounded).to_json_schema
|
|
334
338
|
# => {"type"=>"integer", "minimum"=>101}
|
|
335
339
|
|
|
336
|
-
# Every value `positive` rejects,
|
|
340
|
+
# Every value `positive` rejects: other types, negative integers and non-integer numbers
|
|
337
341
|
positive.negate.to_json_schema
|
|
338
342
|
# => {"anyOf"=>[{"type"=>["null", "boolean", "string", "array", "object"]},
|
|
339
343
|
# {"type"=>"integer", "maximum"=>-1},
|
|
340
344
|
# {"type"=>"number", "not"=>{"multipleOf"=>1}}]}
|
|
341
345
|
|
|
342
|
-
positive.covers(bounded) # => :no
|
|
346
|
+
positive.covers(bounded) # => :no: `bounded` takes negative integers, `positive` does not
|
|
343
347
|
positive.satisfiability # => :yes
|
|
344
348
|
```
|
|
345
349
|
|
|
346
350
|
### Comparing two versions of a schema
|
|
347
351
|
|
|
348
|
-
|
|
352
|
+
`subtract` tells you what an edit did to a schema. `old.subtract(new)` accepts exactly the values `old` accepts and `new` rejects, so it accepts nothing when the edit lost nothing.
|
|
349
353
|
|
|
350
354
|
```ruby
|
|
351
355
|
old = JSONSchema.canonicalize({ "type" => "string" })
|
|
@@ -361,19 +365,23 @@ new.subtract(old).satisfiability
|
|
|
361
365
|
# => :no
|
|
362
366
|
```
|
|
363
367
|
|
|
364
|
-
|
|
368
|
+
The direction to check depends on who sends the value:
|
|
365
369
|
|
|
366
|
-
|
|
370
|
+
- For a request schema, check `old.subtract(new)`. It accepts the payloads existing callers send that the new schema rejects.
|
|
371
|
+
- For a response schema, check `new.subtract(old)`. It accepts the values the new schema lets a server return that callers never agreed to read.
|
|
367
372
|
|
|
368
|
-
`:
|
|
373
|
+
`:no` on the difference proves the edit safe in that direction. `:unknown` means the canonicalizer could not decide, and proves nothing either way. Read it as the answer that keeps you safe:
|
|
374
|
+
|
|
375
|
+
- `satisfiability`: only `:no` proves a schema accepts nothing. Treat `:unknown` like `:yes`.
|
|
376
|
+
- `a.covers(b)`: only `:yes` proves `a` accepts every value `b` accepts. Treat `:unknown` like `:no`.
|
|
369
377
|
|
|
370
378
|
`JSONSchema::Canonical::Containment`, `Satisfiability`, `Distinctness` and `Kind` hold these symbols as constants, each with an `ALL` list.
|
|
371
379
|
|
|
372
|
-
|
|
380
|
+
The set operations raise `IncompatibleOperands` when the operands differ in draft, `format` assertion or regular-expression engine, or resolve `#` or one external resource to different schemas. They raise `UnsupportedOperand` when an operand is `:raw`, and `UnsupportedResult` when the canonical form cannot express the result exactly. All three live under `JSONSchema::Canonical`.
|
|
373
381
|
|
|
374
382
|
### Finding the dead subschemas of a document
|
|
375
383
|
|
|
376
|
-
`JSONSchema::Canonical.find_unsatisfiable` walks a whole document and
|
|
384
|
+
`JSONSchema::Canonical.find_unsatisfiable` walks a whole document and reports each subschema that accepts no value, with the keywords at fault and their pointers:
|
|
377
385
|
|
|
378
386
|
```ruby
|
|
379
387
|
reasons = JSONSchema::Canonical.find_unsatisfiable(
|
|
@@ -396,11 +404,11 @@ reasons.key?("/properties/name")
|
|
|
396
404
|
# => false
|
|
397
405
|
```
|
|
398
406
|
|
|
399
|
-
A reason is a `LiteralReason` (written as `false`), an `EmptyReason` (one part admits nothing by itself) or a `ConflictReason` (each part admits values, together they admit none). A pointer
|
|
407
|
+
A reason is a `LiteralReason` (written as `false`), an `EmptyReason` (one part admits nothing by itself) or a `ConflictReason` (each part admits values, together they admit none). A missing pointer does not prove that subschema satisfiable. For a document the canonical form cannot model, `find_unsatisfiable` reports nothing, the same way `satisfiability` answers `:unknown`.
|
|
400
408
|
|
|
401
409
|
## Schema Bundling and Dereferencing
|
|
402
410
|
|
|
403
|
-
Produce a Compound Schema Document ([Appendix B](https://json-schema.org/draft/2020-12/json-schema-core#appendix-B)) by embedding all external `$ref` targets into a draft-appropriate container. The
|
|
411
|
+
Produce a Compound Schema Document ([Appendix B](https://json-schema.org/draft/2020-12/json-schema-core#appendix-B)) by embedding all external `$ref` targets into a draft-appropriate container. The bundle accepts the same values as the original.
|
|
404
412
|
|
|
405
413
|
```ruby
|
|
406
414
|
address_schema = {
|
|
@@ -430,7 +438,7 @@ dereferenced = JSONSchema.dereference(schema, registry: registry)
|
|
|
430
438
|
|
|
431
439
|
## Meta-Schema Validation
|
|
432
440
|
|
|
433
|
-
|
|
441
|
+
`JSONSchema::Meta` checks a schema against the meta-schema of its draft:
|
|
434
442
|
|
|
435
443
|
```ruby
|
|
436
444
|
JSONSchema::Meta.valid?({ "type" => "string" }) # => true
|
|
@@ -443,9 +451,24 @@ rescue JSONSchema::ValidationError => e
|
|
|
443
451
|
end
|
|
444
452
|
```
|
|
445
453
|
|
|
454
|
+
For a custom metaschema, register it and point `$schema` at its URI:
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
metaschema = {
|
|
458
|
+
"$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
459
|
+
"$id" => "https://example.com/meta",
|
|
460
|
+
"type" => "object",
|
|
461
|
+
"required" => ["title"]
|
|
462
|
+
}
|
|
463
|
+
meta_registry = JSONSchema::Registry.new([["https://example.com/meta", metaschema]])
|
|
464
|
+
|
|
465
|
+
JSONSchema::Meta.valid?({ "$schema" => "https://example.com/meta", "title" => "Person" }, registry: meta_registry) # => true
|
|
466
|
+
JSONSchema::Meta.valid?({ "$schema" => "https://example.com/meta" }, registry: meta_registry) # => false
|
|
467
|
+
```
|
|
468
|
+
|
|
446
469
|
## External References
|
|
447
470
|
|
|
448
|
-
By default, `
|
|
471
|
+
By default, `jsonschema_rs` fetches external `$ref` targets over HTTP and from the local file system. Pass a `retriever:` to load them yourself:
|
|
449
472
|
|
|
450
473
|
```ruby
|
|
451
474
|
schemas = {
|
|
@@ -468,9 +491,21 @@ validator.valid?({ "name" => "Alice", "age" => 30 }) # => true
|
|
|
468
491
|
validator.valid?({ "name" => "Bob" }) # => false (missing "age")
|
|
469
492
|
```
|
|
470
493
|
|
|
494
|
+
For schemas from untrusted sources, pass `offline: true`. The validator then refuses to fetch any `$ref` target, so a schema cannot reach your network or file system. Schemas held in a `Registry` still resolve:
|
|
495
|
+
|
|
496
|
+
```ruby
|
|
497
|
+
begin
|
|
498
|
+
JSONSchema.validator_for({ "$ref" => "https://example.com/other.json" }, offline: true)
|
|
499
|
+
rescue JSONSchema::ReferencingError => e
|
|
500
|
+
e.message # => "Resource 'https://example.com/other.json' is not present in a registry ..."
|
|
501
|
+
end
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
`bundle` and `dereference` take `offline:` too.
|
|
505
|
+
|
|
471
506
|
## Schema Registry
|
|
472
507
|
|
|
473
|
-
|
|
508
|
+
A `Registry` holds schemas by URI, so a validator resolves `$ref`s to them without fetching anything:
|
|
474
509
|
|
|
475
510
|
```ruby
|
|
476
511
|
registry = JSONSchema::Registry.new([
|
|
@@ -501,7 +536,7 @@ validator.valid?({
|
|
|
501
536
|
}) # => true
|
|
502
537
|
```
|
|
503
538
|
|
|
504
|
-
|
|
539
|
+
`Registry` also takes a default `draft:` and a `retriever:` for URIs it does not hold:
|
|
505
540
|
|
|
506
541
|
```ruby
|
|
507
542
|
registry = JSONSchema::Registry.new(
|
|
@@ -513,7 +548,7 @@ registry = JSONSchema::Registry.new(
|
|
|
513
548
|
|
|
514
549
|
## Regular Expression Configuration
|
|
515
550
|
|
|
516
|
-
|
|
551
|
+
`pattern_options:` picks the regex engine for `pattern` and `patternProperties` and sets its limits:
|
|
517
552
|
|
|
518
553
|
```ruby
|
|
519
554
|
# Default fancy-regex engine with backtracking limits
|
|
@@ -540,24 +575,22 @@ validator = JSONSchema.validator_for(
|
|
|
540
575
|
)
|
|
541
576
|
```
|
|
542
577
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
- `FancyRegexOptions`: Default engine with lookaround and backreferences support
|
|
578
|
+
- `FancyRegexOptions`: default engine, supports lookaround and backreferences
|
|
546
579
|
|
|
547
580
|
- `backtrack_limit`: Maximum backtracking steps
|
|
548
581
|
- `size_limit`: Maximum compiled regex size in bytes
|
|
549
582
|
- `dfa_size_limit`: Maximum DFA cache size in bytes
|
|
550
583
|
|
|
551
|
-
- `RegexOptions`:
|
|
584
|
+
- `RegexOptions`: matches in linear time, no lookaround or backreferences
|
|
552
585
|
|
|
553
586
|
- `size_limit`: Maximum compiled regex size in bytes
|
|
554
587
|
- `dfa_size_limit`: Maximum DFA cache size in bytes
|
|
555
588
|
|
|
556
|
-
|
|
589
|
+
If you validate against schemas from untrusted sources, use `RegexOptions`: a crafted pattern cannot make it backtrack.
|
|
557
590
|
|
|
558
591
|
## Email Format Configuration
|
|
559
592
|
|
|
560
|
-
|
|
593
|
+
`email_options:` makes `{"format": "email"}` stricter or looser than the spec default:
|
|
561
594
|
|
|
562
595
|
```ruby
|
|
563
596
|
# Require a top-level domain (reject "user@localhost")
|
|
@@ -587,16 +620,14 @@ validator = JSONSchema.validator_for(
|
|
|
587
620
|
)
|
|
588
621
|
```
|
|
589
622
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
- `require_tld`: Require a top-level domain (e.g., reject "user@localhost")
|
|
623
|
+
- `require_tld`: Require a top-level domain (e.g., reject "user@localhost") (default: false)
|
|
593
624
|
- `allow_domain_literal`: Allow IP address literals like "user@[127.0.0.1]" (default: true)
|
|
594
625
|
- `allow_display_text`: Allow display names like "Name <user@example.com>" (default: true)
|
|
595
626
|
- `minimum_sub_domains`: Minimum number of domain segments required
|
|
596
627
|
|
|
597
628
|
## Error Handling
|
|
598
629
|
|
|
599
|
-
`
|
|
630
|
+
A `ValidationError` carries the message, both locations and a `kind` with keyword-specific details:
|
|
600
631
|
|
|
601
632
|
```ruby
|
|
602
633
|
schema = { "type" => "string", "maxLength" => 5 }
|
|
@@ -619,7 +650,7 @@ end
|
|
|
619
650
|
|
|
620
651
|
### Error Kind Properties
|
|
621
652
|
|
|
622
|
-
|
|
653
|
+
`kind` has generic accessors:
|
|
623
654
|
|
|
624
655
|
```ruby
|
|
625
656
|
JSONSchema.each_error({ "minimum" => 5 }, 3).each do |error|
|
|
@@ -632,7 +663,7 @@ end
|
|
|
632
663
|
|
|
633
664
|
### Error Message Masking
|
|
634
665
|
|
|
635
|
-
|
|
666
|
+
Pass `mask:` to replace instance values in error messages with a placeholder:
|
|
636
667
|
|
|
637
668
|
```ruby
|
|
638
669
|
schema = {
|
|
@@ -657,19 +688,19 @@ end
|
|
|
657
688
|
|
|
658
689
|
### Exception Classes
|
|
659
690
|
|
|
660
|
-
- **`JSONSchema::ValidationError
|
|
691
|
+
- **`JSONSchema::ValidationError`**: raised on validation failure
|
|
661
692
|
- `message`, `verbose_message`, `instance_path`, `schema_path`, `evaluation_path`, `kind`, `instance`
|
|
662
693
|
- JSON Pointer helpers: `instance_path_pointer`, `schema_path_pointer`, `evaluation_path_pointer`
|
|
663
|
-
- **`JSONSchema::ReferencingError
|
|
694
|
+
- **`JSONSchema::ReferencingError`**: raised when a `$ref` cannot be resolved
|
|
664
695
|
|
|
665
696
|
## Options Reference
|
|
666
697
|
|
|
667
|
-
|
|
698
|
+
The one-off methods `valid?`, `validate!` and `each_error` accept these keyword arguments:
|
|
668
699
|
|
|
669
700
|
```ruby
|
|
670
701
|
JSONSchema.valid?(schema, instance,
|
|
671
702
|
draft: :draft7, # Specific draft version (symbol)
|
|
672
|
-
validate_formats: true, #
|
|
703
|
+
validate_formats: true, # Check `format` (default: on up to Draft 7, off for 2019-09 and 2020-12)
|
|
673
704
|
ignore_unknown_formats: true, # Don't error on unknown formats (default: true)
|
|
674
705
|
base_uri: "https://example.com", # Base URI for reference resolution
|
|
675
706
|
mask: "[REDACTED]", # Mask sensitive data in error messages
|
|
@@ -680,25 +711,26 @@ JSONSchema.valid?(schema, instance,
|
|
|
680
711
|
vocabularies: ["https://..."], # Vocabularies implemented by custom keywords
|
|
681
712
|
pattern_options: opts, # RegexOptions or FancyRegexOptions
|
|
682
713
|
email_options: opts, # EmailOptions
|
|
683
|
-
http_options: opts
|
|
714
|
+
http_options: opts, # HttpOptions
|
|
715
|
+
offline: false # true refuses to fetch $refs; conflicts with retriever:
|
|
684
716
|
)
|
|
685
717
|
```
|
|
686
718
|
|
|
687
719
|
`evaluate` accepts the same options except `mask` (currently unsupported for evaluation output).
|
|
688
720
|
|
|
689
|
-
`validator_for` accepts the same options except `draft
|
|
721
|
+
`validator_for` accepts the same options except `draft:`. To pin a draft, use a draft-specific class such as `Draft7Validator.new`.
|
|
690
722
|
|
|
691
723
|
Valid draft symbols: `:draft4`, `:draft6`, `:draft7`, `:draft201909`, `:draft202012`.
|
|
692
724
|
|
|
693
725
|
## Performance
|
|
694
726
|
|
|
695
|
-
|
|
727
|
+
Compared with other Ruby validators:
|
|
696
728
|
|
|
697
729
|
- **28-148x** faster than `json_schemer` for complex schemas and large instances
|
|
698
730
|
- **200-567x** faster than `json-schema` where supported
|
|
699
731
|
- **7-130x** faster than `rj_schema` (RapidJSON/C++)
|
|
700
732
|
|
|
701
|
-
|
|
733
|
+
Full results and methodology are in [BENCHMARKS.md](https://github.com/Stranger6667/jsonschema/blob/master/crates/jsonschema-rb/BENCHMARKS.md).
|
|
702
734
|
|
|
703
735
|
### Compile-Time Validators
|
|
704
736
|
|
|
@@ -714,15 +746,15 @@ A complete extension with its build and test commands lives in
|
|
|
714
746
|
|
|
715
747
|
## Acknowledgements
|
|
716
748
|
|
|
717
|
-
|
|
749
|
+
The API design draws on the Python [`jsonschema`](https://github.com/python-jsonschema/jsonschema) package. Thanks to its maintainers and contributors.
|
|
718
750
|
|
|
719
751
|
## Support
|
|
720
752
|
|
|
721
|
-
|
|
753
|
+
Ask questions and suggest improvements in [GitHub Discussions](https://github.com/Stranger6667/jsonschema/discussions).
|
|
722
754
|
|
|
723
755
|
## Sponsorship
|
|
724
756
|
|
|
725
|
-
If you find `
|
|
757
|
+
If you find `jsonschema_rs` useful, please consider [sponsoring its development](https://github.com/sponsors/Stranger6667).
|
|
726
758
|
|
|
727
759
|
## Contributing
|
|
728
760
|
|
data/ext/jsonschema/Cargo.lock
CHANGED
|
@@ -693,9 +693,9 @@ dependencies = [
|
|
|
693
693
|
|
|
694
694
|
[[package]]
|
|
695
695
|
name = "jsonschema"
|
|
696
|
-
version = "0.58.
|
|
696
|
+
version = "0.58.5"
|
|
697
697
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
698
|
-
checksum = "
|
|
698
|
+
checksum = "ea18b8d5e1469b1bdd6b349169305141316a7c26da27f239773b65c830c0f5d6"
|
|
699
699
|
dependencies = [
|
|
700
700
|
"ahash",
|
|
701
701
|
"bytecount",
|
|
@@ -709,7 +709,7 @@ dependencies = [
|
|
|
709
709
|
"jsonschema-macros",
|
|
710
710
|
"jsonschema-regex",
|
|
711
711
|
"jsonschema-value",
|
|
712
|
-
"magnus",
|
|
712
|
+
"magnus 0.9.1",
|
|
713
713
|
"num-bigint",
|
|
714
714
|
"num-cmp",
|
|
715
715
|
"num-traits",
|
|
@@ -727,9 +727,9 @@ dependencies = [
|
|
|
727
727
|
|
|
728
728
|
[[package]]
|
|
729
729
|
name = "jsonschema-macros"
|
|
730
|
-
version = "0.58.
|
|
730
|
+
version = "0.58.5"
|
|
731
731
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
732
|
-
checksum = "
|
|
732
|
+
checksum = "ef3da09f0a27daeae23033d6e018a5c4b606953a3a90eee16f3e360e975444d3"
|
|
733
733
|
dependencies = [
|
|
734
734
|
"jsonschema-macros-core",
|
|
735
735
|
"proc-macro2",
|
|
@@ -737,9 +737,9 @@ dependencies = [
|
|
|
737
737
|
|
|
738
738
|
[[package]]
|
|
739
739
|
name = "jsonschema-macros-core"
|
|
740
|
-
version = "0.58.
|
|
740
|
+
version = "0.58.5"
|
|
741
741
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
742
|
-
checksum = "
|
|
742
|
+
checksum = "a12bd067dca38b8182b307bca050da4c5a73f9be452d06415e03a0957517a70b"
|
|
743
743
|
dependencies = [
|
|
744
744
|
"fancy-regex",
|
|
745
745
|
"indexmap",
|
|
@@ -756,10 +756,10 @@ dependencies = [
|
|
|
756
756
|
|
|
757
757
|
[[package]]
|
|
758
758
|
name = "jsonschema-rb-ext"
|
|
759
|
-
version = "0.58.
|
|
759
|
+
version = "0.58.5"
|
|
760
760
|
dependencies = [
|
|
761
761
|
"jsonschema",
|
|
762
|
-
"magnus",
|
|
762
|
+
"magnus 0.8.2",
|
|
763
763
|
"rb-sys",
|
|
764
764
|
"referencing",
|
|
765
765
|
"serde",
|
|
@@ -770,24 +770,24 @@ dependencies = [
|
|
|
770
770
|
|
|
771
771
|
[[package]]
|
|
772
772
|
name = "jsonschema-regex"
|
|
773
|
-
version = "0.58.
|
|
773
|
+
version = "0.58.5"
|
|
774
774
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
775
|
-
checksum = "
|
|
775
|
+
checksum = "854e9e22c420535035e95eed240cbfb4a5f4b7da74d122d63144e0c31926dbfd"
|
|
776
776
|
dependencies = [
|
|
777
777
|
"regex-syntax",
|
|
778
778
|
]
|
|
779
779
|
|
|
780
780
|
[[package]]
|
|
781
781
|
name = "jsonschema-value"
|
|
782
|
-
version = "0.58.
|
|
782
|
+
version = "0.58.5"
|
|
783
783
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
784
|
-
checksum = "
|
|
784
|
+
checksum = "4e3bfac11ac357ec620880025757d38c67da8fd5b9ef5024d19f0f360002b4ca"
|
|
785
785
|
dependencies = [
|
|
786
786
|
"ahash",
|
|
787
787
|
"bytecount",
|
|
788
788
|
"fraction",
|
|
789
789
|
"getrandom 0.3.4",
|
|
790
|
-
"magnus",
|
|
790
|
+
"magnus 0.9.1",
|
|
791
791
|
"num-bigint",
|
|
792
792
|
"num-cmp",
|
|
793
793
|
"num-traits",
|
|
@@ -845,7 +845,19 @@ version = "0.8.2"
|
|
|
845
845
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
846
846
|
checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
|
|
847
847
|
dependencies = [
|
|
848
|
-
"magnus-macros",
|
|
848
|
+
"magnus-macros 0.8.0",
|
|
849
|
+
"rb-sys",
|
|
850
|
+
"rb-sys-env",
|
|
851
|
+
"seq-macro",
|
|
852
|
+
]
|
|
853
|
+
|
|
854
|
+
[[package]]
|
|
855
|
+
name = "magnus"
|
|
856
|
+
version = "0.9.1"
|
|
857
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
858
|
+
checksum = "4c01bef153b8d0f086991f0e166474a1d57b738221f9a9c11405d498ac95a44d"
|
|
859
|
+
dependencies = [
|
|
860
|
+
"magnus-macros 0.9.1",
|
|
849
861
|
"rb-sys",
|
|
850
862
|
"rb-sys-env",
|
|
851
863
|
"seq-macro",
|
|
@@ -862,6 +874,17 @@ dependencies = [
|
|
|
862
874
|
"syn",
|
|
863
875
|
]
|
|
864
876
|
|
|
877
|
+
[[package]]
|
|
878
|
+
name = "magnus-macros"
|
|
879
|
+
version = "0.9.1"
|
|
880
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
881
|
+
checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
|
|
882
|
+
dependencies = [
|
|
883
|
+
"proc-macro2",
|
|
884
|
+
"quote",
|
|
885
|
+
"syn",
|
|
886
|
+
]
|
|
887
|
+
|
|
865
888
|
[[package]]
|
|
866
889
|
name = "memchr"
|
|
867
890
|
version = "2.8.0"
|
|
@@ -1136,9 +1159,9 @@ dependencies = [
|
|
|
1136
1159
|
|
|
1137
1160
|
[[package]]
|
|
1138
1161
|
name = "referencing"
|
|
1139
|
-
version = "0.58.
|
|
1162
|
+
version = "0.58.5"
|
|
1140
1163
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1141
|
-
checksum = "
|
|
1164
|
+
checksum = "590efadb0a669f1712c1e3ab810a55d4c3d20d0127c455f7236dd0710aba0346"
|
|
1142
1165
|
dependencies = [
|
|
1143
1166
|
"ahash",
|
|
1144
1167
|
"fluent-uri",
|
data/ext/jsonschema/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "jsonschema-rb-ext"
|
|
3
|
-
version = "0.58.
|
|
3
|
+
version = "0.58.5"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
publish = false
|
|
6
6
|
|
|
@@ -10,10 +10,10 @@ name = "jsonschema_rb"
|
|
|
10
10
|
path = "../../src/lib.rs"
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
|
-
jsonschema = { version = "0.58.
|
|
13
|
+
jsonschema = { version = "0.58.5", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
|
|
14
14
|
magnus = { version = "0.8", features = ["rb-sys"] }
|
|
15
15
|
rb-sys = "0.9"
|
|
16
|
-
referencing = "0.58.
|
|
16
|
+
referencing = "0.58.5"
|
|
17
17
|
serde = { version = "1", features = ["derive"] }
|
|
18
18
|
serde_json = { version = "1", features = ["arbitrary_precision"] }
|
|
19
19
|
strum = "0.28.0"
|
data/lib/jsonschema/version.rb
CHANGED
data/sig/jsonschema.rbs
CHANGED
|
@@ -391,7 +391,8 @@ module JSONSchema
|
|
|
391
391
|
?registry: Registry?,
|
|
392
392
|
?pattern_options: (RegexOptions | FancyRegexOptions)?,
|
|
393
393
|
?email_options: EmailOptions?,
|
|
394
|
-
?http_options: HttpOptions
|
|
394
|
+
?http_options: HttpOptions?,
|
|
395
|
+
?offline: bool?
|
|
395
396
|
) -> Validator
|
|
396
397
|
|
|
397
398
|
# Create a map of validators for all sub-schemas in a JSON Schema.
|
|
@@ -408,7 +409,8 @@ module JSONSchema
|
|
|
408
409
|
?keywords: ::Hash[::String, untyped]?,
|
|
409
410
|
?pattern_options: (RegexOptions | FancyRegexOptions)?,
|
|
410
411
|
?email_options: EmailOptions?,
|
|
411
|
-
?http_options: HttpOptions
|
|
412
|
+
?http_options: HttpOptions?,
|
|
413
|
+
?offline: bool?
|
|
412
414
|
) -> ValidatorMap
|
|
413
415
|
|
|
414
416
|
# Bundle a JSON Schema into a Compound Schema Document.
|
|
@@ -421,7 +423,8 @@ module JSONSchema
|
|
|
421
423
|
?draft: draft?,
|
|
422
424
|
?retriever: (^(String) -> untyped)?,
|
|
423
425
|
?registry: Registry?,
|
|
424
|
-
?base_uri: String
|
|
426
|
+
?base_uri: String?,
|
|
427
|
+
?offline: bool?
|
|
425
428
|
) -> Hash[String, untyped]
|
|
426
429
|
|
|
427
430
|
# Dereference a JSON Schema by recursively replacing all $ref values
|
|
@@ -431,7 +434,8 @@ module JSONSchema
|
|
|
431
434
|
?draft: draft?,
|
|
432
435
|
?retriever: (^(String) -> untyped)?,
|
|
433
436
|
?registry: Registry?,
|
|
434
|
-
?base_uri: String
|
|
437
|
+
?base_uri: String?,
|
|
438
|
+
?offline: bool?
|
|
435
439
|
) -> untyped
|
|
436
440
|
|
|
437
441
|
# Detect the JSON Schema draft for a schema and return the corresponding validator class.
|
|
@@ -454,7 +458,8 @@ module JSONSchema
|
|
|
454
458
|
?registry: Registry?,
|
|
455
459
|
?pattern_options: (RegexOptions | FancyRegexOptions)?,
|
|
456
460
|
?email_options: EmailOptions?,
|
|
457
|
-
?http_options: HttpOptions
|
|
461
|
+
?http_options: HttpOptions?,
|
|
462
|
+
?offline: bool?
|
|
458
463
|
) -> bool
|
|
459
464
|
|
|
460
465
|
# One-off validation raising on error.
|
|
@@ -473,7 +478,8 @@ module JSONSchema
|
|
|
473
478
|
?registry: Registry?,
|
|
474
479
|
?pattern_options: (RegexOptions | FancyRegexOptions)?,
|
|
475
480
|
?email_options: EmailOptions?,
|
|
476
|
-
?http_options: HttpOptions
|
|
481
|
+
?http_options: HttpOptions?,
|
|
482
|
+
?offline: bool?
|
|
477
483
|
) -> nil
|
|
478
484
|
|
|
479
485
|
# Iterate validation errors. Returns Array when no block given, nil when block given.
|
|
@@ -492,7 +498,8 @@ module JSONSchema
|
|
|
492
498
|
?registry: Registry?,
|
|
493
499
|
?pattern_options: (RegexOptions | FancyRegexOptions)?,
|
|
494
500
|
?email_options: EmailOptions?,
|
|
495
|
-
?http_options: HttpOptions
|
|
501
|
+
?http_options: HttpOptions?,
|
|
502
|
+
?offline: bool?
|
|
496
503
|
) -> Array[ValidationError]
|
|
497
504
|
| (
|
|
498
505
|
untyped schema,
|
|
@@ -509,7 +516,8 @@ module JSONSchema
|
|
|
509
516
|
?registry: Registry?,
|
|
510
517
|
?pattern_options: (RegexOptions | FancyRegexOptions)?,
|
|
511
518
|
?email_options: EmailOptions?,
|
|
512
|
-
?http_options: HttpOptions
|
|
519
|
+
?http_options: HttpOptions?,
|
|
520
|
+
?offline: bool?
|
|
513
521
|
) { (ValidationError) -> void } -> nil
|
|
514
522
|
|
|
515
523
|
# One-off evaluation returning structured output.
|
|
@@ -527,7 +535,8 @@ module JSONSchema
|
|
|
527
535
|
?registry: Registry?,
|
|
528
536
|
?pattern_options: (RegexOptions | FancyRegexOptions)?,
|
|
529
537
|
?email_options: EmailOptions?,
|
|
530
|
-
?http_options: HttpOptions
|
|
538
|
+
?http_options: HttpOptions?,
|
|
539
|
+
?offline: bool?
|
|
531
540
|
) -> Evaluation
|
|
532
541
|
|
|
533
542
|
# Reusable JSON Schema validator.
|
data/src/canonical.rs
CHANGED
|
@@ -58,7 +58,7 @@ macro_rules! canonical_error_class {
|
|
|
58
58
|
let exception_class =
|
|
59
59
|
ExceptionClass::from_value(error_class.as_value()).expect("ExceptionClass");
|
|
60
60
|
// The cached handle is invisible to Ruby's GC; registration pins it across compaction.
|
|
61
|
-
|
|
61
|
+
ruby.gc_register_mark_object(exception_class);
|
|
62
62
|
exception_class
|
|
63
63
|
});
|
|
64
64
|
};
|
data/src/lib.rs
CHANGED
|
@@ -25,9 +25,7 @@ use jsonschema::{
|
|
|
25
25
|
};
|
|
26
26
|
use magnus::{
|
|
27
27
|
error::ErrorType,
|
|
28
|
-
function,
|
|
29
|
-
gc::{register_address, register_mark_object, unregister_address},
|
|
30
|
-
method,
|
|
28
|
+
function, method,
|
|
31
29
|
prelude::*,
|
|
32
30
|
rb_sys::AsRawValue,
|
|
33
31
|
scan_args::scan_args,
|
|
@@ -133,7 +131,7 @@ fn build_validator(
|
|
|
133
131
|
///
|
|
134
132
|
/// Persistent `Validator` instances protect their callbacks via the GC mark phase
|
|
135
133
|
/// (see `Validator::mark_callback_roots`). One-off calls have no such wrapper, so
|
|
136
|
-
/// this guard calls `
|
|
134
|
+
/// this guard calls `gc_register_address` on construction and `gc_unregister_address` on
|
|
137
135
|
/// drop to keep callbacks alive while validation runs.
|
|
138
136
|
struct CallbackRootGuard {
|
|
139
137
|
roots: Vec<Value>,
|
|
@@ -154,7 +152,7 @@ impl CallbackRootGuard {
|
|
|
154
152
|
// We do not mutate `roots` after this point, so references used for GC address
|
|
155
153
|
// registration remain valid for the lifetime of the guard.
|
|
156
154
|
for root in &roots {
|
|
157
|
-
|
|
155
|
+
ruby.gc_register_address(root);
|
|
158
156
|
}
|
|
159
157
|
|
|
160
158
|
Self { roots }
|
|
@@ -163,8 +161,9 @@ impl CallbackRootGuard {
|
|
|
163
161
|
|
|
164
162
|
impl Drop for CallbackRootGuard {
|
|
165
163
|
fn drop(&mut self) {
|
|
164
|
+
let ruby = Ruby::get().expect("Ruby VM should be initialized");
|
|
166
165
|
for root in &self.roots {
|
|
167
|
-
|
|
166
|
+
ruby.gc_unregister_address(root);
|
|
168
167
|
}
|
|
169
168
|
}
|
|
170
169
|
}
|
|
@@ -250,7 +249,7 @@ static VALIDATION_ERROR_CLASS: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
|
250
249
|
.expect("JSONSchema::ValidationError must be defined before native extension is used");
|
|
251
250
|
let exc_cls = ExceptionClass::from_value(cls.as_value())
|
|
252
251
|
.expect("JSONSchema::ValidationError must be an exception class");
|
|
253
|
-
|
|
252
|
+
ruby.gc_register_mark_object(exc_cls);
|
|
254
253
|
exc_cls
|
|
255
254
|
});
|
|
256
255
|
|
|
@@ -264,7 +263,7 @@ static REFERENCING_ERROR_CLASS: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
|
264
263
|
.expect("JSONSchema::ReferencingError must be defined before native extension is used");
|
|
265
264
|
let exc_cls = ExceptionClass::from_value(cls.as_value())
|
|
266
265
|
.expect("JSONSchema::ReferencingError must be an exception class");
|
|
267
|
-
|
|
266
|
+
ruby.gc_register_mark_object(exc_cls);
|
|
268
267
|
exc_cls
|
|
269
268
|
});
|
|
270
269
|
|
|
@@ -585,9 +584,9 @@ pub struct Validator {
|
|
|
585
584
|
mask: Option<String>,
|
|
586
585
|
/// Marked during Ruby's GC mark phase to keep runtime callbacks alive.
|
|
587
586
|
callback_roots: CallbackRoots,
|
|
588
|
-
/// Protects callbacks via `
|
|
587
|
+
/// Protects callbacks via `gc_register_address` during schema compilation —
|
|
589
588
|
/// before this wrapper exists and `mark()` can run. Held so that its `Drop`
|
|
590
|
-
/// impl calls `
|
|
589
|
+
/// impl calls `gc_unregister_address` to balance the registrations.
|
|
591
590
|
_compilation_roots: CompilationRootsRef,
|
|
592
591
|
}
|
|
593
592
|
|
data/src/options.rs
CHANGED
|
@@ -6,9 +6,7 @@ use std::{
|
|
|
6
6
|
|
|
7
7
|
use magnus::{
|
|
8
8
|
block::Proc,
|
|
9
|
-
function,
|
|
10
|
-
gc::{register_address, unregister_address},
|
|
11
|
-
method,
|
|
9
|
+
function, method,
|
|
12
10
|
prelude::*,
|
|
13
11
|
scan_args::{get_kwargs, scan_args, KwArgs},
|
|
14
12
|
value::Opaque,
|
|
@@ -19,7 +17,7 @@ use jsonschema::json::{magnus_invalidate_members_cache, Magnus, RbNode};
|
|
|
19
17
|
|
|
20
18
|
use crate::{
|
|
21
19
|
registry::Registry,
|
|
22
|
-
retriever::{make_retriever, RubyRetriever},
|
|
20
|
+
retriever::{make_retriever, DetachedRetriever, RubyRetriever},
|
|
23
21
|
ser::{map_to_ruby, value_to_ruby},
|
|
24
22
|
static_id::{define_rb_intern, StaticId},
|
|
25
23
|
CUSTOM_KEYWORD_CAUSE, LAST_CALLBACK_ERROR,
|
|
@@ -85,10 +83,10 @@ pub struct ParsedOptions<'i> {
|
|
|
85
83
|
/// mechanism for preventing collection of objects referenced by native extensions.
|
|
86
84
|
/// For one-off validation functions (module-level `valid?`, `validate!`, etc.),
|
|
87
85
|
/// where no persistent `Validator` exists, a [`CallbackRootGuard`](crate::CallbackRootGuard)
|
|
88
|
-
/// temporarily registers the same roots via `
|
|
86
|
+
/// temporarily registers the same roots via `gc_register_address` for the duration
|
|
89
87
|
/// of the call.
|
|
90
88
|
///
|
|
91
|
-
/// * **`CompilationRoots`** — registered via `
|
|
89
|
+
/// * **`CompilationRoots`** — registered via `gc_register_address` immediately when
|
|
92
90
|
/// added, so callbacks are protected during schema compilation (before the
|
|
93
91
|
/// `Validator` wrapper and its mark function exist). Unregistered on drop.
|
|
94
92
|
///
|
|
@@ -104,10 +102,10 @@ pub struct CompilationRoots {
|
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
impl CompilationRoots {
|
|
107
|
-
fn add(&self, value: Opaque<Value>) -> Result<(), ()> {
|
|
105
|
+
fn add(&self, ruby: &Ruby, value: Opaque<Value>) -> Result<(), ()> {
|
|
108
106
|
let mut roots = self.roots.lock().map_err(|_| ())?;
|
|
109
107
|
let pinned = Box::pin(value);
|
|
110
|
-
|
|
108
|
+
ruby.gc_register_address(pinned.as_ref().get_ref());
|
|
111
109
|
roots.push(pinned);
|
|
112
110
|
Ok(())
|
|
113
111
|
}
|
|
@@ -119,8 +117,9 @@ impl Drop for CompilationRoots {
|
|
|
119
117
|
Ok(roots) => roots,
|
|
120
118
|
Err(poisoned) => poisoned.into_inner(),
|
|
121
119
|
};
|
|
120
|
+
let ruby = Ruby::get().expect("Ruby VM should be initialized");
|
|
122
121
|
for root in roots.drain(..) {
|
|
123
|
-
|
|
122
|
+
ruby.gc_unregister_address(root.as_ref().get_ref());
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
125
|
}
|
|
@@ -523,7 +522,7 @@ pub fn make_options_from_kwargs(
|
|
|
523
522
|
"`offline` cannot be used together with `retriever`",
|
|
524
523
|
));
|
|
525
524
|
}
|
|
526
|
-
let mut opts = jsonschema::options_for::<Magnus>();
|
|
525
|
+
let mut opts = jsonschema::options_for::<Magnus>().with_retriever(DetachedRetriever(None));
|
|
527
526
|
let mut registry = None;
|
|
528
527
|
let mut retriever = None;
|
|
529
528
|
let retriever_was_provided = retriever_val.is_some();
|
|
@@ -553,12 +552,14 @@ pub fn make_options_from_kwargs(
|
|
|
553
552
|
|
|
554
553
|
if let Some(val) = retriever_val {
|
|
555
554
|
if let Some(ret) = make_retriever(ruby, val)? {
|
|
556
|
-
compilation_roots
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
555
|
+
compilation_roots
|
|
556
|
+
.add(ruby, Opaque::from(val))
|
|
557
|
+
.map_err(|()| {
|
|
558
|
+
Error::new(
|
|
559
|
+
ruby.exception_runtime_error(),
|
|
560
|
+
"Compilation callback root storage is poisoned",
|
|
561
|
+
)
|
|
562
|
+
})?;
|
|
562
563
|
{
|
|
563
564
|
let mut roots = callback_roots.lock().map_err(|_| {
|
|
564
565
|
Error::new(
|
|
@@ -586,7 +587,7 @@ pub fn make_options_from_kwargs(
|
|
|
586
587
|
if let Some(registry_retriever_value) = reg.retriever_value(ruby) {
|
|
587
588
|
if let Some(ret) = make_retriever(ruby, registry_retriever_value)? {
|
|
588
589
|
compilation_roots
|
|
589
|
-
.add(Opaque::from(registry_retriever_value))
|
|
590
|
+
.add(ruby, Opaque::from(registry_retriever_value))
|
|
590
591
|
.map_err(|()| {
|
|
591
592
|
Error::new(
|
|
592
593
|
ruby.exception_runtime_error(),
|
|
@@ -633,7 +634,7 @@ pub fn make_options_from_kwargs(
|
|
|
633
634
|
})?;
|
|
634
635
|
|
|
635
636
|
compilation_roots
|
|
636
|
-
.add(Opaque::from(callback))
|
|
637
|
+
.add(ruby, Opaque::from(callback))
|
|
637
638
|
.map_err(|()| {
|
|
638
639
|
Error::new(
|
|
639
640
|
ruby.exception_runtime_error(),
|
|
@@ -692,7 +693,7 @@ pub fn make_options_from_kwargs(
|
|
|
692
693
|
class: Opaque::from(callback),
|
|
693
694
|
});
|
|
694
695
|
compilation_roots
|
|
695
|
-
.add(Opaque::from(callback))
|
|
696
|
+
.add(ruby, Opaque::from(callback))
|
|
696
697
|
.map_err(|()| {
|
|
697
698
|
Error::new(
|
|
698
699
|
ruby.exception_runtime_error(),
|
|
@@ -755,7 +756,7 @@ pub fn make_options_from_kwargs(
|
|
|
755
756
|
Ok(inst) => {
|
|
756
757
|
let opaque_inst = Opaque::from(inst);
|
|
757
758
|
compilation_roots_for_keyword
|
|
758
|
-
.add(opaque_inst)
|
|
759
|
+
.add(&inner_ruby, opaque_inst)
|
|
759
760
|
.map_err(|()| {
|
|
760
761
|
jsonschema::ValidationError::custom(
|
|
761
762
|
"Compilation callback root storage is poisoned",
|
|
@@ -839,9 +840,9 @@ pub fn make_options_from_kwargs(
|
|
|
839
840
|
if let Some(ref ca_cert) = hopts.ca_cert {
|
|
840
841
|
http_opts = http_opts.add_root_certificate(ca_cert);
|
|
841
842
|
}
|
|
842
|
-
|
|
843
|
-
.with_http_options(&http_opts)
|
|
843
|
+
let retriever = jsonschema::HttpRetriever::new(&http_opts)
|
|
844
844
|
.map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
|
|
845
|
+
opts = opts.with_retriever(DetachedRetriever(Some(retriever)));
|
|
845
846
|
}
|
|
846
847
|
|
|
847
848
|
if offline {
|
data/src/registry.rs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
use std::sync::Arc;
|
|
2
2
|
|
|
3
3
|
use magnus::{
|
|
4
|
-
function,
|
|
5
|
-
gc::{register_address, unregister_address},
|
|
6
|
-
method,
|
|
4
|
+
function, method,
|
|
7
5
|
prelude::*,
|
|
8
6
|
scan_args::{get_kwargs, scan_args},
|
|
9
7
|
value::Opaque,
|
|
@@ -23,13 +21,13 @@ struct RetrieverBuildRootGuard {
|
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
impl RetrieverBuildRootGuard {
|
|
26
|
-
fn new(root: Option<Value>) -> Self {
|
|
24
|
+
fn new(ruby: &Ruby, root: Option<Value>) -> Self {
|
|
27
25
|
let mut roots = Vec::new();
|
|
28
26
|
if let Some(value) = root {
|
|
29
27
|
roots.push(value);
|
|
30
28
|
}
|
|
31
29
|
for value in &roots {
|
|
32
|
-
|
|
30
|
+
ruby.gc_register_address(value);
|
|
33
31
|
}
|
|
34
32
|
Self { roots }
|
|
35
33
|
}
|
|
@@ -37,8 +35,9 @@ impl RetrieverBuildRootGuard {
|
|
|
37
35
|
|
|
38
36
|
impl Drop for RetrieverBuildRootGuard {
|
|
39
37
|
fn drop(&mut self) {
|
|
38
|
+
let ruby = Ruby::get().expect("Ruby VM should be initialized");
|
|
40
39
|
for value in &self.roots {
|
|
41
|
-
|
|
40
|
+
ruby.gc_unregister_address(value);
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
}
|
|
@@ -148,7 +147,7 @@ impl Registry {
|
|
|
148
147
|
|
|
149
148
|
// Keep the retriever proc GC-rooted for the entire build, because `build`
|
|
150
149
|
// may call into retriever callbacks while traversing referenced resources.
|
|
151
|
-
let _retriever_build_guard = RetrieverBuildRootGuard::new(retriever_build_root);
|
|
150
|
+
let _retriever_build_guard = RetrieverBuildRootGuard::new(ruby, retriever_build_root);
|
|
152
151
|
for item in resources {
|
|
153
152
|
let pair: RArray = TryConvert::try_convert(item)?;
|
|
154
153
|
if pair.len() != 2 {
|
data/src/retriever.rs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
use jsonschema::{Retrieve, Uri};
|
|
3
3
|
use magnus::{block::Proc, prelude::*, value::Opaque, Error, Ruby, Value};
|
|
4
4
|
use serde_json::Value as JsonValue;
|
|
5
|
+
use std::{ffi::c_void, panic::AssertUnwindSafe};
|
|
5
6
|
|
|
6
7
|
use crate::ser::to_value;
|
|
7
8
|
|
|
@@ -98,3 +99,52 @@ pub fn make_retriever(ruby: &Ruby, value: Value) -> Result<Option<RubyRetriever>
|
|
|
98
99
|
|
|
99
100
|
Ok(Some(RubyRetriever::new(proc)))
|
|
100
101
|
}
|
|
102
|
+
|
|
103
|
+
/// The built-in HTTP and file retriever, run with the GVL released so other Ruby threads keep running while it waits.
|
|
104
|
+
pub struct DetachedRetriever(pub Option<jsonschema::HttpRetriever>);
|
|
105
|
+
|
|
106
|
+
type RetrieveResult = Result<JsonValue, Box<dyn std::error::Error + Send + Sync>>;
|
|
107
|
+
|
|
108
|
+
#[allow(unsafe_code)]
|
|
109
|
+
impl Retrieve for DetachedRetriever {
|
|
110
|
+
fn retrieve(&self, uri: &Uri<String>) -> RetrieveResult {
|
|
111
|
+
struct Call<'a> {
|
|
112
|
+
retriever: &'a DetachedRetriever,
|
|
113
|
+
uri: &'a Uri<String>,
|
|
114
|
+
result: Option<std::thread::Result<RetrieveResult>>,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
unsafe extern "C" fn call(data: *mut c_void) -> *mut c_void {
|
|
118
|
+
// SAFETY: `data` is the `Call` below, alive until `rb_thread_call_without_gvl` returns
|
|
119
|
+
let call = unsafe { &mut *data.cast::<Call<'_>>() };
|
|
120
|
+
call.result = Some(std::panic::catch_unwind(AssertUnwindSafe(|| {
|
|
121
|
+
match &call.retriever.0 {
|
|
122
|
+
Some(retriever) => retriever.retrieve(call.uri),
|
|
123
|
+
// A client per retrieval, as the core default retriever does; building one up front would cost every validator build
|
|
124
|
+
None => jsonschema::HttpRetriever::new(&jsonschema::HttpOptions::new())?
|
|
125
|
+
.retrieve(call.uri),
|
|
126
|
+
}
|
|
127
|
+
})));
|
|
128
|
+
std::ptr::null_mut()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let mut data = Call {
|
|
132
|
+
retriever: self,
|
|
133
|
+
uri,
|
|
134
|
+
result: None,
|
|
135
|
+
};
|
|
136
|
+
// SAFETY: `call` touches no Ruby objects, so it may run without the GVL
|
|
137
|
+
unsafe {
|
|
138
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
139
|
+
Some(call),
|
|
140
|
+
(&raw mut data).cast(),
|
|
141
|
+
None,
|
|
142
|
+
std::ptr::null_mut(),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
match data.result.expect("Retrieval runs to completion") {
|
|
146
|
+
Ok(result) => result,
|
|
147
|
+
Err(panic) => std::panic::resume_unwind(panic),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
data/src/ser.rs
CHANGED
|
@@ -3,7 +3,6 @@ use jsonschema::canonical::json::{canonical_number, BorrowedNumber};
|
|
|
3
3
|
use magnus::{
|
|
4
4
|
error::ErrorType,
|
|
5
5
|
exception::ExceptionClass,
|
|
6
|
-
gc::register_mark_object,
|
|
7
6
|
prelude::*,
|
|
8
7
|
rb_sys::AsRawValue,
|
|
9
8
|
value::{Lazy, ReprValue},
|
|
@@ -28,7 +27,7 @@ static BIG_DECIMAL_CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
|
28
27
|
let cls: RClass = ruby
|
|
29
28
|
.eval("BigDecimal")
|
|
30
29
|
.expect("BigDecimal class must exist");
|
|
31
|
-
|
|
30
|
+
ruby.gc_register_mark_object(cls);
|
|
32
31
|
cls
|
|
33
32
|
});
|
|
34
33
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jsonschema_rs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.58.
|
|
4
|
+
version: 0.58.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dygalo
|
|
@@ -80,7 +80,7 @@ metadata:
|
|
|
80
80
|
homepage_uri: https://github.com/Stranger6667/jsonschema/tree/master/crates/jsonschema-rb
|
|
81
81
|
source_code_uri: https://github.com/Stranger6667/jsonschema
|
|
82
82
|
changelog_uri: https://github.com/Stranger6667/jsonschema/blob/master/crates/jsonschema-rb/CHANGELOG.md
|
|
83
|
-
documentation_uri: https://github.com/Stranger6667/jsonschema/
|
|
83
|
+
documentation_uri: https://github.com/Stranger6667/jsonschema/blob/ruby-v0.58.5/crates/jsonschema-rb/README.md
|
|
84
84
|
bug_tracker_uri: https://github.com/Stranger6667/jsonschema/issues
|
|
85
85
|
funding_uri: https://github.com/sponsors/Stranger6667
|
|
86
86
|
rubygems_mfa_required: 'true'
|