jsonschema_rs 0.45.0 → 0.46.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.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb-ext"
3
- version = "0.45.0"
3
+ version = "0.46.0"
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.45.0", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring"] }
13
+ jsonschema = { version = "0.46.0", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring"] }
14
14
  magnus = { version = "0.8", features = ["rb-sys"] }
15
15
  rb-sys = "0.9"
16
- referencing = "0.45.0"
16
+ referencing = "0.46.0"
17
17
  serde = { version = "1", features = ["derive"] }
18
18
  serde_json = { version = "1", features = ["arbitrary_precision"] }
19
19
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONSchema
4
- VERSION = "0.45.0"
4
+ VERSION = "0.46.0"
5
5
  end
data/sig/jsonschema.rbs CHANGED
@@ -41,6 +41,22 @@ module JSONSchema
41
41
  ?http_options: HttpOptions?
42
42
  ) -> Validator
43
43
 
44
+ # Create a map of validators for all sub-schemas in a JSON Schema.
45
+ def self.validator_map_for: (
46
+ untyped schema,
47
+ ?validate_formats: bool?,
48
+ ?ignore_unknown_formats: bool?,
49
+ ?mask: ::String?,
50
+ ?base_uri: ::String?,
51
+ ?retriever: (^(::String) -> untyped)?,
52
+ ?registry: Registry?,
53
+ ?formats: ::Hash[::String, ^(::String) -> bool]?,
54
+ ?keywords: ::Hash[::String, untyped]?,
55
+ ?pattern_options: (RegexOptions | FancyRegexOptions)?,
56
+ ?email_options: EmailOptions?,
57
+ ?http_options: HttpOptions?
58
+ ) -> ValidatorMap
59
+
44
60
  # Bundle a JSON Schema into a Compound Schema Document.
45
61
  # All externally-referenced schemas reachable via $ref are embedded in a
46
62
  # draft-appropriate container (definitions for Draft 4/6/7, $defs for
@@ -54,6 +70,16 @@ module JSONSchema
54
70
  ?base_uri: String?
55
71
  ) -> Hash[String, untyped]
56
72
 
73
+ # Dereference a JSON Schema by recursively replacing all $ref values
74
+ # with the schemas they point to.
75
+ def self.dereference: (
76
+ untyped schema,
77
+ ?draft: draft?,
78
+ ?retriever: (^(String) -> untyped)?,
79
+ ?registry: Registry?,
80
+ ?base_uri: String?
81
+ ) -> untyped
82
+
57
83
  # Detect the JSON Schema draft for a schema and return the corresponding validator class.
58
84
  # Draft is detected automatically from the `$schema` field. Defaults to Draft202012Validator.
59
85
  def self.validator_cls_for: (untyped schema) -> (singleton(Draft4Validator) | singleton(Draft6Validator) | singleton(Draft7Validator) | singleton(Draft201909Validator) | singleton(Draft202012Validator))
@@ -165,6 +191,16 @@ module JSONSchema
165
191
  end
166
192
  public
167
193
 
194
+ # Map of validators for all sub-schemas in a JSON Schema, keyed by JSON Pointer.
195
+ class ValidatorMap
196
+ def []: (::String pointer) -> Validator?
197
+ def fetch: (::String pointer) -> Validator
198
+ def key?: (::String pointer) -> bool
199
+ def keys: () -> ::Array[::String]
200
+ def length: () -> ::Integer
201
+ def size: () -> ::Integer
202
+ end
203
+
168
204
  # JSON Schema Draft 4 validator.
169
205
  class Draft4Validator < Validator
170
206
  end
@@ -281,6 +317,20 @@ module JSONSchema
281
317
  def inspect: () -> String
282
318
  end
283
319
 
320
+ # Resolver for JSON Schema references.
321
+ class Resolver
322
+ def base_uri: () -> String
323
+ def dynamic_scope: () -> Array[String]
324
+ def lookup: (String reference) -> Resolved
325
+ end
326
+
327
+ # Resolved reference containing the dereferenced schema and its context.
328
+ class Resolved
329
+ def contents: () -> untyped
330
+ def resolver: () -> Resolver
331
+ def draft: () -> Integer
332
+ end
333
+
284
334
  # Schema registry for reference resolution.
285
335
  class Registry
286
336
  # Create a registry from URI/schema pairs.
data/src/lib.rs CHANGED
@@ -12,6 +12,7 @@ mod registry;
12
12
  mod retriever;
13
13
  mod ser;
14
14
  mod static_id;
15
+ mod validator_map;
15
16
 
16
17
  use jsonschema::{paths::LocationSegment, ValidationOptions};
17
18
  use magnus::{
@@ -22,8 +23,8 @@ use magnus::{
22
23
  prelude::*,
23
24
  scan_args::scan_args,
24
25
  value::{Lazy, ReprValue},
25
- DataTypeFunctions, Error, Exception, ExceptionClass, RClass, RHash, RModule, RObject, Ruby,
26
- Value,
26
+ DataTypeFunctions, Error, Exception, ExceptionClass, RClass, RHash, RModule, RObject, RString,
27
+ Ruby, Value,
27
28
  };
28
29
  use referencing::unescape_segment;
29
30
  use std::{
@@ -84,16 +85,21 @@ struct BuiltValidator {
84
85
  fn build_validator(
85
86
  ruby: &Ruby,
86
87
  options: ValidationOptions,
88
+ registry: Option<&jsonschema::Registry<'_>>,
87
89
  retriever: Option<RubyRetriever>,
88
90
  callback_roots: CallbackRoots,
89
91
  compilation_roots: Arc<CompilationRoots>,
90
92
  schema: &serde_json::Value,
91
93
  ) -> Result<BuiltValidator, Error> {
92
- let validator = match retriever {
93
- Some(ret) => options.with_retriever(ret).build(schema),
94
- None => options.build(schema),
94
+ let mut options = match retriever {
95
+ Some(ret) => options.with_retriever(ret),
96
+ None => options,
97
+ };
98
+ if let Some(registry) = registry {
99
+ options = options.with_registry(registry);
95
100
  }
96
- .map_err(|error| {
101
+
102
+ let validator = options.build(schema).map_err(|error| {
97
103
  if let jsonschema::error::ValidationErrorKind::Referencing(err) = error.kind() {
98
104
  if let Some(message) = retriever_error_message(err) {
99
105
  Error::new(ruby.exception_arg_error(), message)
@@ -157,7 +163,7 @@ fn build_parsed_options(
157
163
  ruby: &Ruby,
158
164
  kw: ExtractedKwargs,
159
165
  draft_override: Option<jsonschema::Draft>,
160
- ) -> Result<ParsedOptions, Error> {
166
+ ) -> Result<ParsedOptions<'_>, Error> {
161
167
  let (
162
168
  draft_val,
163
169
  validate_formats,
@@ -446,7 +452,7 @@ fn to_ruby_error_value(
446
452
  into_ruby_error(ruby, error, root_instance, &message, mask)
447
453
  }
448
454
 
449
- fn referencing_error(ruby: &Ruby, message: String) -> Error {
455
+ pub(crate) fn referencing_error(ruby: &Ruby, message: String) -> Error {
450
456
  let exc_class = ruby.get_inner(&REFERENCING_ERROR_CLASS);
451
457
  Error::new(exc_class, message)
452
458
  }
@@ -598,6 +604,22 @@ impl DataTypeFunctions for Validator {
598
604
  }
599
605
 
600
606
  impl Validator {
607
+ pub(crate) fn from_jsonschema_with_roots(
608
+ validator: jsonschema::Validator,
609
+ mask: Option<String>,
610
+ has_ruby_callbacks: bool,
611
+ callback_roots: CallbackRoots,
612
+ compilation_roots: CompilationRootsRef,
613
+ ) -> Self {
614
+ Self {
615
+ validator,
616
+ mask,
617
+ has_ruby_callbacks,
618
+ callback_roots,
619
+ _compilation_roots: compilation_roots,
620
+ }
621
+ }
622
+
601
623
  fn mark_callback_roots(&self, marker: &magnus::gc::Marker) {
602
624
  // Avoid panicking in Ruby GC mark paths; preserving existing roots is safer than aborting.
603
625
  let roots = match self.callback_roots.lock() {
@@ -802,6 +824,24 @@ fn validator_cls_for(ruby: &Ruby, schema: Value) -> Result<RClass, Error> {
802
824
  } else {
803
825
  jsonschema::Draft::default()
804
826
  }
827
+ } else if let Some(rstring) = RString::from_value(schema) {
828
+ #[allow(unsafe_code)]
829
+ let bytes = unsafe { rstring.as_slice() };
830
+ let value: serde_json::Value = serde_json::from_slice(bytes).map_err(|e| {
831
+ Error::new(
832
+ ruby.exception_arg_error(),
833
+ format!("Invalid JSON string: {e}"),
834
+ )
835
+ })?;
836
+ if let serde_json::Value::Object(map) = &value {
837
+ if let Some(serde_json::Value::String(s)) = map.get("$schema") {
838
+ jsonschema::Draft::from_schema_uri(s)
839
+ } else {
840
+ jsonschema::Draft::default()
841
+ }
842
+ } else {
843
+ jsonschema::Draft::default()
844
+ }
805
845
  } else {
806
846
  jsonschema::Draft::default()
807
847
  };
@@ -832,6 +872,7 @@ fn validator_for(ruby: &Ruby, args: &[Value]) -> Result<Validator, Error> {
832
872
  } = build_validator(
833
873
  ruby,
834
874
  parsed.options,
875
+ parsed.registry,
835
876
  parsed.retriever,
836
877
  parsed.callback_roots,
837
878
  parsed.compilation_roots,
@@ -846,6 +887,49 @@ fn validator_for(ruby: &Ruby, args: &[Value]) -> Result<Validator, Error> {
846
887
  })
847
888
  }
848
889
 
890
+ fn validator_map_for(ruby: &Ruby, args: &[Value]) -> Result<validator_map::ValidatorMap, Error> {
891
+ let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
892
+ let (schema,) = parsed_args.required;
893
+ let kw = extract_kwargs_no_draft(ruby, parsed_args.keywords)?;
894
+
895
+ let json_schema = to_schema_value(ruby, schema)?;
896
+ let parsed = build_parsed_options(ruby, kw, None)?;
897
+ let has_ruby_callbacks = parsed.has_ruby_callbacks;
898
+ let callback_roots = parsed.callback_roots;
899
+ let compilation_roots = parsed.compilation_roots;
900
+ let mask = parsed.mask;
901
+
902
+ let mut options = match parsed.retriever {
903
+ Some(ret) => parsed.options.with_retriever(ret),
904
+ None => parsed.options,
905
+ };
906
+ if let Some(registry) = parsed.registry {
907
+ options = options.with_registry(registry);
908
+ }
909
+
910
+ match options.build_map(&json_schema) {
911
+ Ok(inner) => Ok(validator_map::ValidatorMap {
912
+ inner,
913
+ has_ruby_callbacks,
914
+ callback_roots,
915
+ compilation_roots,
916
+ mask,
917
+ }),
918
+ // Error handling mirrors build_validator: retriever_error_message → referencing_error → ArgumentError
919
+ Err(error) => {
920
+ if let jsonschema::error::ValidationErrorKind::Referencing(err) = error.kind() {
921
+ if let Some(message) = retriever_error_message(err) {
922
+ Err(Error::new(ruby.exception_arg_error(), message))
923
+ } else {
924
+ Err(referencing_error(ruby, err.to_string()))
925
+ }
926
+ } else {
927
+ Err(Error::new(ruby.exception_arg_error(), error.to_string()))
928
+ }
929
+ }
930
+ }
931
+ }
932
+
849
933
  fn bundle(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
850
934
  let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
851
935
  let (schema,) = parsed_args.required;
@@ -853,7 +937,11 @@ fn bundle(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
853
937
 
854
938
  let json_schema = to_schema_value(ruby, schema)?;
855
939
  let parsed = build_parsed_options(ruby, kw, None)?;
856
- match parsed.options.bundle(&json_schema) {
940
+ let mut options = parsed.options;
941
+ if let Some(registry) = parsed.registry {
942
+ options = options.with_registry(registry);
943
+ }
944
+ match options.bundle(&json_schema) {
857
945
  Ok(bundled) => ser::value_to_ruby(ruby, &bundled),
858
946
  Err(e @ jsonschema::ReferencingError::Unretrievable { .. }) => {
859
947
  Err(referencing_error(ruby, e.to_string()))
@@ -862,6 +950,29 @@ fn bundle(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
862
950
  }
863
951
  }
864
952
 
953
+ fn dereference(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
954
+ let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
955
+ let (schema,) = parsed_args.required;
956
+ let kw = extract_kwargs(ruby, parsed_args.keywords)?;
957
+
958
+ let json_schema = to_schema_value(ruby, schema)?;
959
+ let parsed = build_parsed_options(ruby, kw, None)?;
960
+ let mut options = match parsed.retriever {
961
+ Some(ret) => parsed.options.with_retriever(ret),
962
+ None => parsed.options,
963
+ };
964
+ if let Some(registry) = parsed.registry {
965
+ options = options.with_registry(registry);
966
+ }
967
+ match options.dereference(&json_schema) {
968
+ Ok(result) => ser::value_to_ruby(ruby, &result),
969
+ Err(e @ jsonschema::ReferencingError::Unretrievable { .. }) => {
970
+ Err(referencing_error(ruby, e.to_string()))
971
+ }
972
+ Err(e) => Err(Error::new(ruby.exception_runtime_error(), e.to_string())),
973
+ }
974
+ }
975
+
865
976
  /// to_string(object) -> String
866
977
  ///
867
978
  /// Serialize a Ruby value to canonical JSON.
@@ -888,6 +999,7 @@ fn is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
888
999
  } = build_validator(
889
1000
  ruby,
890
1001
  parsed.options,
1002
+ parsed.registry,
891
1003
  parsed.retriever,
892
1004
  parsed.callback_roots,
893
1005
  parsed.compilation_roots,
@@ -927,6 +1039,7 @@ fn validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
927
1039
  } = build_validator(
928
1040
  ruby,
929
1041
  parsed.options,
1042
+ parsed.registry,
930
1043
  parsed.retriever,
931
1044
  parsed.callback_roots,
932
1045
  parsed.compilation_roots,
@@ -978,6 +1091,7 @@ fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
978
1091
  } = build_validator(
979
1092
  ruby,
980
1093
  parsed.options,
1094
+ parsed.registry,
981
1095
  parsed.retriever,
982
1096
  parsed.callback_roots,
983
1097
  parsed.compilation_roots,
@@ -1073,6 +1187,7 @@ fn evaluate(ruby: &Ruby, args: &[Value]) -> Result<Evaluation, Error> {
1073
1187
  } = build_validator(
1074
1188
  ruby,
1075
1189
  parsed.options,
1190
+ parsed.registry,
1076
1191
  parsed.retriever,
1077
1192
  parsed.callback_roots,
1078
1193
  parsed.compilation_roots,
@@ -1126,6 +1241,7 @@ macro_rules! define_draft_validator {
1126
1241
  } = build_validator(
1127
1242
  ruby,
1128
1243
  parsed.options,
1244
+ parsed.registry,
1129
1245
  parsed.retriever,
1130
1246
  parsed.callback_roots,
1131
1247
  parsed.compilation_roots,
@@ -1201,9 +1317,9 @@ fn meta_is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
1201
1317
 
1202
1318
  let json_schema = to_schema_value(ruby, schema)?;
1203
1319
 
1204
- let result = if let Some(reg) = registry {
1320
+ let result = if let Some(registry) = registry {
1205
1321
  jsonschema::meta::options()
1206
- .with_registry(reg.inner.clone())
1322
+ .with_registry(registry.inner.as_ref())
1207
1323
  .validate(&json_schema)
1208
1324
  } else {
1209
1325
  jsonschema::meta::validate(&json_schema)
@@ -1230,9 +1346,9 @@ fn meta_validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1230
1346
 
1231
1347
  let json_schema = to_schema_value(ruby, schema)?;
1232
1348
 
1233
- let result = if let Some(reg) = registry {
1349
+ let result = if let Some(registry) = registry {
1234
1350
  jsonschema::meta::options()
1235
- .with_registry(reg.inner.clone())
1351
+ .with_registry(registry.inner.as_ref())
1236
1352
  .validate(&json_schema)
1237
1353
  } else {
1238
1354
  jsonschema::meta::validate(&json_schema)
@@ -1356,7 +1472,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1356
1472
  // Module-level functions
1357
1473
  module.define_singleton_method("validator_cls_for", function!(validator_cls_for, 1))?;
1358
1474
  module.define_singleton_method("validator_for", function!(validator_for, -1))?;
1475
+ module.define_singleton_method("validator_map_for", function!(validator_map_for, -1))?;
1359
1476
  module.define_singleton_method("bundle", function!(bundle, -1))?;
1477
+ module.define_singleton_method("dereference", function!(dereference, -1))?;
1360
1478
  module.define_singleton_method("valid?", function!(is_valid, -1))?;
1361
1479
  module.define_singleton_method("validate!", function!(validate, -1))?;
1362
1480
  module.define_singleton_method("each_error", function!(each_error, -1))?;
@@ -1427,10 +1545,17 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1427
1545
  // Internal implementation detail for shared validator behavior.
1428
1546
  let _: Value = module.funcall("private_constant", ("Validator",))?;
1429
1547
 
1548
+ module.const_set("Draft4", 4u8)?;
1549
+ module.const_set("Draft6", 6u8)?;
1550
+ module.const_set("Draft7", 7u8)?;
1551
+ module.const_set("Draft201909", 19u8)?;
1552
+ module.const_set("Draft202012", 20u8)?;
1553
+
1430
1554
  evaluation::define_class(ruby, &module)?;
1431
1555
  registry::define_class(ruby, &module)?;
1432
1556
  error_kind::define_class(ruby, &module)?;
1433
1557
  options::define_classes(ruby, &module)?;
1558
+ validator_map::define_class(ruby, &module)?;
1434
1559
 
1435
1560
  let meta_module = module.define_module("Meta")?;
1436
1561
  meta_module.define_singleton_method("valid?", function!(meta_is_valid, -1))?;
data/src/options.rs CHANGED
@@ -56,9 +56,10 @@ define_rb_intern!(static SYM_CALL: "call");
56
56
  define_rb_intern!(static SYM_NEW: "new");
57
57
  define_rb_intern!(static SYM_VALIDATE: "validate");
58
58
 
59
- pub struct ParsedOptions {
59
+ pub struct ParsedOptions<'i> {
60
60
  pub mask: Option<String>,
61
- pub options: jsonschema::ValidationOptions,
61
+ pub options: jsonschema::ValidationOptions<'i>,
62
+ pub registry: Option<&'i jsonschema::Registry<'static>>,
62
63
  pub retriever: Option<RubyRetriever>,
63
64
  // Runtime callbacks invoked during `validator.*` calls (formats / custom keywords).
64
65
  // Retriever callbacks are used at build time and do not affect GVL behavior at runtime.
@@ -420,8 +421,9 @@ pub fn make_options_from_kwargs(
420
421
  pattern_options_val: Option<Value>,
421
422
  email_options_val: Option<Value>,
422
423
  http_options_val: Option<Value>,
423
- ) -> Result<ParsedOptions, Error> {
424
+ ) -> Result<ParsedOptions<'_>, Error> {
424
425
  let mut opts = jsonschema::options();
426
+ let mut registry = None;
425
427
  let mut retriever = None;
426
428
  let retriever_was_provided = retriever_val.is_some();
427
429
  let mut has_ruby_callbacks = false;
@@ -473,7 +475,7 @@ pub fn make_options_from_kwargs(
473
475
  "registry must be a JSONSchema::Registry instance",
474
476
  )
475
477
  })?;
476
- opts = opts.with_registry(reg.inner.clone());
478
+ registry = Some(reg.inner.as_ref());
477
479
 
478
480
  if !retriever_was_provided && retriever.is_none() {
479
481
  if let Some(registry_retriever_value) = reg.retriever_value(ruby) {
@@ -763,6 +765,7 @@ pub fn make_options_from_kwargs(
763
765
  Ok(ParsedOptions {
764
766
  mask,
765
767
  options: opts,
768
+ registry,
766
769
  retriever,
767
770
  has_ruby_callbacks,
768
771
  callback_roots,