jsonschema_rs 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/src/lib.rs ADDED
@@ -0,0 +1,1349 @@
1
+ //! Ruby bindings for the jsonschema crate.
2
+ #![allow(unreachable_pub)]
3
+ #![allow(clippy::trivially_copy_pass_by_ref)]
4
+ #![allow(clippy::doc_markdown)]
5
+ #![allow(clippy::unused_self)]
6
+ #![allow(clippy::struct_field_names)]
7
+
8
+ mod error_kind;
9
+ mod evaluation;
10
+ mod options;
11
+ mod registry;
12
+ mod retriever;
13
+ mod ser;
14
+ mod static_id;
15
+
16
+ use jsonschema::{paths::LocationSegment, ValidationOptions};
17
+ use magnus::{
18
+ function,
19
+ gc::{register_address, register_mark_object, unregister_address},
20
+ method,
21
+ prelude::*,
22
+ scan_args::scan_args,
23
+ value::{Lazy, ReprValue},
24
+ DataTypeFunctions, Error, Exception, ExceptionClass, RClass, RModule, RObject, Ruby, Value,
25
+ };
26
+ use referencing::unescape_segment;
27
+ use std::{
28
+ cell::RefCell,
29
+ panic::{self, AssertUnwindSafe},
30
+ sync::Arc,
31
+ };
32
+
33
+ use crate::{
34
+ error_kind::ValidationErrorKind,
35
+ evaluation::Evaluation,
36
+ options::{
37
+ extract_evaluate_kwargs, extract_kwargs, extract_kwargs_no_draft, make_options_from_kwargs,
38
+ parse_draft_symbol, CallbackRoots, CompilationRoots, CompilationRootsRef, ExtractedKwargs,
39
+ ParsedOptions,
40
+ },
41
+ registry::Registry,
42
+ retriever::{retriever_error_message, RubyRetriever},
43
+ ser::{to_schema_value, to_value},
44
+ static_id::define_rb_intern,
45
+ };
46
+
47
+ // Report Rust heap usage to Ruby GC so it can account for native memory pressure.
48
+ rb_sys::set_global_tracking_allocator!();
49
+
50
+ define_rb_intern!(static ID_ALLOCATE: "allocate");
51
+ define_rb_intern!(static ID_AT_MESSAGE: "@message");
52
+ define_rb_intern!(static ID_AT_VERBOSE_MESSAGE: "@verbose_message");
53
+ define_rb_intern!(static ID_AT_INSTANCE_PATH: "@instance_path");
54
+ define_rb_intern!(static ID_AT_SCHEMA_PATH: "@schema_path");
55
+ define_rb_intern!(static ID_AT_EVALUATION_PATH: "@evaluation_path");
56
+ define_rb_intern!(static ID_AT_INSTANCE_PATH_POINTER: "@instance_path_pointer");
57
+ define_rb_intern!(static ID_AT_SCHEMA_PATH_POINTER: "@schema_path_pointer");
58
+ define_rb_intern!(static ID_AT_EVALUATION_PATH_POINTER: "@evaluation_path_pointer");
59
+ define_rb_intern!(static ID_AT_KIND: "@kind");
60
+ define_rb_intern!(static ID_AT_INSTANCE: "@instance");
61
+
62
+ define_rb_intern!(static ID_SYM_MESSAGE: "message");
63
+ define_rb_intern!(static ID_SYM_VERBOSE_MESSAGE: "verbose_message");
64
+ define_rb_intern!(static ID_SYM_INSTANCE_PATH: "instance_path");
65
+ define_rb_intern!(static ID_SYM_SCHEMA_PATH: "schema_path");
66
+ define_rb_intern!(static ID_SYM_EVALUATION_PATH: "evaluation_path");
67
+ define_rb_intern!(static ID_SYM_KIND: "kind");
68
+ define_rb_intern!(static ID_SYM_INSTANCE: "instance");
69
+ define_rb_intern!(static ID_SYM_INSTANCE_PATH_POINTER: "instance_path_pointer");
70
+ define_rb_intern!(static ID_SYM_SCHEMA_PATH_POINTER: "schema_path_pointer");
71
+ define_rb_intern!(static ID_SYM_EVALUATION_PATH_POINTER: "evaluation_path_pointer");
72
+
73
+ struct BuiltValidator {
74
+ validator: jsonschema::Validator,
75
+ callback_roots: CallbackRoots,
76
+ compilation_roots: CompilationRootsRef,
77
+ }
78
+
79
+ fn build_validator(
80
+ ruby: &Ruby,
81
+ options: ValidationOptions,
82
+ retriever: Option<RubyRetriever>,
83
+ callback_roots: CallbackRoots,
84
+ compilation_roots: Arc<CompilationRoots>,
85
+ schema: &serde_json::Value,
86
+ ) -> Result<BuiltValidator, Error> {
87
+ let validator = match retriever {
88
+ Some(ret) => options.with_retriever(ret).build(schema),
89
+ None => options.build(schema),
90
+ }
91
+ .map_err(|error| {
92
+ if let jsonschema::error::ValidationErrorKind::Referencing(err) = error.kind() {
93
+ if let Some(message) = retriever_error_message(err) {
94
+ Error::new(ruby.exception_arg_error(), message)
95
+ } else {
96
+ referencing_error(ruby, err.to_string())
97
+ }
98
+ } else {
99
+ Error::new(ruby.exception_arg_error(), error.to_string())
100
+ }
101
+ })?;
102
+
103
+ Ok(BuiltValidator {
104
+ validator,
105
+ callback_roots,
106
+ compilation_roots,
107
+ })
108
+ }
109
+
110
+ /// RAII guard that registers Ruby callback values as GC roots for the duration
111
+ /// of a one-off validation call (module-level `valid?`, `validate!`, etc.).
112
+ ///
113
+ /// Persistent `Validator` instances protect their callbacks via the GC mark phase
114
+ /// (see `Validator::mark_callback_roots`). One-off calls have no such wrapper, so
115
+ /// this guard calls `register_address` on construction and `unregister_address` on
116
+ /// drop to keep callbacks alive while validation runs.
117
+ struct CallbackRootGuard {
118
+ roots: Vec<Value>,
119
+ }
120
+
121
+ impl CallbackRootGuard {
122
+ fn new(ruby: &Ruby, callback_roots: &CallbackRoots) -> Self {
123
+ let roots = {
124
+ let roots_guard = match callback_roots.lock() {
125
+ Ok(roots) => roots,
126
+ Err(poisoned) => poisoned.into_inner(),
127
+ };
128
+ roots_guard
129
+ .iter()
130
+ .map(|root| ruby.get_inner(*root))
131
+ .collect::<Vec<_>>()
132
+ };
133
+ // We do not mutate `roots` after this point, so references used for GC address
134
+ // registration remain valid for the lifetime of the guard.
135
+ for root in &roots {
136
+ register_address(root);
137
+ }
138
+
139
+ Self { roots }
140
+ }
141
+ }
142
+
143
+ impl Drop for CallbackRootGuard {
144
+ fn drop(&mut self) {
145
+ for root in &self.roots {
146
+ unregister_address(root);
147
+ }
148
+ }
149
+ }
150
+
151
+ fn build_parsed_options(
152
+ ruby: &Ruby,
153
+ kw: ExtractedKwargs,
154
+ draft_override: Option<jsonschema::Draft>,
155
+ ) -> Result<ParsedOptions, Error> {
156
+ let (
157
+ draft_val,
158
+ validate_formats,
159
+ ignore_unknown_formats,
160
+ mask,
161
+ base_uri,
162
+ retriever,
163
+ formats,
164
+ keywords,
165
+ registry,
166
+ ) = kw.base;
167
+ let parsed_draft = match draft_val {
168
+ Some(val) => Some(parse_draft_symbol(ruby, val)?),
169
+ None => None,
170
+ };
171
+ make_options_from_kwargs(
172
+ ruby,
173
+ draft_override.or(parsed_draft),
174
+ validate_formats,
175
+ ignore_unknown_formats,
176
+ mask,
177
+ base_uri,
178
+ retriever,
179
+ formats,
180
+ keywords,
181
+ registry,
182
+ kw.pattern_options,
183
+ kw.email_options,
184
+ kw.http_options,
185
+ )
186
+ }
187
+
188
+ thread_local! {
189
+ static LAST_CALLBACK_ERROR: RefCell<Option<Error>> = const { RefCell::new(None) };
190
+ /// When `true`, the custom panic hook suppresses output (inside `catch_unwind` blocks).
191
+ static SUPPRESS_PANIC_OUTPUT: RefCell<bool> = const { RefCell::new(false) };
192
+ }
193
+
194
+ static VALIDATION_ERROR_CLASS: Lazy<ExceptionClass> = Lazy::new(|ruby| {
195
+ let module: RModule = ruby
196
+ .class_object()
197
+ .const_get("JSONSchema")
198
+ .expect("JSONSchema module must be defined before native extension is used");
199
+ let cls: RClass = module
200
+ .const_get("ValidationError")
201
+ .expect("JSONSchema::ValidationError must be defined before native extension is used");
202
+ let exc_cls = ExceptionClass::from_value(cls.as_value())
203
+ .expect("JSONSchema::ValidationError must be an exception class");
204
+ register_mark_object(exc_cls);
205
+ exc_cls
206
+ });
207
+
208
+ static REFERENCING_ERROR_CLASS: Lazy<ExceptionClass> = Lazy::new(|ruby| {
209
+ let module: RModule = ruby
210
+ .class_object()
211
+ .const_get("JSONSchema")
212
+ .expect("JSONSchema module must be defined before native extension is used");
213
+ let cls: RClass = module
214
+ .const_get("ReferencingError")
215
+ .expect("JSONSchema::ReferencingError must be defined before native extension is used");
216
+ let exc_cls = ExceptionClass::from_value(cls.as_value())
217
+ .expect("JSONSchema::ReferencingError must be an exception class");
218
+ register_mark_object(exc_cls);
219
+ exc_cls
220
+ });
221
+
222
+ struct StringWriter<'a>(&'a mut String);
223
+
224
+ #[allow(unsafe_code)]
225
+ impl std::io::Write for StringWriter<'_> {
226
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
227
+ // SAFETY: `serde_json` always produces valid UTF-8
228
+ self.0
229
+ .push_str(unsafe { std::str::from_utf8_unchecked(buf) });
230
+ Ok(buf.len())
231
+ }
232
+
233
+ fn flush(&mut self) -> std::io::Result<()> {
234
+ Ok(())
235
+ }
236
+ }
237
+
238
+ /// Build a verbose error message with schema path, instance path, and instance value.
239
+ fn build_verbose_message(
240
+ mut message: String,
241
+ schema_path: &jsonschema::paths::Location,
242
+ instance_path: &jsonschema::paths::Location,
243
+ root_instance: Option<&serde_json::Value>,
244
+ failing_instance: &serde_json::Value,
245
+ mask: Option<&str>,
246
+ ) -> String {
247
+ let schema_path_str = schema_path.as_str();
248
+ let instance_path_str = instance_path.as_str();
249
+
250
+ let estimated_addition =
251
+ 150 + schema_path_str.len() + instance_path_str.len() + mask.map_or(100, str::len); // Mask length or ~100 for JSON serialization
252
+
253
+ message.reserve(estimated_addition);
254
+ message.push_str("\n\nFailed validating");
255
+
256
+ let is_index_segment =
257
+ |segment: &str| segment.bytes().all(|b| b.is_ascii_digit()) && !segment.is_empty();
258
+ let is_schema_property_map = |segment: Option<&str>| {
259
+ matches!(
260
+ segment,
261
+ Some(
262
+ "properties"
263
+ | "patternProperties"
264
+ | "dependentSchemas"
265
+ | "$defs"
266
+ | "definitions"
267
+ | "dependencies",
268
+ )
269
+ )
270
+ };
271
+ let push_segment = |m: &mut String, segment: &str, is_index: bool| {
272
+ if is_index {
273
+ m.push_str(segment);
274
+ } else {
275
+ m.push('"');
276
+ m.push_str(segment);
277
+ m.push('"');
278
+ }
279
+ };
280
+
281
+ let mut schema_segments = Vec::new();
282
+ let mut previous_schema_segment: Option<String> = None;
283
+ for segment in schema_path_str.split('/').skip(1) {
284
+ let segment = unescape_segment(segment);
285
+ let segment = segment.as_ref();
286
+ let is_index = is_index_segment(segment)
287
+ && !is_schema_property_map(previous_schema_segment.as_deref());
288
+ schema_segments.push((segment.to_owned(), is_index));
289
+ previous_schema_segment = Some(segment.to_owned());
290
+ }
291
+
292
+ if let Some((last, rest)) = schema_segments.split_last() {
293
+ message.push(' ');
294
+ push_segment(&mut message, &last.0, last.1);
295
+ message.push_str(" in schema");
296
+ for (segment, is_index) in rest {
297
+ message.push('[');
298
+ push_segment(&mut message, segment, *is_index);
299
+ message.push(']');
300
+ }
301
+ } else {
302
+ message.push_str(" in schema");
303
+ }
304
+
305
+ message.push_str("\n\nOn instance");
306
+ let mut current = root_instance;
307
+ for segment in instance_path_str.split('/').skip(1) {
308
+ let segment = unescape_segment(segment);
309
+ let segment = segment.as_ref();
310
+ let is_index = match current {
311
+ Some(serde_json::Value::Object(_)) => false,
312
+ _ => is_index_segment(segment),
313
+ };
314
+ message.push('[');
315
+ push_segment(&mut message, segment, is_index);
316
+ message.push(']');
317
+
318
+ current = match (current, is_index) {
319
+ (Some(serde_json::Value::Array(values)), true) => segment
320
+ .parse::<usize>()
321
+ .ok()
322
+ .and_then(|idx| values.get(idx)),
323
+ (Some(serde_json::Value::Object(values)), false) => values.get(segment),
324
+ _ => None,
325
+ };
326
+ }
327
+ message.push_str(":\n ");
328
+
329
+ if let Some(mask) = mask {
330
+ message.push_str(mask);
331
+ } else {
332
+ let mut writer = StringWriter(&mut message);
333
+ serde_json::to_writer(&mut writer, failing_instance).expect("Failed to serialize JSON");
334
+ }
335
+
336
+ message
337
+ }
338
+
339
+ /// Compute the display message for a validation error, respecting the mask option.
340
+ fn error_message(error: &jsonschema::ValidationError<'_>, mask: Option<&str>) -> String {
341
+ if let Some(mask) = mask {
342
+ error.masked_with(mask).to_string()
343
+ } else {
344
+ error.to_string()
345
+ }
346
+ }
347
+
348
+ /// Convert a jsonschema `ValidationError` to a Ruby `ValidationError`.
349
+ fn into_ruby_error(
350
+ ruby: &Ruby,
351
+ error: jsonschema::ValidationError<'_>,
352
+ root_instance: Option<&serde_json::Value>,
353
+ message: &str,
354
+ mask: Option<&str>,
355
+ ) -> Result<Value, Error> {
356
+ let rb_message = ruby.into_value(message);
357
+ let verbose_message = build_verbose_message(
358
+ message.to_owned(),
359
+ error.schema_path(),
360
+ error.instance_path(),
361
+ root_instance,
362
+ error.instance(),
363
+ mask,
364
+ );
365
+
366
+ let (instance, kind, instance_path, schema_path, evaluation_path) = error.into_parts();
367
+
368
+ let instance_path_ptr = ruby.into_value(instance_path.as_str());
369
+ let schema_path_ptr = ruby.into_value(schema_path.as_str());
370
+ let evaluation_path_ptr = ruby.into_value(evaluation_path.as_str());
371
+
372
+ let into_path_segment = |segment: LocationSegment<'_>| -> Value {
373
+ match segment {
374
+ LocationSegment::Property(property) => ruby.into_value(property.as_ref()),
375
+ LocationSegment::Index(idx) => ruby.into_value(idx),
376
+ }
377
+ };
378
+
379
+ let kind_obj = ValidationErrorKind::new(ruby, &kind, mask)?;
380
+ let rb_instance = ser::value_to_ruby(ruby, instance.as_ref())?;
381
+
382
+ let exc_class = ruby.get_inner(&VALIDATION_ERROR_CLASS);
383
+
384
+ let exc: RObject = exc_class.funcall(*ID_ALLOCATE, ())?;
385
+
386
+ exc.ivar_set(*ID_AT_MESSAGE, rb_message)?;
387
+ exc.ivar_set(
388
+ *ID_AT_VERBOSE_MESSAGE,
389
+ ruby.into_value(verbose_message.as_str()),
390
+ )?;
391
+ exc.ivar_set(
392
+ *ID_AT_INSTANCE_PATH,
393
+ ruby.ary_from_iter(instance_path.into_iter().map(&into_path_segment)),
394
+ )?;
395
+ exc.ivar_set(
396
+ *ID_AT_SCHEMA_PATH,
397
+ ruby.ary_from_iter(schema_path.into_iter().map(&into_path_segment)),
398
+ )?;
399
+ exc.ivar_set(
400
+ *ID_AT_EVALUATION_PATH,
401
+ ruby.ary_from_iter(evaluation_path.into_iter().map(&into_path_segment)),
402
+ )?;
403
+ exc.ivar_set(*ID_AT_INSTANCE_PATH_POINTER, instance_path_ptr)?;
404
+ exc.ivar_set(*ID_AT_SCHEMA_PATH_POINTER, schema_path_ptr)?;
405
+ exc.ivar_set(*ID_AT_EVALUATION_PATH_POINTER, evaluation_path_ptr)?;
406
+ exc.ivar_set(*ID_AT_KIND, ruby.into_value(kind_obj))?;
407
+ exc.ivar_set(*ID_AT_INSTANCE, rb_instance)?;
408
+
409
+ Ok(exc.as_value())
410
+ }
411
+
412
+ /// Convert a jsonschema `ValidationError` into a Ruby `ValidationError` value.
413
+ fn to_ruby_error_value(
414
+ ruby: &Ruby,
415
+ error: jsonschema::ValidationError<'_>,
416
+ root_instance: Option<&serde_json::Value>,
417
+ mask: Option<&str>,
418
+ ) -> Result<Value, Error> {
419
+ let message = error_message(&error, mask);
420
+ into_ruby_error(ruby, error, root_instance, &message, mask)
421
+ }
422
+
423
+ fn referencing_error(ruby: &Ruby, message: String) -> Error {
424
+ let exc_class = ruby.get_inner(&REFERENCING_ERROR_CLASS);
425
+ Error::new(exc_class, message)
426
+ }
427
+
428
+ fn raise_validation_error(
429
+ ruby: &Ruby,
430
+ error: jsonschema::ValidationError<'_>,
431
+ root_instance: Option<&serde_json::Value>,
432
+ mask: Option<&str>,
433
+ ) -> Error {
434
+ let message = error_message(&error, mask);
435
+ match into_ruby_error(ruby, error, root_instance, &message, mask) {
436
+ Ok(exc_value) => {
437
+ if let Some(exc) = Exception::from_value(exc_value) {
438
+ exc.into()
439
+ } else {
440
+ let exc_class = ruby.get_inner(&VALIDATION_ERROR_CLASS);
441
+ Error::new(exc_class, message)
442
+ }
443
+ }
444
+ Err(e) => e,
445
+ }
446
+ }
447
+
448
+ /// RAII guard that sets `SUPPRESS_PANIC_OUTPUT` to `true` on creation and
449
+ /// resets it to `false` on drop, ensuring the flag is always restored even
450
+ /// if `catch_unwind` itself encounters a double-panic.
451
+ struct SuppressPanicGuard;
452
+
453
+ impl SuppressPanicGuard {
454
+ fn new() -> Self {
455
+ SUPPRESS_PANIC_OUTPUT.with(|flag| *flag.borrow_mut() = true);
456
+ SuppressPanicGuard
457
+ }
458
+ }
459
+
460
+ impl Drop for SuppressPanicGuard {
461
+ fn drop(&mut self) {
462
+ SUPPRESS_PANIC_OUTPUT.with(|flag| *flag.borrow_mut() = false);
463
+ }
464
+ }
465
+
466
+ /// Run a closure with panic output suppressed, catching panics.
467
+ fn catch_unwind_silent<F, R>(f: F) -> Result<R, Box<dyn std::any::Any + Send>>
468
+ where
469
+ F: FnOnce() -> R + panic::UnwindSafe,
470
+ {
471
+ let _guard = SuppressPanicGuard::new();
472
+ panic::catch_unwind(f)
473
+ }
474
+
475
+ #[allow(clippy::needless_pass_by_value)]
476
+ fn handle_callback_panic(ruby: &Ruby, err: Box<dyn std::any::Any + Send>) -> Error {
477
+ LAST_CALLBACK_ERROR.with(|last| {
478
+ if let Some(err) = last.borrow_mut().take() {
479
+ err
480
+ } else {
481
+ let msg = if let Some(s) = err.downcast_ref::<&str>() {
482
+ format!("Validation callback panicked: {s}")
483
+ } else if let Some(s) = err.downcast_ref::<String>() {
484
+ format!("Validation callback panicked: {s}")
485
+ } else {
486
+ "Validation callback panicked".to_string()
487
+ };
488
+ Error::new(ruby.exception_runtime_error(), msg)
489
+ }
490
+ })
491
+ }
492
+
493
+ #[allow(clippy::needless_pass_by_value)]
494
+ fn handle_without_gvl_panic(ruby: &Ruby, err: Box<dyn std::any::Any + Send>) -> Error {
495
+ let msg = if let Some(s) = err.downcast_ref::<&str>() {
496
+ format!("Validation panicked: {s}")
497
+ } else if let Some(s) = err.downcast_ref::<String>() {
498
+ format!("Validation panicked: {s}")
499
+ } else {
500
+ "Validation panicked".to_string()
501
+ };
502
+ Error::new(ruby.exception_runtime_error(), msg)
503
+ }
504
+
505
+ /// Run a closure without holding the Ruby GVL.
506
+ ///
507
+ /// The closure runs on the same thread, just without the GVL held,
508
+ /// allowing other Ruby threads to proceed. The closure MUST NOT
509
+ /// access any Ruby objects or call any Ruby API.
510
+ ///
511
+ /// # Safety
512
+ /// Caller must ensure the closure does not interact with Ruby.
513
+ #[allow(unsafe_code)]
514
+ unsafe fn without_gvl<F, R>(f: F) -> Result<R, Box<dyn std::any::Any + Send>>
515
+ where
516
+ F: FnMut() -> R,
517
+ {
518
+ struct Payload<F, R> {
519
+ f: F,
520
+ result: std::mem::MaybeUninit<Result<R, Box<dyn std::any::Any + Send>>>,
521
+ }
522
+
523
+ unsafe extern "C" fn call<F: FnMut() -> R, R>(
524
+ data: *mut std::ffi::c_void,
525
+ ) -> *mut std::ffi::c_void {
526
+ let payload = unsafe { &mut *data.cast::<Payload<F, R>>() };
527
+ let result = panic::catch_unwind(AssertUnwindSafe(|| (payload.f)()));
528
+ payload.result.write(result);
529
+ std::ptr::null_mut()
530
+ }
531
+
532
+ let mut payload = Payload {
533
+ f,
534
+ result: std::mem::MaybeUninit::uninit(),
535
+ };
536
+
537
+ unsafe {
538
+ rb_sys::rb_thread_call_without_gvl(
539
+ Some(call::<F, R>),
540
+ (&raw mut payload).cast::<std::ffi::c_void>(),
541
+ None,
542
+ std::ptr::null_mut(),
543
+ )
544
+ };
545
+
546
+ unsafe { payload.result.assume_init() }
547
+ }
548
+
549
+ /// Wrapper around `jsonschema::Validator`.
550
+ ///
551
+ /// Holds GC-protection state for Ruby callbacks (format checkers, custom keywords,
552
+ /// retrievers) that live inside the inner `jsonschema::Validator` as trait objects.
553
+ /// See the doc comment on `CallbackRoots` for the full picture.
554
+ #[derive(magnus::TypedData)]
555
+ #[magnus(class = "JSONSchema::Validator", free_immediately, size, mark)]
556
+ pub struct Validator {
557
+ validator: jsonschema::Validator,
558
+ mask: Option<String>,
559
+ has_ruby_callbacks: bool,
560
+ /// Marked during Ruby's GC mark phase to keep runtime callbacks alive.
561
+ callback_roots: CallbackRoots,
562
+ /// Protects callbacks via `register_address` during schema compilation —
563
+ /// before this wrapper exists and `mark()` can run. Held so that its `Drop`
564
+ /// impl calls `unregister_address` to balance the registrations.
565
+ _compilation_roots: CompilationRootsRef,
566
+ }
567
+
568
+ impl DataTypeFunctions for Validator {
569
+ fn mark(&self, marker: &magnus::gc::Marker) {
570
+ self.mark_callback_roots(marker);
571
+ }
572
+ }
573
+
574
+ impl Validator {
575
+ fn mark_callback_roots(&self, marker: &magnus::gc::Marker) {
576
+ // Avoid panicking in Ruby GC mark paths; preserving existing roots is safer than aborting.
577
+ let roots = match self.callback_roots.lock() {
578
+ Ok(roots) => roots,
579
+ Err(poisoned) => poisoned.into_inner(),
580
+ };
581
+ for root in roots.iter().copied() {
582
+ marker.mark(root);
583
+ }
584
+ }
585
+
586
+ #[allow(unsafe_code)]
587
+ fn is_valid(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<bool, Error> {
588
+ let json_instance = to_value(ruby, instance)?;
589
+
590
+ if rb_self.has_ruby_callbacks {
591
+ let result = catch_unwind_silent(AssertUnwindSafe(|| {
592
+ rb_self.validator.is_valid(&json_instance)
593
+ }));
594
+ match result {
595
+ Ok(valid) => Ok(valid),
596
+ Err(err) => Err(handle_callback_panic(ruby, err)),
597
+ }
598
+ } else {
599
+ // SAFETY: validation is pure Rust with no Ruby callbacks
600
+ match unsafe { without_gvl(|| rb_self.validator.is_valid(&json_instance)) } {
601
+ Ok(valid) => Ok(valid),
602
+ Err(err) => Err(handle_without_gvl_panic(ruby, err)),
603
+ }
604
+ }
605
+ }
606
+
607
+ #[allow(unsafe_code)]
608
+ fn validate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<(), Error> {
609
+ let json_instance = to_value(ruby, instance)?;
610
+
611
+ if rb_self.has_ruby_callbacks {
612
+ let result = catch_unwind_silent(AssertUnwindSafe(|| {
613
+ rb_self.validator.validate(&json_instance)
614
+ }));
615
+ match result {
616
+ Ok(Ok(())) => Ok(()),
617
+ Ok(Err(error)) => Err(raise_validation_error(
618
+ ruby,
619
+ error,
620
+ Some(&json_instance),
621
+ rb_self.mask.as_deref(),
622
+ )),
623
+ Err(err) => Err(handle_callback_panic(ruby, err)),
624
+ }
625
+ } else {
626
+ // SAFETY: validation is pure Rust with no Ruby callbacks
627
+ match unsafe { without_gvl(|| rb_self.validator.validate(&json_instance)) } {
628
+ Ok(Ok(())) => Ok(()),
629
+ Ok(Err(error)) => Err(raise_validation_error(
630
+ ruby,
631
+ error,
632
+ Some(&json_instance),
633
+ rb_self.mask.as_deref(),
634
+ )),
635
+ Err(err) => Err(handle_without_gvl_panic(ruby, err)),
636
+ }
637
+ }
638
+ }
639
+
640
+ #[allow(unsafe_code)]
641
+ fn iter_errors(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Value, Error> {
642
+ let json_instance = to_value(ruby, instance)?;
643
+
644
+ if ruby.block_given() {
645
+ // Lazy path: yield errors one at a time to the block
646
+ if rb_self.has_ruby_callbacks {
647
+ let mut iter = rb_self.validator.iter_errors(&json_instance);
648
+ loop {
649
+ let result = catch_unwind_silent(AssertUnwindSafe(|| iter.next()));
650
+ match result {
651
+ Ok(Some(error)) => {
652
+ let ruby_error = to_ruby_error_value(
653
+ ruby,
654
+ error,
655
+ Some(&json_instance),
656
+ rb_self.mask.as_deref(),
657
+ )?;
658
+ ruby.yield_value::<Value, Value>(ruby_error)?;
659
+ }
660
+ Ok(None) => break,
661
+ Err(err) => return Err(handle_callback_panic(ruby, err)),
662
+ }
663
+ }
664
+ } else {
665
+ for error in rb_self.validator.iter_errors(&json_instance) {
666
+ let ruby_error = to_ruby_error_value(
667
+ ruby,
668
+ error,
669
+ Some(&json_instance),
670
+ rb_self.mask.as_deref(),
671
+ )?;
672
+ ruby.yield_value::<Value, Value>(ruby_error)?;
673
+ }
674
+ }
675
+ Ok(ruby.qnil().as_value())
676
+ } else if rb_self.has_ruby_callbacks {
677
+ // Eager path with callbacks
678
+ let result = catch_unwind_silent(AssertUnwindSafe(|| {
679
+ rb_self
680
+ .validator
681
+ .iter_errors(&json_instance)
682
+ .collect::<Vec<_>>()
683
+ }));
684
+ match result {
685
+ Ok(errors) => {
686
+ let arr = ruby.ary_new_capa(errors.len());
687
+ for e in errors {
688
+ arr.push(to_ruby_error_value(
689
+ ruby,
690
+ e,
691
+ Some(&json_instance),
692
+ rb_self.mask.as_deref(),
693
+ )?)?;
694
+ }
695
+ Ok(arr.as_value())
696
+ }
697
+ Err(err) => Err(handle_callback_panic(ruby, err)),
698
+ }
699
+ } else {
700
+ // Eager path without callbacks — release GVL
701
+ // SAFETY: validation is pure Rust with no Ruby callbacks
702
+ let errors = match unsafe {
703
+ without_gvl(|| {
704
+ rb_self
705
+ .validator
706
+ .iter_errors(&json_instance)
707
+ .collect::<Vec<_>>()
708
+ })
709
+ } {
710
+ Ok(errors) => errors,
711
+ Err(err) => return Err(handle_without_gvl_panic(ruby, err)),
712
+ };
713
+ let arr = ruby.ary_new_capa(errors.len());
714
+ for e in errors {
715
+ arr.push(to_ruby_error_value(
716
+ ruby,
717
+ e,
718
+ Some(&json_instance),
719
+ rb_self.mask.as_deref(),
720
+ )?)?;
721
+ }
722
+ Ok(arr.as_value())
723
+ }
724
+ }
725
+
726
+ #[allow(unsafe_code)]
727
+ fn evaluate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Evaluation, Error> {
728
+ let json_instance = to_value(ruby, instance)?;
729
+
730
+ if rb_self.has_ruby_callbacks {
731
+ let result = catch_unwind_silent(AssertUnwindSafe(|| {
732
+ rb_self.validator.evaluate(&json_instance)
733
+ }));
734
+ match result {
735
+ Ok(output) => Ok(Evaluation::new(output)),
736
+ Err(err) => Err(handle_callback_panic(ruby, err)),
737
+ }
738
+ } else {
739
+ // SAFETY: validation is pure Rust with no Ruby callbacks
740
+ let output = match unsafe { without_gvl(|| rb_self.validator.evaluate(&json_instance)) }
741
+ {
742
+ Ok(output) => output,
743
+ Err(err) => return Err(handle_without_gvl_panic(ruby, err)),
744
+ };
745
+ Ok(Evaluation::new(output))
746
+ }
747
+ }
748
+
749
+ fn inspect(&self) -> String {
750
+ let draft = match self.validator.draft() {
751
+ jsonschema::Draft::Draft4 => "Draft4",
752
+ jsonschema::Draft::Draft6 => "Draft6",
753
+ jsonschema::Draft::Draft7 => "Draft7",
754
+ jsonschema::Draft::Draft201909 => "Draft201909",
755
+ jsonschema::Draft::Draft202012 => "Draft202012",
756
+ _ => "Unknown",
757
+ };
758
+ format!("#<JSONSchema::{draft}Validator>")
759
+ }
760
+ }
761
+
762
+ fn validator_for(ruby: &Ruby, args: &[Value]) -> Result<Validator, Error> {
763
+ let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
764
+ let (schema,) = parsed_args.required;
765
+ let kw = extract_kwargs_no_draft(ruby, parsed_args.keywords)?;
766
+
767
+ let json_schema = to_schema_value(ruby, schema)?;
768
+ let parsed = build_parsed_options(ruby, kw, None)?;
769
+ let has_ruby_callbacks = parsed.has_ruby_callbacks;
770
+ let BuiltValidator {
771
+ validator,
772
+ callback_roots,
773
+ compilation_roots,
774
+ } = build_validator(
775
+ ruby,
776
+ parsed.options,
777
+ parsed.retriever,
778
+ parsed.callback_roots,
779
+ parsed.compilation_roots,
780
+ &json_schema,
781
+ )?;
782
+ Ok(Validator {
783
+ validator,
784
+ mask: parsed.mask,
785
+ has_ruby_callbacks,
786
+ callback_roots,
787
+ _compilation_roots: compilation_roots,
788
+ })
789
+ }
790
+
791
+ #[allow(unsafe_code)]
792
+ fn is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
793
+ let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
794
+ let (schema, instance) = parsed_args.required;
795
+ let kw = extract_kwargs(ruby, parsed_args.keywords)?;
796
+
797
+ let json_schema = to_schema_value(ruby, schema)?;
798
+ let json_instance = to_value(ruby, instance)?;
799
+ let parsed = build_parsed_options(ruby, kw, None)?;
800
+ let has_ruby_callbacks = parsed.has_ruby_callbacks;
801
+ let BuiltValidator {
802
+ validator,
803
+ callback_roots,
804
+ compilation_roots: _compilation_roots,
805
+ } = build_validator(
806
+ ruby,
807
+ parsed.options,
808
+ parsed.retriever,
809
+ parsed.callback_roots,
810
+ parsed.compilation_roots,
811
+ &json_schema,
812
+ )?;
813
+
814
+ if has_ruby_callbacks {
815
+ let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
816
+ let result = catch_unwind_silent(AssertUnwindSafe(|| validator.is_valid(&json_instance)));
817
+ match result {
818
+ Ok(valid) => Ok(valid),
819
+ Err(err) => Err(handle_callback_panic(ruby, err)),
820
+ }
821
+ } else {
822
+ // SAFETY: validation is pure Rust with no Ruby callbacks
823
+ match unsafe { without_gvl(|| validator.is_valid(&json_instance)) } {
824
+ Ok(valid) => Ok(valid),
825
+ Err(err) => Err(handle_without_gvl_panic(ruby, err)),
826
+ }
827
+ }
828
+ }
829
+
830
+ #[allow(unsafe_code)]
831
+ fn validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
832
+ let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
833
+ let (schema, instance) = parsed_args.required;
834
+ let kw = extract_kwargs(ruby, parsed_args.keywords)?;
835
+
836
+ let json_schema = to_schema_value(ruby, schema)?;
837
+ let json_instance = to_value(ruby, instance)?;
838
+ let parsed = build_parsed_options(ruby, kw, None)?;
839
+ let has_ruby_callbacks = parsed.has_ruby_callbacks;
840
+ let BuiltValidator {
841
+ validator,
842
+ callback_roots,
843
+ compilation_roots: _compilation_roots,
844
+ } = build_validator(
845
+ ruby,
846
+ parsed.options,
847
+ parsed.retriever,
848
+ parsed.callback_roots,
849
+ parsed.compilation_roots,
850
+ &json_schema,
851
+ )?;
852
+
853
+ if has_ruby_callbacks {
854
+ let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
855
+ let result = catch_unwind_silent(AssertUnwindSafe(|| validator.validate(&json_instance)));
856
+ match result {
857
+ Ok(Ok(())) => Ok(()),
858
+ Ok(Err(error)) => Err(raise_validation_error(
859
+ ruby,
860
+ error,
861
+ Some(&json_instance),
862
+ parsed.mask.as_deref(),
863
+ )),
864
+ Err(err) => Err(handle_callback_panic(ruby, err)),
865
+ }
866
+ } else {
867
+ // SAFETY: validation is pure Rust with no Ruby callbacks
868
+ match unsafe { without_gvl(|| validator.validate(&json_instance)) } {
869
+ Ok(Ok(())) => Ok(()),
870
+ Ok(Err(error)) => Err(raise_validation_error(
871
+ ruby,
872
+ error,
873
+ Some(&json_instance),
874
+ parsed.mask.as_deref(),
875
+ )),
876
+ Err(err) => Err(handle_without_gvl_panic(ruby, err)),
877
+ }
878
+ }
879
+ }
880
+
881
+ #[allow(unsafe_code)]
882
+ fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
883
+ let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
884
+ let (schema, instance) = parsed_args.required;
885
+ let kw = extract_kwargs(ruby, parsed_args.keywords)?;
886
+
887
+ let json_schema = to_schema_value(ruby, schema)?;
888
+ let json_instance = to_value(ruby, instance)?;
889
+ let parsed = build_parsed_options(ruby, kw, None)?;
890
+ let has_ruby_callbacks = parsed.has_ruby_callbacks;
891
+ let BuiltValidator {
892
+ validator,
893
+ callback_roots,
894
+ compilation_roots: _compilation_roots,
895
+ } = build_validator(
896
+ ruby,
897
+ parsed.options,
898
+ parsed.retriever,
899
+ parsed.callback_roots,
900
+ parsed.compilation_roots,
901
+ &json_schema,
902
+ )?;
903
+
904
+ if ruby.block_given() {
905
+ // Lazy path: yield errors one at a time to the block
906
+ if has_ruby_callbacks {
907
+ let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
908
+ let mut iter = validator.iter_errors(&json_instance);
909
+ loop {
910
+ let result = catch_unwind_silent(AssertUnwindSafe(|| iter.next()));
911
+ match result {
912
+ Ok(Some(error)) => {
913
+ let ruby_error = to_ruby_error_value(
914
+ ruby,
915
+ error,
916
+ Some(&json_instance),
917
+ parsed.mask.as_deref(),
918
+ )?;
919
+ ruby.yield_value::<Value, Value>(ruby_error)?;
920
+ }
921
+ Ok(None) => break,
922
+ Err(err) => return Err(handle_callback_panic(ruby, err)),
923
+ }
924
+ }
925
+ } else {
926
+ for error in validator.iter_errors(&json_instance) {
927
+ let ruby_error =
928
+ to_ruby_error_value(ruby, error, Some(&json_instance), parsed.mask.as_deref())?;
929
+ ruby.yield_value::<Value, Value>(ruby_error)?;
930
+ }
931
+ }
932
+ Ok(ruby.qnil().as_value())
933
+ } else if has_ruby_callbacks {
934
+ // Eager path with callbacks
935
+ let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
936
+ let result = catch_unwind_silent(AssertUnwindSafe(|| {
937
+ validator.iter_errors(&json_instance).collect::<Vec<_>>()
938
+ }));
939
+ match result {
940
+ Ok(errors) => {
941
+ let arr = ruby.ary_new_capa(errors.len());
942
+ for e in errors {
943
+ arr.push(to_ruby_error_value(
944
+ ruby,
945
+ e,
946
+ Some(&json_instance),
947
+ parsed.mask.as_deref(),
948
+ )?)?;
949
+ }
950
+ Ok(arr.as_value())
951
+ }
952
+ Err(err) => Err(handle_callback_panic(ruby, err)),
953
+ }
954
+ } else {
955
+ // Eager path without callbacks — release GVL
956
+ // SAFETY: validation is pure Rust with no Ruby callbacks
957
+ let errors = match unsafe {
958
+ without_gvl(|| validator.iter_errors(&json_instance).collect::<Vec<_>>())
959
+ } {
960
+ Ok(errors) => errors,
961
+ Err(err) => return Err(handle_without_gvl_panic(ruby, err)),
962
+ };
963
+ let arr = ruby.ary_new_capa(errors.len());
964
+ for e in errors {
965
+ arr.push(to_ruby_error_value(
966
+ ruby,
967
+ e,
968
+ Some(&json_instance),
969
+ parsed.mask.as_deref(),
970
+ )?)?;
971
+ }
972
+ Ok(arr.as_value())
973
+ }
974
+ }
975
+
976
+ #[allow(unsafe_code)]
977
+ fn evaluate(ruby: &Ruby, args: &[Value]) -> Result<Evaluation, Error> {
978
+ let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
979
+ let (schema, instance) = parsed_args.required;
980
+ let kw = extract_evaluate_kwargs(ruby, parsed_args.keywords)?;
981
+
982
+ let json_schema = to_schema_value(ruby, schema)?;
983
+ let json_instance = to_value(ruby, instance)?;
984
+ let parsed = build_parsed_options(ruby, kw, None)?;
985
+ let has_ruby_callbacks = parsed.has_ruby_callbacks;
986
+ let BuiltValidator {
987
+ validator,
988
+ callback_roots,
989
+ compilation_roots: _compilation_roots,
990
+ } = build_validator(
991
+ ruby,
992
+ parsed.options,
993
+ parsed.retriever,
994
+ parsed.callback_roots,
995
+ parsed.compilation_roots,
996
+ &json_schema,
997
+ )?;
998
+
999
+ if has_ruby_callbacks {
1000
+ let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
1001
+ let result = catch_unwind_silent(AssertUnwindSafe(|| validator.evaluate(&json_instance)));
1002
+ match result {
1003
+ Ok(output) => Ok(Evaluation::new(output)),
1004
+ Err(err) => Err(handle_callback_panic(ruby, err)),
1005
+ }
1006
+ } else {
1007
+ // SAFETY: validation is pure Rust with no Ruby callbacks
1008
+ let output = match unsafe { without_gvl(|| validator.evaluate(&json_instance)) } {
1009
+ Ok(output) => output,
1010
+ Err(err) => return Err(handle_without_gvl_panic(ruby, err)),
1011
+ };
1012
+ Ok(Evaluation::new(output))
1013
+ }
1014
+ }
1015
+
1016
+ macro_rules! define_draft_validator {
1017
+ ($name:ident, $class_name:expr, $draft:expr) => {
1018
+ #[derive(magnus::TypedData)]
1019
+ #[magnus(class = $class_name, free_immediately, size, mark)]
1020
+ pub struct $name {
1021
+ inner: Validator,
1022
+ }
1023
+
1024
+ impl DataTypeFunctions for $name {
1025
+ fn mark(&self, marker: &magnus::gc::Marker) {
1026
+ self.inner.mark_callback_roots(marker);
1027
+ }
1028
+ }
1029
+
1030
+ impl $name {
1031
+ fn new_impl(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
1032
+ let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1033
+ let (schema,) = parsed_args.required;
1034
+ let kw = extract_kwargs_no_draft(ruby, parsed_args.keywords)?;
1035
+
1036
+ let json_schema = to_schema_value(ruby, schema)?;
1037
+ let parsed = build_parsed_options(ruby, kw, Some($draft))?;
1038
+ let has_ruby_callbacks = parsed.has_ruby_callbacks;
1039
+ let BuiltValidator {
1040
+ validator,
1041
+ callback_roots,
1042
+ compilation_roots,
1043
+ } = build_validator(
1044
+ ruby,
1045
+ parsed.options,
1046
+ parsed.retriever,
1047
+ parsed.callback_roots,
1048
+ parsed.compilation_roots,
1049
+ &json_schema,
1050
+ )?;
1051
+ Ok($name {
1052
+ inner: Validator {
1053
+ validator,
1054
+ mask: parsed.mask,
1055
+ has_ruby_callbacks,
1056
+ callback_roots,
1057
+ _compilation_roots: compilation_roots,
1058
+ },
1059
+ })
1060
+ }
1061
+
1062
+ fn is_valid(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<bool, Error> {
1063
+ Validator::is_valid(ruby, &rb_self.inner, instance)
1064
+ }
1065
+
1066
+ fn validate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<(), Error> {
1067
+ Validator::validate(ruby, &rb_self.inner, instance)
1068
+ }
1069
+
1070
+ fn iter_errors(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Value, Error> {
1071
+ Validator::iter_errors(ruby, &rb_self.inner, instance)
1072
+ }
1073
+
1074
+ fn evaluate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Evaluation, Error> {
1075
+ Validator::evaluate(ruby, &rb_self.inner, instance)
1076
+ }
1077
+
1078
+ fn inspect(&self) -> String {
1079
+ self.inner.inspect()
1080
+ }
1081
+ }
1082
+ };
1083
+ }
1084
+
1085
+ define_draft_validator!(
1086
+ Draft4Validator,
1087
+ "JSONSchema::Draft4Validator",
1088
+ jsonschema::Draft::Draft4
1089
+ );
1090
+ define_draft_validator!(
1091
+ Draft6Validator,
1092
+ "JSONSchema::Draft6Validator",
1093
+ jsonschema::Draft::Draft6
1094
+ );
1095
+ define_draft_validator!(
1096
+ Draft7Validator,
1097
+ "JSONSchema::Draft7Validator",
1098
+ jsonschema::Draft::Draft7
1099
+ );
1100
+ define_draft_validator!(
1101
+ Draft201909Validator,
1102
+ "JSONSchema::Draft201909Validator",
1103
+ jsonschema::Draft::Draft201909
1104
+ );
1105
+ define_draft_validator!(
1106
+ Draft202012Validator,
1107
+ "JSONSchema::Draft202012Validator",
1108
+ jsonschema::Draft::Draft202012
1109
+ );
1110
+
1111
+ fn meta_is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
1112
+ use magnus::scan_args::get_kwargs;
1113
+ let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1114
+ let (schema,) = parsed_args.required;
1115
+ let kw: magnus::scan_args::KwArgs<(), (Option<Option<&Registry>>,), ()> =
1116
+ get_kwargs(parsed_args.keywords, &[], &[*options::KW_REGISTRY])?;
1117
+ let registry = kw.optional.0.flatten();
1118
+
1119
+ let json_schema = to_schema_value(ruby, schema)?;
1120
+
1121
+ let result = if let Some(reg) = registry {
1122
+ jsonschema::meta::options()
1123
+ .with_registry(reg.inner.clone())
1124
+ .validate(&json_schema)
1125
+ } else {
1126
+ jsonschema::meta::validate(&json_schema)
1127
+ };
1128
+
1129
+ match result {
1130
+ Ok(()) => Ok(true),
1131
+ Err(error) => {
1132
+ if let jsonschema::error::ValidationErrorKind::Referencing(err) = error.kind() {
1133
+ return Err(referencing_error(ruby, err.to_string()));
1134
+ }
1135
+ Ok(false)
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ fn meta_validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1141
+ use magnus::scan_args::get_kwargs;
1142
+ let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1143
+ let (schema,) = parsed_args.required;
1144
+ let kw: magnus::scan_args::KwArgs<(), (Option<Option<&Registry>>,), ()> =
1145
+ get_kwargs(parsed_args.keywords, &[], &[*options::KW_REGISTRY])?;
1146
+ let registry = kw.optional.0.flatten();
1147
+
1148
+ let json_schema = to_schema_value(ruby, schema)?;
1149
+
1150
+ let result = if let Some(reg) = registry {
1151
+ jsonschema::meta::options()
1152
+ .with_registry(reg.inner.clone())
1153
+ .validate(&json_schema)
1154
+ } else {
1155
+ jsonschema::meta::validate(&json_schema)
1156
+ };
1157
+
1158
+ match result {
1159
+ Ok(()) => Ok(()),
1160
+ Err(error) => {
1161
+ if let jsonschema::error::ValidationErrorKind::Referencing(err) = error.kind() {
1162
+ return Err(referencing_error(ruby, err.to_string()));
1163
+ }
1164
+ Err(raise_validation_error(
1165
+ ruby,
1166
+ error,
1167
+ Some(&json_schema),
1168
+ None,
1169
+ ))
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ // ValidationError instance methods (defined from Rust, called on exception instances)
1175
+
1176
+ fn validation_error_to_s(ruby: &Ruby, rb_self: Value) -> Result<Value, Error> {
1177
+ let obj = RObject::from_value(rb_self).ok_or_else(|| {
1178
+ Error::new(
1179
+ Ruby::get().expect("Ruby").exception_type_error(),
1180
+ "expected object",
1181
+ )
1182
+ })?;
1183
+ let message: Value = obj.ivar_get(*ID_AT_MESSAGE)?;
1184
+ if message.is_nil() {
1185
+ ruby.call_super(())
1186
+ } else {
1187
+ Ok(message)
1188
+ }
1189
+ }
1190
+
1191
+ fn validation_error_inspect(_ruby: &Ruby, rb_self: Value) -> Result<String, Error> {
1192
+ let msg: String = rb_self.funcall("to_s", ())?;
1193
+ Ok(format!("#<JSONSchema::ValidationError: {msg}>"))
1194
+ }
1195
+
1196
+ fn validation_error_eq(ruby: &Ruby, rb_self: Value, other: Value) -> Result<bool, Error> {
1197
+ let exc_class = ruby.get_inner(&VALIDATION_ERROR_CLASS);
1198
+ let other_obj = match RObject::from_value(other) {
1199
+ Some(obj) if obj.is_kind_of(exc_class) => obj,
1200
+ _ => return Ok(false),
1201
+ };
1202
+ let self_obj = RObject::from_value(rb_self)
1203
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected object"))?;
1204
+
1205
+ let self_key = ruby.ary_new_capa(3);
1206
+ self_key.push(self_obj.ivar_get::<_, Value>(*ID_AT_MESSAGE)?)?;
1207
+ self_key.push(self_obj.ivar_get::<_, Value>(*ID_AT_SCHEMA_PATH)?)?;
1208
+ self_key.push(self_obj.ivar_get::<_, Value>(*ID_AT_INSTANCE_PATH)?)?;
1209
+
1210
+ let other_key = ruby.ary_new_capa(3);
1211
+ other_key.push(other_obj.ivar_get::<_, Value>(*ID_AT_MESSAGE)?)?;
1212
+ other_key.push(other_obj.ivar_get::<_, Value>(*ID_AT_SCHEMA_PATH)?)?;
1213
+ other_key.push(other_obj.ivar_get::<_, Value>(*ID_AT_INSTANCE_PATH)?)?;
1214
+
1215
+ self_key.funcall("==", (other_key,))
1216
+ }
1217
+
1218
+ fn validation_error_hash(ruby: &Ruby, rb_self: Value) -> Result<Value, Error> {
1219
+ let obj = RObject::from_value(rb_self)
1220
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected object"))?;
1221
+ let arr = ruby.ary_new_capa(3);
1222
+ arr.push(obj.ivar_get::<_, Value>(*ID_AT_MESSAGE)?)?;
1223
+ arr.push(obj.ivar_get::<_, Value>(*ID_AT_SCHEMA_PATH)?)?;
1224
+ arr.push(obj.ivar_get::<_, Value>(*ID_AT_INSTANCE_PATH)?)?;
1225
+ arr.funcall("hash", ())
1226
+ }
1227
+
1228
+ #[magnus::init(name = "jsonschema_rb")]
1229
+ fn init(ruby: &Ruby) -> Result<(), Error> {
1230
+ // Conditionally suppress panic output — only when inside `catch_unwind`
1231
+ // blocks used for Ruby callback panics (format checkers, custom keywords).
1232
+ // Other panics pass through to the default handler to preserve debugging output.
1233
+ let default_hook = std::panic::take_hook();
1234
+ std::panic::set_hook(Box::new(move |info| {
1235
+ let suppress = SUPPRESS_PANIC_OUTPUT.with(|flag| *flag.borrow());
1236
+ if !suppress {
1237
+ default_hook(info);
1238
+ }
1239
+ }));
1240
+
1241
+ let module = ruby.define_module("JSONSchema")?;
1242
+
1243
+ // ValidationError < StandardError
1244
+ let validation_error_class =
1245
+ module.define_error("ValidationError", ruby.exception_standard_error())?;
1246
+ let validation_error_rclass =
1247
+ RClass::from_value(validation_error_class.as_value()).expect("ExceptionClass is an RClass");
1248
+ for sym_id in [
1249
+ &*ID_SYM_MESSAGE,
1250
+ &*ID_SYM_VERBOSE_MESSAGE,
1251
+ &*ID_SYM_INSTANCE_PATH,
1252
+ &*ID_SYM_SCHEMA_PATH,
1253
+ &*ID_SYM_EVALUATION_PATH,
1254
+ &*ID_SYM_KIND,
1255
+ &*ID_SYM_INSTANCE,
1256
+ &*ID_SYM_INSTANCE_PATH_POINTER,
1257
+ &*ID_SYM_SCHEMA_PATH_POINTER,
1258
+ &*ID_SYM_EVALUATION_PATH_POINTER,
1259
+ ] {
1260
+ let _: Value = validation_error_rclass.funcall("attr_reader", (sym_id.to_symbol(),))?;
1261
+ }
1262
+ validation_error_rclass.define_method("message", method!(validation_error_to_s, 0))?;
1263
+ validation_error_rclass.define_method("to_s", method!(validation_error_to_s, 0))?;
1264
+ validation_error_rclass.define_method("inspect", method!(validation_error_inspect, 0))?;
1265
+ validation_error_rclass.define_method("==", method!(validation_error_eq, 1))?;
1266
+ validation_error_rclass.define_alias("eql?", "==")?;
1267
+ validation_error_rclass.define_method("hash", method!(validation_error_hash, 0))?;
1268
+
1269
+ // ReferencingError < StandardError
1270
+ module.define_error("ReferencingError", ruby.exception_standard_error())?;
1271
+
1272
+ // Module-level functions
1273
+ module.define_singleton_method("validator_for", function!(validator_for, -1))?;
1274
+ module.define_singleton_method("valid?", function!(is_valid, -1))?;
1275
+ module.define_singleton_method("validate!", function!(validate, -1))?;
1276
+ module.define_singleton_method("each_error", function!(each_error, -1))?;
1277
+ module.define_singleton_method("evaluate", function!(evaluate, -1))?;
1278
+
1279
+ // Validator class
1280
+ let validator_class = module.define_class("Validator", ruby.class_object())?;
1281
+ validator_class.define_method("valid?", method!(Validator::is_valid, 1))?;
1282
+ validator_class.define_method("validate!", method!(Validator::validate, 1))?;
1283
+ validator_class.define_method("each_error", method!(Validator::iter_errors, 1))?;
1284
+ validator_class.define_method("evaluate", method!(Validator::evaluate, 1))?;
1285
+ validator_class.define_method("inspect", method!(Validator::inspect, 0))?;
1286
+
1287
+ // Draft-specific validators
1288
+ macro_rules! define_draft_class {
1289
+ ($ruby:expr, $module:expr, $name:ident, $class_str:expr, $superclass:expr) => {
1290
+ let cls = $module.define_class($class_str, $superclass)?;
1291
+ cls.define_singleton_method("new", function!($name::new_impl, -1))?;
1292
+ cls.define_method("valid?", method!($name::is_valid, 1))?;
1293
+ cls.define_method("validate!", method!($name::validate, 1))?;
1294
+ cls.define_method("each_error", method!($name::iter_errors, 1))?;
1295
+ cls.define_method("evaluate", method!($name::evaluate, 1))?;
1296
+ cls.define_method("inspect", method!($name::inspect, 0))?;
1297
+ };
1298
+ }
1299
+
1300
+ define_draft_class!(
1301
+ ruby,
1302
+ module,
1303
+ Draft4Validator,
1304
+ "Draft4Validator",
1305
+ validator_class
1306
+ );
1307
+ define_draft_class!(
1308
+ ruby,
1309
+ module,
1310
+ Draft6Validator,
1311
+ "Draft6Validator",
1312
+ validator_class
1313
+ );
1314
+ define_draft_class!(
1315
+ ruby,
1316
+ module,
1317
+ Draft7Validator,
1318
+ "Draft7Validator",
1319
+ validator_class
1320
+ );
1321
+ define_draft_class!(
1322
+ ruby,
1323
+ module,
1324
+ Draft201909Validator,
1325
+ "Draft201909Validator",
1326
+ validator_class
1327
+ );
1328
+ define_draft_class!(
1329
+ ruby,
1330
+ module,
1331
+ Draft202012Validator,
1332
+ "Draft202012Validator",
1333
+ validator_class
1334
+ );
1335
+
1336
+ // Internal implementation detail for shared validator behavior.
1337
+ let _: Value = module.funcall("private_constant", ("Validator",))?;
1338
+
1339
+ evaluation::define_class(ruby, &module)?;
1340
+ registry::define_class(ruby, &module)?;
1341
+ error_kind::define_class(ruby, &module)?;
1342
+ options::define_classes(ruby, &module)?;
1343
+
1344
+ let meta_module = module.define_module("Meta")?;
1345
+ meta_module.define_singleton_method("valid?", function!(meta_is_valid, -1))?;
1346
+ meta_module.define_singleton_method("validate!", function!(meta_validate, -1))?;
1347
+
1348
+ Ok(())
1349
+ }