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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eebce6f817d3f7c3f2735c03d72c8e70ae14399ec0379cf97e08c4af53f82ac6
4
- data.tar.gz: 11532b9e972888378db6428b9f42427aa338da51a456b6e2a4df9320a32a7008
3
+ metadata.gz: c602494a0fa886d7e8ea3b14a5a707014b7723234371424cdb2315aa0a1d451f
4
+ data.tar.gz: 1634e82743fe6cd6c4634809a38dccc8bfbbf488e8e55a4038359ad8b0630399
5
5
  SHA512:
6
- metadata.gz: b71d1b975f2621fa079548c032dfb6277046d01595a3f17ffe5a2b5d29f42d85f1216db919a5183e2b148f6364210b41792d0cf4802c50740e3d1cbfad420cfd
7
- data.tar.gz: 03fa98bcd49da60b01087762c3a3b58831258a29781e098c24abf85e8a9b2945bfe663a2c9b36f2263ebc1b964e51a801fe5e93dec23bedb3e4d3d8e86eda47c
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.4...HEAD
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.4"
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.4", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
18
- magnus = { version = "0.8", features = ["rb-sys"] }
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
- - 📚 Full support for popular JSON Schema drafts
49
- - 🌐 Remote reference fetching (network/file)
48
+ - 📚 Drafts 4, 6, 7, 2019-09 and 2020-12
50
49
  - 🔧 Custom keywords and format validators
51
- - ✨ Meta-schema validation for schema documents
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
  - [![Draft 2020-12](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2020-12.json)](https://bowtie.report/#/implementations/rust-jsonschema)
61
61
  - [![Draft 2019-09](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2019-09.json)](https://bowtie.report/#/implementations/rust-jsonschema)
62
62
  - [![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft7.json)](https://bowtie.report/#/implementations/rust-jsonschema)
63
63
  - [![Draft 6](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft6.json)](https://bowtie.report/#/implementations/rust-jsonschema)
64
64
  - [![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft4.json)](https://bowtie.report/#/implementations/rust-jsonschema)
65
65
 
66
- You can check the current status on the [Bowtie Report](https://bowtie.report/#/implementations/rust-jsonschema).
66
+ Per-draft compliance results are on the [Bowtie Report](https://bowtie.report/#/implementations/rust-jsonschema).
67
67
 
68
68
  ## Playground
69
69
 
70
- If you'd like to try `jsonschema`, you can check the WebAssembly-powered [playground](https://jsonschema.dygalo.dev/) to see the results instantly.
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 are available for:
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
- If no pre-built gem is available for your platform, it will be compiled from source during installation. You'll need:
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
- For validating multiple instances against the same schema, create a reusable validator.
95
- `validator_for` automatically detects the draft version from the `$schema` keyword in the schema and falls back to Draft 2020-12:
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
- You can use draft-specific validators for different JSON Schema versions:
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)` - called during schema compilation
154
- - `validate(instance)` - raise on failure, return normally on success
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
- A class may also implement `iter_errors(instance)`, returning an array of exceptions, to report several problems from one keyword. `each_error` then surfaces each of them; when the method is absent it falls back to the single error from `validate`:
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
- When `validate` raises, the original exception is preserved as the `cause` of the `ValidationError`, so callers can inspect it (errors from `iter_errors` keep their exception as `cause` in the same way):
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
- When you need more than a boolean result, use the `evaluate` API to access the [JSON Schema Output v1](https://json-schema.org/draft/2020-12/json-schema-core#name-output-formatting) formats:
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** — simplest, just valid/invalid:
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** — flat list of all evaluation nodes:
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** — nested tree following schema structure:
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** — flat list of all errors across evaluation nodes:
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** — flat list of annotations from successfully validated nodes.
270
- When a node fails validation, its annotations appear as `droppedAnnotations` in the list/hierarchical output instead.
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
- Use `Canonical::JSON.to_string` when you need a stable JSON representation:
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
- Main use case: deduplicating equivalent JSON Schemas.
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 without changing the values it accepts. Schemas that accept the same values reduce to equal `CanonicalSchema` objects, and contradictions reduce to `false`:
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 schema *is* the set of values it accepts, so canonical schemas combine as sets. Every emitted schema carries `$schema`, left out of the comments below:
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, across the types it never admitted
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 - `bounded` takes negative integers, `positive` does not
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
- The difference answers what editing a schema did to the values it takes: it accepts exactly what the old schema took and the new one turns away, and is unsatisfiable exactly when nothing was lost.
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
- Compare a request schema new-against-old and a response schema old-against-new: narrowing a request turns away payloads a caller used to send, widening a response returns values a caller never agreed to read.
368
+ The direction to check depends on who sends the value:
365
369
 
366
- `:no` on the difference proves nothing was lost. `:unknown` proves nothing either way.
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
- `:unknown` is undecided, not negative, so the two questions have opposite safe tests: only `satisfiability == :no` proves a schema admits nothing, and only `covers == :yes` proves containment.
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
- Both operands must share one setup - the same draft, format policy, regular-expression engine and definitions - or `IncompatibleOperands` is raised. `UnsupportedOperand` means an operand is a `Raw` pass-through, and `UnsupportedResult` that the canonical form does not support the result. All three live under `JSONSchema::Canonical`.
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 answers which of its subschemas admit no value, and why - the keywords at fault and where they sit:
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 left out is not proven satisfiable: a document canonicalization cannot model reports nothing, as `satisfiability` answers `:unknown`.
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 result validates identically to the original.
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
- Validate that a JSON Schema document is itself valid:
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, `jsonschema` resolves HTTP references and file references from the local file system. You can implement a custom retriever to handle external references:
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
- For applications that frequently use the same schemas, create a registry to store and reference them:
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
- The registry also accepts `draft:` and `retriever:` options:
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
- When validating schemas with regex patterns (in `pattern` or `patternProperties`), you can configure the underlying regex engine:
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
- The available options:
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`: Safer engine with linear-time guarantee
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
- This configuration is crucial when working with untrusted schemas where attackers might craft malicious regex patterns.
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
- When validating email addresses using `{"format": "email"}`, you can customize the validation behavior:
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
- Available options:
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
- `jsonschema` provides detailed validation errors through the `ValidationError` class:
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
- Each error has a `kind` property with convenient accessors:
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
- When working with sensitive data, you can mask instance values in error messages:
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`** - raised on validation failure
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`** - raised when `$ref` cannot be resolved
694
+ - **`JSONSchema::ReferencingError`**: raised when a `$ref` cannot be resolved
664
695
 
665
696
  ## Options Reference
666
697
 
667
- One-off validation methods (`valid?`, `validate!`, `each_error`, `evaluate`) accept these keyword arguments:
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, # Enable format validation (default: false)
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 # HttpOptions
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:` — use draft-specific validators (`Draft7Validator.new`, etc.) to pin a draft version.
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
- `jsonschema` is designed for high performance, outperforming other Ruby JSON Schema validators in most scenarios:
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
- For detailed benchmarks, see our [full performance comparison](https://github.com/Stranger6667/jsonschema/blob/master/crates/jsonschema-rb/BENCHMARKS.md).
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
- This library draws API design inspiration from the Python [`jsonschema`](https://github.com/python-jsonschema/jsonschema) package. We're grateful to the Python `jsonschema` maintainers and contributors for their pioneering work in JSON Schema validation.
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
- If you have questions, need help, or want to suggest improvements, please use [GitHub Discussions](https://github.com/Stranger6667/jsonschema/discussions).
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 `jsonschema` useful, please consider [sponsoring its development](https://github.com/sponsors/Stranger6667).
757
+ If you find `jsonschema_rs` useful, please consider [sponsoring its development](https://github.com/sponsors/Stranger6667).
726
758
 
727
759
  ## Contributing
728
760
 
@@ -693,9 +693,9 @@ dependencies = [
693
693
 
694
694
  [[package]]
695
695
  name = "jsonschema"
696
- version = "0.58.4"
696
+ version = "0.58.5"
697
697
  source = "registry+https://github.com/rust-lang/crates.io-index"
698
- checksum = "5761b0ca0a31464030d7c2a98087fde3124ebb5e09b5a112cdb815f03b8ee150"
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.4"
730
+ version = "0.58.5"
731
731
  source = "registry+https://github.com/rust-lang/crates.io-index"
732
- checksum = "75e6d7c2c9afe6aa7de831af841801c954b83b1742f0e228b505326456a70b8f"
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.4"
740
+ version = "0.58.5"
741
741
  source = "registry+https://github.com/rust-lang/crates.io-index"
742
- checksum = "65bcdd5a5c9e2c6ddefdddef36f1a27f5183649bd135f633e1bc6bfa97982407"
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.4"
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.4"
773
+ version = "0.58.5"
774
774
  source = "registry+https://github.com/rust-lang/crates.io-index"
775
- checksum = "f1ffba4378a3d6ee7f1ab65a8ed95c711ccfe4a3870932bdce89d8260abed38a"
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.4"
782
+ version = "0.58.5"
783
783
  source = "registry+https://github.com/rust-lang/crates.io-index"
784
- checksum = "386121f898b560ff6aaa7974952720e9d9276a201eca31296387c2ed8707855e"
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.4"
1162
+ version = "0.58.5"
1140
1163
  source = "registry+https://github.com/rust-lang/crates.io-index"
1141
- checksum = "5cfa1c91745dbca76a406b926918d7c63e15a4525ebab939fec3e015a1fb82b1"
1164
+ checksum = "590efadb0a669f1712c1e3ab810a55d4c3d20d0127c455f7236dd0710aba0346"
1142
1165
  dependencies = [
1143
1166
  "ahash",
1144
1167
  "fluent-uri",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb-ext"
3
- version = "0.58.4"
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.4", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
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.4"
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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONSchema
4
- VERSION = "0.58.4"
4
+ VERSION = "0.58.5"
5
5
  end
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
- magnus::gc::register_mark_object(exception_class);
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 `register_address` on construction and `unregister_address` on
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
- register_address(root);
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
- unregister_address(root);
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
- register_mark_object(exc_cls);
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
- register_mark_object(exc_cls);
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 `register_address` during schema compilation —
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 `unregister_address` to balance the registrations.
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 `register_address` for the duration
86
+ /// temporarily registers the same roots via `gc_register_address` for the duration
89
87
  /// of the call.
90
88
  ///
91
- /// * **`CompilationRoots`** — registered via `register_address` immediately when
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
- register_address(pinned.as_ref().get_ref());
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
- unregister_address(root.as_ref().get_ref());
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.add(Opaque::from(val)).map_err(|()| {
557
- Error::new(
558
- ruby.exception_runtime_error(),
559
- "Compilation callback root storage is poisoned",
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
- opts = opts
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
- register_address(value);
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
- unregister_address(value);
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
- register_mark_object(cls);
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
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/tree/master/crates/jsonschema-rb#readme
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'