corvus_json_schema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +390 -0
  3. data/Cargo.toml +8 -0
  4. data/LICENSE +201 -0
  5. data/README.md +111 -0
  6. data/VERSIONHISTORY.md +7 -0
  7. data/ext/corvus_json_schema/Cargo.toml +18 -0
  8. data/ext/corvus_json_schema/extconf.rb +6 -0
  9. data/ext/corvus_json_schema/rustfmt.toml +2 -0
  10. data/ext/corvus_json_schema/src/lib.rs +643 -0
  11. data/ext/corvus_json_schema/vendor/corvus-json-schema/Cargo.toml +35 -0
  12. data/ext/corvus_json_schema/vendor/corvus-json-schema/LICENSE +201 -0
  13. data/ext/corvus_json_schema/vendor/corvus-json-schema/README.md +139 -0
  14. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/compiler.rs +1118 -0
  15. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/dialect.rs +108 -0
  16. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/document.rs +905 -0
  17. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan/fused.rs +1187 -0
  18. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan.rs +1728 -0
  19. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval.rs +1451 -0
  20. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/formats.rs +1021 -0
  21. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/instance.rs +228 -0
  22. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/lib.rs +189 -0
  23. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/loader.rs +403 -0
  24. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/metaschemas.rs +29 -0
  25. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/node.rs +417 -0
  26. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/numbers.rs +244 -0
  27. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/options.rs +108 -0
  28. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/pattern.rs +1591 -0
  29. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/results.rs +316 -0
  30. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/uri.rs +274 -0
  31. data/lib/corvus_json_schema/version.rb +5 -0
  32. data/lib/corvus_json_schema.rb +65 -0
  33. metadata +94 -0
@@ -0,0 +1,1451 @@
1
+ //! Evaluation: one implementation, monomorphised twice (like the C# `Evaluator.Eval<TMode>`). `Fast` has every
2
+ //! reporting call compiled away and fails fast at the first violation; `Collect` is exhaustive and reports every
3
+ //! keyword to a `JsonSchemaResultsCollector` with evaluation path, schema location and instance location, reproducing
4
+ //! the C# collecting mode (keyword order, paths, messages, which subschema results are committed or discarded).
5
+
6
+ use std::collections::HashMap;
7
+ use std::sync::OnceLock;
8
+
9
+ use serde_json::{Number, Value};
10
+
11
+ use crate::compiler::{AnnotationSource, collect_annotations};
12
+ use crate::dialect::Dialect;
13
+ use crate::formats::FormatKind;
14
+ use crate::instance::{ArrayView, Instance, Kind, ObjectView, View};
15
+ use crate::node::*;
16
+ use crate::numbers::{Num, cmp, multiple_of, num_eq, number_text};
17
+ use crate::options::FormatValidator;
18
+ use crate::results::{JsonSchemaResultsCollector, Message, encode_pointer_segment};
19
+ use crate::uri::resolve_pointer;
20
+
21
+ mod plan;
22
+
23
+ /// The compiled program an evaluator runs.
24
+ pub(crate) struct Program {
25
+ pub nodes: Vec<SchemaNode>,
26
+ pub root: NodeId,
27
+ pub uses_dynamic_scope: bool,
28
+ pub max_depth: u32,
29
+ pub formats: HashMap<String, FormatValidator>,
30
+ /// For fast mode: each node's target after following pure `$ref` hops.
31
+ pub fast_target: Vec<NodeId>,
32
+ /// Name lookup tables for nodes with many properties.
33
+ pub property_maps: Vec<Option<HashMap<String, NodeId>>>,
34
+ #[allow(clippy::vec_box)]
35
+ pub documents: Vec<Box<Value>>,
36
+ pub annotation_sources: Vec<Option<AnnotationSource>>,
37
+ pub annotations: OnceLock<Vec<Option<Vec<AnnotationEntry>>>>,
38
+ pub assert_format_set: bool,
39
+ /// Fail-fast plans (with a dynamic scope kept, bodies push their resource on entry).
40
+ plans: Vec<plan::Plan>,
41
+ }
42
+
43
+ const PROPERTY_MAP_THRESHOLD: usize = 8;
44
+
45
+ impl Program {
46
+ #[allow(clippy::too_many_arguments, clippy::vec_box)]
47
+ pub fn new(
48
+ nodes: Vec<SchemaNode>,
49
+ root: NodeId,
50
+ uses_dynamic_scope: bool,
51
+ max_depth: u32,
52
+ formats: HashMap<String, FormatValidator>,
53
+ documents: Vec<Box<Value>>,
54
+ annotation_sources: Vec<Option<AnnotationSource>>,
55
+ assert_format_set: bool,
56
+ ) -> Program {
57
+ let fast_target = (0..nodes.len() as NodeId)
58
+ .map(|id| {
59
+ let mut current = id;
60
+ for _ in 0..16 {
61
+ let n = &nodes[current as usize];
62
+ // Pure-$ref hops, and forwards: a node whose only assertion is one allOf branch (C#'s
63
+ // NodePlan.Forward), unless either end is on an in-place cycle, which keeps its guard.
64
+ let next = match pure_ref_target(n) {
65
+ Some(next) => next,
66
+ None => match forward_target(n) {
67
+ Some(next)
68
+ if !n.in_place_cycle && !nodes[next as usize].in_place_cycle && next != current =>
69
+ {
70
+ next
71
+ }
72
+ _ => break,
73
+ },
74
+ };
75
+ if uses_dynamic_scope && nodes[next as usize].resource_id != n.resource_id {
76
+ break;
77
+ }
78
+ current = next;
79
+ }
80
+ current
81
+ })
82
+ .collect();
83
+ let property_maps = nodes
84
+ .iter()
85
+ .map(|n| {
86
+ n.properties.as_ref().filter(|p| p.len() > PROPERTY_MAP_THRESHOLD).map(|p| p.iter().cloned().collect())
87
+ })
88
+ .collect();
89
+ let mut program = Program {
90
+ nodes,
91
+ root,
92
+ uses_dynamic_scope,
93
+ max_depth,
94
+ formats,
95
+ fast_target,
96
+ property_maps,
97
+ documents,
98
+ annotation_sources,
99
+ annotations: OnceLock::new(),
100
+ assert_format_set,
101
+ plans: Vec::new(),
102
+ };
103
+ program.plans = plan::compile_plans(&program);
104
+ program
105
+ }
106
+
107
+ /// The annotation keywords of every node (computed on the first evaluation with a collector).
108
+ pub fn annotations(&self) -> &[Option<Vec<AnnotationEntry>>] {
109
+ self.annotations.get_or_init(|| {
110
+ self.nodes
111
+ .iter()
112
+ .zip(&self.annotation_sources)
113
+ .map(|(n, src)| {
114
+ let src = src.as_ref()?;
115
+ let (value, _) = resolve_pointer(&self.documents[src.document as usize], &n.pointer)?;
116
+ let Value::Object(e) = value else { return None };
117
+ collect_annotations(e, n.dialect, src.vocab, src.content, self.assert_format_set)
118
+ })
119
+ .collect()
120
+ })
121
+ }
122
+
123
+ #[inline]
124
+ fn property(&self, id: NodeId, n: &SchemaNode, name: &str) -> Option<NodeId> {
125
+ match &self.property_maps[id as usize] {
126
+ Some(map) => map.get(name).copied(),
127
+ None => n.properties.as_ref()?.iter().find(|(k, _)| k == name).map(|&(_, c)| c),
128
+ }
129
+ }
130
+ }
131
+
132
+ /// The single reference of a node that is nothing but `$ref` (or a static `$dynamicRef`), for elision.
133
+ fn pure_ref_target(n: &SchemaNode) -> Option<NodeId> {
134
+ let refs = n.ref_.is_some() as u8 + n.static_dynamic_ref.is_some() as u8;
135
+ if refs != 1 || n.always_true || n.always_false {
136
+ return None;
137
+ }
138
+ if n.has_type
139
+ || n.const_value.is_some()
140
+ || n.enum_values.is_some()
141
+ || n.has_number_keywords()
142
+ || n.has_string_keywords()
143
+ || n.has_object_keywords()
144
+ || n.has_array_keywords()
145
+ || n.dynamic_ref.is_some()
146
+ || n.all_of.is_some()
147
+ || n.any_of.is_some()
148
+ || n.one_of.is_some()
149
+ || n.not.is_some()
150
+ || n.if_.is_some()
151
+ {
152
+ return None;
153
+ }
154
+ n.ref_.or(n.static_dynamic_ref)
155
+ }
156
+
157
+ /// The branch of a node that is nothing but a one-branch `allOf`, for fail-fast forwarding.
158
+ fn forward_target(n: &SchemaNode) -> Option<NodeId> {
159
+ let [only] = n.all_of.as_deref()? else { return None };
160
+ if n.always_true
161
+ || n.always_false
162
+ || n.ref_.is_some()
163
+ || n.static_dynamic_ref.is_some()
164
+ || n.has_type
165
+ || n.const_value.is_some()
166
+ || n.enum_values.is_some()
167
+ || n.has_number_keywords()
168
+ || n.has_string_keywords()
169
+ || n.has_object_keywords()
170
+ || n.has_array_keywords()
171
+ || n.dynamic_ref.is_some()
172
+ || n.any_of.is_some()
173
+ || n.one_of.is_some()
174
+ || n.not.is_some()
175
+ || n.if_.is_some()
176
+ {
177
+ return None;
178
+ }
179
+ Some(*only)
180
+ }
181
+
182
+ // Messages (Corvus.Text.Json Strings.resx).
183
+ const EVALUATED_SUBSCHEMA: &str = "The value was expected to match the subschema.";
184
+ const MATCHED_ALL: &str = "The value matched all subschema.";
185
+ const DID_NOT_MATCH_ALL: &str = "The value did not match all subschema.";
186
+ const MATCHED_AT_LEAST_ONE: &str = "The value matched at least one subschema.";
187
+ const DID_NOT_MATCH_AT_LEAST_ONE: &str = "The value did not match at least one subschema.";
188
+ const MATCHED_NO_SCHEMA: &str = "The instance matched no schema.";
189
+ const MATCHED_EXACTLY_ONE: &str = "The value matched exactly one subschema.";
190
+ const MATCHED_MORE_THAN_ONE: &str = "The instance matched more than one schema.";
191
+ const MATCHED_NOT: &str =
192
+ "The value matched the subschema in a not composition, which means the evaluation was not a match.";
193
+ const DID_NOT_MATCH_NOT: &str =
194
+ "The value did not match the subschema in a not composition, which means the evaluation was a match.";
195
+ const MATCHED_IF_FOR_THEN: &str = "The value matched the subschema in a binary or ternay if, which means the evaluation will go on to match the then subschema.";
196
+ const MATCHED_IF_FOR_ELSE: &str = "The value did not match the subschema in a ternary if, which means the evaluation will go on to match the else subschema.";
197
+ const MATCHED_THEN: &str = "The value matched the then subschema corresponding to a binary or ternary if.";
198
+ const DID_NOT_MATCH_THEN: &str = "The value did not match the then subschema corresponding to a binary or ternary if.";
199
+ const MATCHED_ELSE: &str = "The value matched the else subschema corresponding to a ternary if.";
200
+ const DID_NOT_MATCH_ELSE: &str = "The value did not match the else subschema corresponding to a ternary if.";
201
+ const UNIQUE_ITEMS: &str = "The array was expected to contain unique items.";
202
+ const PROPERTY_NAME_FAILED: &str = "The property name did not match the schema.";
203
+
204
+ /// `" 'v'"`, or nothing for an empty value (`JsonSchemaEvaluation.AppendSingleQuotedValue`).
205
+ fn q(v: &str) -> String {
206
+ if v.is_empty() { String::new() } else { format!(" '{v}'") }
207
+ }
208
+
209
+ const TYPE_ORDER: [(u8, &str); 7] = [
210
+ (type_mask::ARRAY, "array"),
211
+ (type_mask::OBJECT, "object"),
212
+ (type_mask::NULL, "null"),
213
+ (type_mask::BOOLEAN, "boolean"),
214
+ (type_mask::NUMBER, "number"),
215
+ (type_mask::INTEGER, "integer"),
216
+ (type_mask::STRING, "string"),
217
+ ];
218
+
219
+ fn type_message(mask: u8) -> String {
220
+ let names: Vec<&str> = TYPE_ORDER.iter().filter(|(m, _)| mask & m != 0).map(|(_, n)| *n).collect();
221
+ match names.len() {
222
+ 0 => String::new(),
223
+ 1 => format!("The value was expected to be of type '{}'", names[0]),
224
+ _ => format!(
225
+ "The value was expected to be of type '[{}]'",
226
+ names.iter().map(|n| format!("\"{n}\"")).collect::<Vec<_>>().join(", ")
227
+ ),
228
+ }
229
+ }
230
+
231
+ fn const_message(value: &Value) -> String {
232
+ match value {
233
+ Value::String(s) => format!("Expected the value to be the string{}", q(s)),
234
+ Value::Number(n) => format!("The value was expected to be equal to{}", q(&number_text(n))),
235
+ Value::Bool(b) => format!("Expected the value to be '{b}'"),
236
+ Value::Null => "Expected the value to be 'null'".to_string(),
237
+ _ => String::new(),
238
+ }
239
+ }
240
+
241
+ /// Whether a value is of one of the types in a mask.
242
+ #[inline]
243
+ pub(crate) fn matches_type<'a, I: Instance<'a>>(mask: u8, x: I) -> bool {
244
+ match x.kind() {
245
+ Kind::Null => mask & type_mask::NULL != 0,
246
+ Kind::Bool => mask & type_mask::BOOLEAN != 0,
247
+ Kind::Number => {
248
+ mask & type_mask::NUMBER != 0
249
+ || (mask & type_mask::INTEGER != 0 && matches!(x.view(), View::Number(n) if is_integer(&n)))
250
+ }
251
+ Kind::String => mask & type_mask::STRING != 0,
252
+ Kind::Array => mask & type_mask::ARRAY != 0,
253
+ Kind::Object => mask & type_mask::OBJECT != 0,
254
+ }
255
+ }
256
+
257
+ #[inline]
258
+ fn is_integer(n: &Number) -> bool {
259
+ n.is_i64() || n.is_u64() || n.as_f64().is_some_and(|f| f.is_finite() && f.fract() == 0.0)
260
+ }
261
+
262
+ /// JSON equality: numbers by value, objects by their property sets, arrays element-wise. The two values may be read
263
+ /// differently (an instance against a schema's constant).
264
+ pub(crate) fn json_equal<'a, 'b, A: Instance<'a>, B: Instance<'b>>(a: A, b: B) -> bool {
265
+ match (a.view(), b.view()) {
266
+ (View::Null, View::Null) => true,
267
+ (View::Bool(x), View::Bool(y)) => x == y,
268
+ (View::Number(x), View::Number(y)) => num_eq(&x, &y),
269
+ (View::String(x), View::String(y)) => x == y,
270
+ (View::Array(x), View::Array(y)) => x.len() == y.len() && x.iter().zip(y.iter()).all(|(p, q)| json_equal(p, q)),
271
+ (View::Object(x), View::Object(y)) => {
272
+ if x.len() != y.len() {
273
+ return false;
274
+ }
275
+ // Objects usually list their members in the same order: compare position by position, and look names up
276
+ // (a hash each) only from the first position where the names differ.
277
+ for (i, ((k, v), (l, w))) in x.iter().zip(y.iter()).enumerate() {
278
+ if k != l {
279
+ return x.iter().skip(i).all(|(k, v)| y.get(k).is_some_and(|w| json_equal(v, w)));
280
+ }
281
+ if !json_equal(v, w) {
282
+ return false;
283
+ }
284
+ }
285
+ true
286
+ }
287
+ _ => false,
288
+ }
289
+ }
290
+
291
+ /// A hash of a JSON value that agrees with JSON equality (object hashing is order-independent).
292
+ fn json_hash<'a, I: Instance<'a>>(v: I) -> u64 {
293
+ const K: u64 = 0x9e37_79b9_7f4a_7c15;
294
+ match v.view() {
295
+ View::Null => 0x53,
296
+ View::Bool(b) => 0x51 + b as u64,
297
+ View::Number(n) => match Num::of(&n) {
298
+ Num::I(i) => (i as u64).wrapping_mul(K) ^ 0x1234,
299
+ Num::F(f) => f.to_bits().wrapping_mul(K) ^ 0x4321,
300
+ },
301
+ View::String(s) => str_hash(s),
302
+ View::Array(a) => a.iter().fold(0x54 + a.len() as u64, |h, x| h.wrapping_mul(31).wrapping_add(json_hash(x))),
303
+ View::Object(o) => {
304
+ // Equal objects have the same member values, so a sum of the values' hashes (whatever the order) agrees with
305
+ // equality; hashing the names too would cost more than the collisions it saves.
306
+ o.iter().fold(0x55 + o.len() as u64, |h, (_, x)| h.wrapping_add(json_hash(x).wrapping_mul(0x2c1b_3c6d)))
307
+ }
308
+ }
309
+ }
310
+
311
+ /// A string hash, eight bytes at a time.
312
+ fn str_hash(s: &str) -> u64 {
313
+ const K: u64 = 0x9e37_79b9_7f4a_7c15;
314
+ let b = s.as_bytes();
315
+ let mut h = (b.len() as u64).wrapping_mul(K);
316
+ let mut chunks = b.chunks_exact(8);
317
+ for c in &mut chunks {
318
+ h = (h.rotate_left(5) ^ u64::from_le_bytes(c.try_into().unwrap())).wrapping_mul(K);
319
+ }
320
+ // The tail as one word without copying it out: the last eight bytes when there are that many (overlapping the
321
+ // chunks already hashed), else the overlapping first and last four, else the bytes themselves.
322
+ let n = b.len();
323
+ let tail = if n >= 8 {
324
+ u64::from_le_bytes(b[n - 8..].try_into().unwrap())
325
+ } else if n >= 4 {
326
+ u32::from_le_bytes(b[..4].try_into().unwrap()) as u64
327
+ | (u32::from_le_bytes(b[n - 4..].try_into().unwrap()) as u64) << 32
328
+ } else {
329
+ b.iter().fold(0, |w, &c| w << 8 | c as u64)
330
+ };
331
+ (h.rotate_left(5) ^ tail).wrapping_mul(K)
332
+ }
333
+
334
+ /// `uniqueItems`: pairwise for short arrays, by hash + equality otherwise.
335
+ pub(crate) fn all_unique<'a, A: ArrayView<'a>>(a: A) -> bool {
336
+ let n = a.len();
337
+ if n < 2 {
338
+ return true;
339
+ }
340
+ // Arrays of strings (the common case: lists of names) compare by length first, without hashing or allocating.
341
+ if n <= 32 && a.iter().all(|x| x.kind() == Kind::String) {
342
+ let s = |i: usize| match a.get(i).view() {
343
+ View::String(t) => t,
344
+ _ => unreachable!("every item is a string"),
345
+ };
346
+ for i in 1..n {
347
+ for j in 0..i {
348
+ let (x, y) = (s(i), s(j));
349
+ if x.len() == y.len() && x == y {
350
+ return false;
351
+ }
352
+ }
353
+ }
354
+ return true;
355
+ }
356
+ if n <= 16 {
357
+ for i in 1..n {
358
+ for j in 0..i {
359
+ if json_equal(a.get(i), a.get(j)) {
360
+ return false;
361
+ }
362
+ }
363
+ }
364
+ return true;
365
+ }
366
+ // Sort by hash: equal values have equal hashes, so only runs of equal hashes need comparing.
367
+ let mut hashed: Vec<(u64, u32)> = a.iter().enumerate().map(|(i, x)| (json_hash(x), i as u32)).collect();
368
+ hashed.sort_unstable();
369
+ let mut start = 0;
370
+ for end in 1..=n {
371
+ if end == n || hashed[end].0 != hashed[start].0 {
372
+ for i in start + 1..end {
373
+ for j in start..i {
374
+ if json_equal(a.get(hashed[i].1 as usize), a.get(hashed[j].1 as usize)) {
375
+ return false;
376
+ }
377
+ }
378
+ }
379
+ start = end;
380
+ }
381
+ }
382
+ true
383
+ }
384
+
385
+ /// The length of a string in code points (what `minLength`/`maxLength` count).
386
+ #[inline]
387
+ fn code_points(s: &str) -> u64 {
388
+ if s.is_ascii() { s.len() as u64 } else { s.chars().count() as u64 }
389
+ }
390
+
391
+ fn base64_decode(s: &str) -> Option<Vec<u8>> {
392
+ let b = s.as_bytes();
393
+ if b.len() % 4 != 0 {
394
+ return None;
395
+ }
396
+ let val = |c: u8| -> Option<u32> {
397
+ Some(match c {
398
+ b'A'..=b'Z' => (c - b'A') as u32,
399
+ b'a'..=b'z' => (c - b'a' + 26) as u32,
400
+ b'0'..=b'9' => (c - b'0' + 52) as u32,
401
+ b'+' => 62,
402
+ b'/' => 63,
403
+ _ => return None,
404
+ })
405
+ };
406
+ let mut out = Vec::with_capacity(b.len() / 4 * 3);
407
+ for (ci, chunk) in b.chunks(4).enumerate() {
408
+ let last = ci == b.len() / 4 - 1;
409
+ let pad = chunk.iter().rev().take_while(|&&c| c == b'=').count();
410
+ if pad > 2 || (pad > 0 && !last) {
411
+ return None;
412
+ }
413
+ let mut acc = 0u32;
414
+ for &c in &chunk[..4 - pad] {
415
+ acc = (acc << 6) | val(c)?;
416
+ }
417
+ acc <<= 6 * pad as u32;
418
+ let bytes = [(acc >> 16) as u8, (acc >> 8) as u8, acc as u8];
419
+ out.extend_from_slice(&bytes[..3 - pad]);
420
+ }
421
+ Some(out)
422
+ }
423
+
424
+ /// Draft 7 content assertion.
425
+ fn content_ok(s: &str, kind: ContentKind) -> bool {
426
+ match kind {
427
+ ContentKind::Base64 => base64_decode(s).is_some(),
428
+ ContentKind::Json => serde_json::from_str::<Value>(s).is_ok(),
429
+ ContentKind::Base64Json => base64_decode(s).is_some_and(|d| serde_json::from_slice::<Value>(&d).is_ok()),
430
+ ContentKind::None => true,
431
+ }
432
+ }
433
+
434
+ // --------------------------------------------------------------------------------------------------------------------
435
+ // Evaluated properties/items
436
+
437
+ /// Evaluated-property (by index in the instance object) or evaluated-item bits.
438
+ pub(crate) enum Bits {
439
+ /// Up to 256 properties or items, without an allocation.
440
+ Inline([u64; 4]),
441
+ Heap(Vec<u64>),
442
+ }
443
+
444
+ impl Bits {
445
+ fn new(len: usize) -> Bits {
446
+ if len <= 256 { Bits::Inline([0; 4]) } else { Bits::Heap(vec![0; len.div_ceil(64)]) }
447
+ }
448
+
449
+ #[inline]
450
+ fn words(&self) -> &[u64] {
451
+ match self {
452
+ Bits::Inline(w) => w,
453
+ Bits::Heap(w) => w,
454
+ }
455
+ }
456
+
457
+ #[inline]
458
+ fn words_mut(&mut self) -> &mut [u64] {
459
+ match self {
460
+ Bits::Inline(w) => w,
461
+ Bits::Heap(w) => w,
462
+ }
463
+ }
464
+
465
+ #[inline]
466
+ fn set(&mut self, i: usize) {
467
+ self.words_mut()[i >> 6] |= 1 << (i & 63);
468
+ }
469
+
470
+ #[inline]
471
+ fn get(&self, i: usize) -> bool {
472
+ self.words()[i >> 6] & (1 << (i & 63)) != 0
473
+ }
474
+
475
+ fn merge(&mut self, other: &Bits) {
476
+ for (a, b) in self.words_mut().iter_mut().zip(other.words()) {
477
+ *a |= b;
478
+ }
479
+ }
480
+ }
481
+
482
+ fn container_len<'a, I: Instance<'a>>(x: I) -> usize {
483
+ match x.view() {
484
+ View::Object(o) => o.len(),
485
+ View::Array(a) => a.len(),
486
+ _ => 0,
487
+ }
488
+ }
489
+
490
+ #[inline(always)]
491
+ fn is_object<'a, I: Instance<'a>>(x: I) -> bool {
492
+ x.kind() == Kind::Object
493
+ }
494
+
495
+ #[inline(always)]
496
+ fn is_array<'a, I: Instance<'a>>(x: I) -> bool {
497
+ x.kind() == Kind::Array
498
+ }
499
+
500
+ // --------------------------------------------------------------------------------------------------------------------
501
+ // The evaluator
502
+
503
+ /// The oneOf/anyOf branches a discriminator leaves as candidates for an instance.
504
+ enum Selection<'a> {
505
+ All,
506
+ None,
507
+ Subset(&'a [u32]),
508
+ }
509
+
510
+ impl Selection<'_> {
511
+ fn indexes(&self, len: usize) -> impl Iterator<Item = usize> + '_ {
512
+ let (range, subset) = match self {
513
+ Selection::All => (0..len, None),
514
+ Selection::None => (0..0, None),
515
+ Selection::Subset(s) => (0..0, Some(s.iter().map(|&i| i as usize))),
516
+ };
517
+ range.chain(subset.into_iter().flatten())
518
+ }
519
+ }
520
+
521
+ fn select<'a, 'x, I: Instance<'x>>(d: Option<&'a Discriminator>, x: I) -> Selection<'a> {
522
+ let (Some(d), View::Object(o)) = (d, x.view()) else { return Selection::All };
523
+ let Some(value) = o.get(&d.property) else {
524
+ return if d.all_require { Selection::None } else { Selection::All };
525
+ };
526
+ let hit = d.known.iter().find(|(k, _)| k.matches(value));
527
+ Selection::Subset(hit.map_or(&d.unknown, |(_, branches)| branches))
528
+ }
529
+
530
+ pub(crate) trait Mode {
531
+ const COLLECT: bool;
532
+ }
533
+
534
+ pub(crate) struct Fast;
535
+ impl Mode for Fast {
536
+ const COLLECT: bool = false;
537
+ }
538
+
539
+ pub(crate) struct Collect;
540
+ impl Mode for Collect {
541
+ const COLLECT: bool = true;
542
+ }
543
+
544
+ pub(crate) struct Evaluator<'p, 'c> {
545
+ p: &'p Program,
546
+ annotations: &'p [Option<Vec<AnnotationEntry>>],
547
+ c: Option<&'c mut JsonSchemaResultsCollector>,
548
+ scope: Vec<u32>,
549
+ depth: u32,
550
+ pub depth_exceeded: bool,
551
+ /// A string reused to evaluate property names against propertyNames (a `String`, not a `Value`: dropping a
552
+ /// `Value` is an out-of-line call, which every evaluation paid).
553
+ name_buffer: String,
554
+ }
555
+
556
+ /// Records a keyword result: in collecting mode reports it and accumulates `ok`; in fast mode returns on failure.
557
+ macro_rules! check {
558
+ ($self:ident, $M:ty, $ok:ident, $m:expr, $msg:expr, $kw:expr) => {{
559
+ let m: bool = $m;
560
+ if <$M>::COLLECT {
561
+ $self.col().evaluated_keyword(m, $msg, $kw);
562
+ $ok &= m;
563
+ } else if !m {
564
+ return false;
565
+ }
566
+ }};
567
+ }
568
+
569
+ /// Accumulates a sub-result: in fast mode returns on failure.
570
+ macro_rules! and {
571
+ ($M:ty, $ok:ident, $m:expr) => {{
572
+ let m: bool = $m;
573
+ if <$M>::COLLECT {
574
+ $ok &= m;
575
+ } else if !m {
576
+ return false;
577
+ }
578
+ }};
579
+ }
580
+
581
+ impl<'p, 'c> Evaluator<'p, 'c> {
582
+ pub fn new(p: &'p Program, c: Option<&'c mut JsonSchemaResultsCollector>) -> Self {
583
+ let annotations: &'p [Option<Vec<AnnotationEntry>>] = if c.is_some() { p.annotations() } else { &[] };
584
+ Evaluator { p, annotations, c, scope: Vec::new(), depth: 0, depth_exceeded: false, name_buffer: String::new() }
585
+ }
586
+
587
+ #[inline(always)]
588
+ fn col(&mut self) -> &mut JsonSchemaResultsCollector {
589
+ self.c.as_deref_mut().unwrap()
590
+ }
591
+
592
+ #[inline(always)]
593
+ fn node(&self, id: NodeId) -> &'p SchemaNode {
594
+ &self.p.nodes[id as usize]
595
+ }
596
+
597
+ /// Evaluates the program's entry in fast mode.
598
+ pub fn validate<'x, I: Instance<'x>>(&mut self, x: I) -> bool {
599
+ let root = self.p.fast_target[self.p.root as usize];
600
+ self.fast(root, x)
601
+ }
602
+
603
+ /// Fail-fast evaluation of a node, through its plan when there are plans.
604
+ #[inline]
605
+ fn fast<'x, I: Instance<'x>>(&mut self, id: NodeId, x: I) -> bool {
606
+ if self.p.plans.is_empty() { self.eval_node::<Fast, I>(id, x, None) } else { self.run(id, x) }
607
+ }
608
+
609
+ /// Evaluates the program's entry, reporting to the collector.
610
+ pub fn evaluate<'x, I: Instance<'x>>(&mut self, x: I) -> bool {
611
+ // A root that is nothing but a $ref reports against its target, with no $ref in the evaluation path.
612
+ let (root, _) = self.resolve(self.p.root);
613
+ let pointer = &self.node(root).pointer;
614
+ self.col().begin_child_context(None, Some(pointer), None);
615
+ let ok = self.eval_node::<Collect, I>(root, x, None);
616
+ self.col().commit_child_context(false, ok, Message::Static(EVALUATED_SUBSCHEMA));
617
+ ok
618
+ }
619
+
620
+ /// The single reference of a pure-$ref node, for collecting mode (a node with annotations is not elided).
621
+ fn collect_pure_ref(&self, id: NodeId) -> Option<NodeId> {
622
+ if self.annotations.get(id as usize).is_some_and(Option::is_some) {
623
+ return None;
624
+ }
625
+ pure_ref_target(self.node(id))
626
+ }
627
+
628
+ /// Follows pure-reference hops (at most 16; not across resources when a dynamic scope is kept), returning the
629
+ /// target and the evaluation-path suffix of the hops (`/$ref`, `/$dynamicRef`, `/$recursiveRef`).
630
+ fn resolve(&self, id: NodeId) -> (NodeId, String) {
631
+ let mut current = id;
632
+ let mut suffix = String::new();
633
+ for _ in 0..16 {
634
+ let n = self.node(current);
635
+ let Some(next) = self.collect_pure_ref(current) else { break };
636
+ if self.p.uses_dynamic_scope && self.node(next).resource_id != n.resource_id {
637
+ break;
638
+ }
639
+ suffix.push_str(if n.ref_.is_some() { "/$ref" } else { "/" });
640
+ if n.ref_.is_none() {
641
+ suffix.push_str(n.static_dynamic_keyword.name());
642
+ }
643
+ current = next;
644
+ }
645
+ (current, suffix)
646
+ }
647
+
648
+ fn eval_node<'x, M: Mode, I: Instance<'x>>(&mut self, id: NodeId, x: I, bits: Option<&mut Bits>) -> bool {
649
+ let n = self.node(id);
650
+ if n.always_true || n.always_false {
651
+ if M::COLLECT {
652
+ self.col().evaluated_boolean_schema(n.always_true);
653
+ }
654
+ return n.always_true;
655
+ }
656
+ let pushed = self.p.uses_dynamic_scope && self.scope.last() != Some(&n.resource_id);
657
+ if pushed {
658
+ self.scope.push(n.resource_id);
659
+ }
660
+ let needs_own = bits.is_none()
661
+ && ((n.unevaluated_properties.is_some() && is_object(x)) || (n.unevaluated_items.is_some() && is_array(x)));
662
+ let ok = if needs_own {
663
+ let mut own = Bits::new(container_len(x));
664
+ self.eval_core::<M, I>(id, n, x, Some(&mut own))
665
+ } else {
666
+ self.eval_core::<M, I>(id, n, x, bits)
667
+ };
668
+ if pushed {
669
+ self.scope.pop();
670
+ }
671
+ ok
672
+ }
673
+
674
+ fn eval_core<'x, M: Mode, I: Instance<'x>>(
675
+ &mut self,
676
+ id: NodeId,
677
+ n: &'p SchemaNode,
678
+ x: I,
679
+ mut bits: Option<&mut Bits>,
680
+ ) -> bool {
681
+ let mut ok = true;
682
+ if n.has_type {
683
+ check!(self, M, ok, matches_type(n.type_mask, x), Message::Lazy(&|| type_message(n.type_mask)), "type");
684
+ }
685
+ if let Some(c) = &n.const_value {
686
+ check!(self, M, ok, json_equal(x, c), Message::Lazy(&|| const_message(c)), "const");
687
+ }
688
+ if let Some(values) = &n.enum_values {
689
+ let m = values.iter().any(|v| json_equal(x, v));
690
+ check!(
691
+ self,
692
+ M,
693
+ ok,
694
+ m,
695
+ Message::Static(if m { MATCHED_AT_LEAST_ONE } else { DID_NOT_MATCH_AT_LEAST_ONE }),
696
+ "enum"
697
+ );
698
+ }
699
+ match x.view() {
700
+ View::Number(num) if n.has_number_keywords() => {
701
+ and!(M, ok, self.eval_number::<M>(n, &num));
702
+ }
703
+ View::String(s) if n.has_string_keywords() => {
704
+ and!(M, ok, self.eval_string::<M>(n, s));
705
+ }
706
+ View::Object(o) if n.has_object_keywords() => {
707
+ and!(M, ok, self.eval_object::<M, I>(id, n, o, x, bits.as_deref_mut()));
708
+ }
709
+ View::Array(a) if n.has_array_keywords() => {
710
+ and!(M, ok, self.eval_array::<M, I>(n, a, bits.as_deref_mut()));
711
+ }
712
+ _ => {}
713
+ }
714
+ and!(M, ok, self.eval_in_place::<M, I>(n, x, bits.as_deref_mut()));
715
+ match x.view() {
716
+ View::Object(o) if n.unevaluated_properties.is_some() => {
717
+ and!(M, ok, self.eval_unevaluated_properties::<M, I>(n, o, bits.as_deref_mut().unwrap()));
718
+ }
719
+ View::Array(a) if n.unevaluated_items.is_some() => {
720
+ and!(M, ok, self.eval_unevaluated_items::<M, I>(n, a, bits.unwrap()));
721
+ }
722
+ _ => {}
723
+ }
724
+ if M::COLLECT {
725
+ if let Some(list) = &self.annotations[id as usize] {
726
+ for a in list {
727
+ if a.strings_only && x.kind() != Kind::String {
728
+ continue;
729
+ }
730
+ let v = &a.value;
731
+ self.col().ignored_keyword(Message::Lazy(&|| v.to_string()), &a.keyword);
732
+ }
733
+ }
734
+ }
735
+ ok
736
+ }
737
+
738
+ // ----------------------------------------------------------------------------------------------------------------
739
+ // Numbers and strings
740
+
741
+ fn eval_number<M: Mode>(&mut self, n: &'p SchemaNode, x: &Number) -> bool {
742
+ let mut ok = true;
743
+ if n.assert_format && n.format_kind.is_numeric() {
744
+ if let Some(f) = &n.format {
745
+ let m = match self.p.formats.get(f) {
746
+ Some(custom) => custom(&x.to_string()),
747
+ None => n.format_kind.check_number(x),
748
+ };
749
+ let kind = n.format_kind.name();
750
+ check!(
751
+ self,
752
+ M,
753
+ ok,
754
+ m,
755
+ Message::Lazy(&|| format!(
756
+ "The value was expected to be in a supported format, and within bounds for '{kind}'"
757
+ )),
758
+ "format"
759
+ );
760
+ }
761
+ }
762
+ let xv = Num::of(x);
763
+ use std::cmp::Ordering::*;
764
+ if let Some(b) = &n.minimum {
765
+ let m = cmp(xv, Num::of(b)) != Less;
766
+ check!(
767
+ self,
768
+ M,
769
+ ok,
770
+ m,
771
+ Message::Lazy(&|| format!(
772
+ "The value was expected to be greater than or equal to{}",
773
+ q(&number_text(b))
774
+ )),
775
+ "minimum"
776
+ );
777
+ }
778
+ if let Some(b) = &n.maximum {
779
+ let m = cmp(xv, Num::of(b)) != Greater;
780
+ check!(
781
+ self,
782
+ M,
783
+ ok,
784
+ m,
785
+ Message::Lazy(&|| format!("The value was expected to be less than or equal to{}", q(&number_text(b)))),
786
+ "maximum"
787
+ );
788
+ }
789
+ if let Some(b) = &n.exclusive_minimum {
790
+ let m = cmp(xv, Num::of(b)) == Greater;
791
+ check!(
792
+ self,
793
+ M,
794
+ ok,
795
+ m,
796
+ Message::Lazy(&|| format!("The value was expected to be greater than{}", q(&number_text(b)))),
797
+ "exclusiveMinimum"
798
+ );
799
+ }
800
+ if let Some(b) = &n.exclusive_maximum {
801
+ let m = cmp(xv, Num::of(b)) == Less;
802
+ check!(
803
+ self,
804
+ M,
805
+ ok,
806
+ m,
807
+ Message::Lazy(&|| format!("The value was expected to be less than{}", q(&number_text(b)))),
808
+ "exclusiveMaximum"
809
+ );
810
+ }
811
+ if let Some(d) = &n.multiple_of {
812
+ let m = multiple_of(x, d);
813
+ check!(
814
+ self,
815
+ M,
816
+ ok,
817
+ m,
818
+ Message::Lazy(&|| format!("The value was expected to be a multiple of{}", q(&number_text(d)))),
819
+ "multipleOf"
820
+ );
821
+ }
822
+ ok
823
+ }
824
+
825
+ fn eval_string<M: Mode>(&mut self, n: &'p SchemaNode, s: &str) -> bool {
826
+ let mut ok = true;
827
+ if n.min_length.is_some() || n.max_length.is_some() {
828
+ let len = code_points(s);
829
+ if let Some(min) = n.min_length {
830
+ check!(
831
+ self,
832
+ M,
833
+ ok,
834
+ len >= min,
835
+ Message::Lazy(&|| format!(
836
+ "Expected the length of the value to be greater than or equal to '{min}'"
837
+ )),
838
+ "minLength"
839
+ );
840
+ }
841
+ if let Some(max) = n.max_length {
842
+ check!(
843
+ self,
844
+ M,
845
+ ok,
846
+ len <= max,
847
+ Message::Lazy(&|| format!("Expected the length of the value to be less than or equal to '{max}'")),
848
+ "maxLength"
849
+ );
850
+ }
851
+ }
852
+ if let Some(p) = &n.pattern {
853
+ check!(
854
+ self,
855
+ M,
856
+ ok,
857
+ p.is_match(s),
858
+ Message::Lazy(&|| format!("Expected the value to match the regular expression{}", q(&p.source))),
859
+ "pattern"
860
+ );
861
+ }
862
+ if n.assert_format && !n.format_kind.is_numeric() {
863
+ if let Some(f) = &n.format {
864
+ let (m, message): (bool, Message) = match self.p.formats.get(f) {
865
+ Some(custom) => (custom(s), Message::Lazy(&|| format!("Expected a string in the '{f}' format."))),
866
+ None if n.format_kind == FormatKind::Unknown => (true, Message::None),
867
+ None => (
868
+ n.format_kind.check_string(s, n.dialect <= Dialect::Draft6),
869
+ n.format_kind.message().map_or(Message::None, Message::Static),
870
+ ),
871
+ };
872
+ check!(self, M, ok, m, message, "format");
873
+ }
874
+ }
875
+ if n.assert_content {
876
+ let m = content_ok(s, n.content);
877
+ let (message, keyword) = match n.content {
878
+ ContentKind::Base64 => ("Expected a valid Base64-encoded string.", "contentEncoding"),
879
+ ContentKind::Json => ("Expected valid JSON content.", "contentMediaType"),
880
+ _ => ("Expected valid Base64-encoded JSON content.", "contentMediaType"),
881
+ };
882
+ check!(self, M, ok, m, Message::Static(message), keyword);
883
+ }
884
+ ok
885
+ }
886
+
887
+ // ----------------------------------------------------------------------------------------------------------------
888
+ // Objects
889
+
890
+ /// A child application at a new instance location (a property value or an array item).
891
+ #[inline]
892
+ fn eval_at<'x, M: Mode, I: Instance<'x>>(
893
+ &mut self,
894
+ child: NodeId,
895
+ path: &dyn Fn() -> String,
896
+ value: I,
897
+ doc_segment: &dyn Fn() -> String,
898
+ ) -> bool {
899
+ if !M::COLLECT {
900
+ return self.fast(self.p.fast_target[child as usize], value);
901
+ }
902
+ let (target, suffix) = self.resolve(child);
903
+ let pointer = &self.node(target).pointer;
904
+ let eval_segment = format!("{}{}", path(), suffix);
905
+ let doc = doc_segment();
906
+ self.col().begin_child_context(Some(&eval_segment), Some(pointer), Some(&doc));
907
+ let ok = self.eval_node::<Collect, I>(target, value, None);
908
+ self.col().commit_child_context(ok, ok, Message::Static(EVALUATED_SUBSCHEMA));
909
+ ok
910
+ }
911
+
912
+ fn eval_object<'x, M: Mode, I: Instance<'x>>(
913
+ &mut self,
914
+ id: NodeId,
915
+ n: &'p SchemaNode,
916
+ o: I::Object,
917
+ _x: I,
918
+ mut bits: Option<&mut Bits>,
919
+ ) -> bool {
920
+ let mut ok = true;
921
+ let len = o.len() as u64;
922
+ if let Some(min) = n.min_properties {
923
+ check!(
924
+ self,
925
+ M,
926
+ ok,
927
+ len >= min,
928
+ Message::Lazy(&|| format!("Expected the property count to be greater than or equal to '{min}'")),
929
+ "minProperties"
930
+ );
931
+ }
932
+ if let Some(max) = n.max_properties {
933
+ check!(
934
+ self,
935
+ M,
936
+ ok,
937
+ len <= max,
938
+ Message::Lazy(&|| format!("Expected the property count to be less than or equal to '{max}'")),
939
+ "maxProperties"
940
+ );
941
+ }
942
+ if n.properties.is_some()
943
+ || n.pattern_properties.is_some()
944
+ || n.additional_properties.is_some()
945
+ || n.property_names.is_some()
946
+ {
947
+ for (i, (k, v)) in o.iter().enumerate() {
948
+ let mut matched = false;
949
+ if let Some(p) = self.p.property(id, n, k) {
950
+ matched = true;
951
+ if let Some(b) = bits.as_deref_mut() {
952
+ b.set(i);
953
+ }
954
+ and!(
955
+ M,
956
+ ok,
957
+ self.eval_at::<M, I>(p, &|| format!("properties/{}", encode_pointer_segment(k)), v, &|| {
958
+ encode_pointer_segment(k).into_owned()
959
+ })
960
+ );
961
+ }
962
+ if let Some(pps) = &n.pattern_properties {
963
+ for pp in pps {
964
+ if !pp.pattern.is_match(k) {
965
+ continue;
966
+ }
967
+ matched = true;
968
+ if let Some(b) = bits.as_deref_mut() {
969
+ b.set(i);
970
+ }
971
+ let source = &pp.pattern.source;
972
+ and!(
973
+ M,
974
+ ok,
975
+ self.eval_at::<M, I>(
976
+ pp.node,
977
+ &|| format!("patternProperties/{}", encode_pointer_segment(source)),
978
+ v,
979
+ &|| encode_pointer_segment(k).into_owned()
980
+ )
981
+ );
982
+ }
983
+ }
984
+ if let Some(ap) = n.additional_properties {
985
+ if !matched {
986
+ if let Some(b) = bits.as_deref_mut() {
987
+ b.set(i);
988
+ }
989
+ and!(
990
+ M,
991
+ ok,
992
+ self.eval_at::<M, I>(ap, &|| "additionalProperties".to_string(), v, &|| {
993
+ encode_pointer_segment(k).into_owned()
994
+ })
995
+ );
996
+ }
997
+ }
998
+ if let Some(pn) = n.property_names {
999
+ let name = Value::String(k.to_string());
1000
+ if M::COLLECT {
1001
+ // Not elided; the document path stays the object's.
1002
+ let pointer = &self.node(pn).pointer;
1003
+ self.col().begin_child_context(Some("propertyNames"), Some(pointer), None);
1004
+ let m = self.eval_node::<Collect, &Value>(pn, &name, None);
1005
+ self.col().commit_child_context(m, m, Message::Static(EVALUATED_SUBSCHEMA));
1006
+ if !m {
1007
+ self.col().evaluated_keyword(false, Message::Static(PROPERTY_NAME_FAILED), "propertyNames");
1008
+ ok = false;
1009
+ }
1010
+ } else if !self.fast(self.p.fast_target[pn as usize], &name) {
1011
+ return false;
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+ if let Some(required) = &n.required_list {
1017
+ for r in required {
1018
+ let present = o.contains_key(r);
1019
+ if M::COLLECT {
1020
+ self.col().evaluated_keyword_for_property(
1021
+ present,
1022
+ Message::Lazy(&|| {
1023
+ format!("Required property {}present '{r}'", if present { "" } else { "not " })
1024
+ }),
1025
+ r,
1026
+ "required",
1027
+ );
1028
+ ok &= present;
1029
+ } else if !present {
1030
+ return false;
1031
+ }
1032
+ }
1033
+ }
1034
+ if let Some(deps) = &n.dependencies {
1035
+ // Rows are reported under the keyword the schema used (dependencies, dependentRequired, dependentSchemas).
1036
+ for d in deps {
1037
+ if !o.contains_key(&d.name) {
1038
+ continue;
1039
+ }
1040
+ let keyword = d.keyword.name();
1041
+ if let Some(required) = &d.required {
1042
+ for r in required {
1043
+ let present = o.contains_key(r);
1044
+ if M::COLLECT {
1045
+ self.col().evaluated_keyword_for_property(
1046
+ present,
1047
+ Message::Lazy(&|| {
1048
+ format!("Required property {}present '{r}'", if present { "" } else { "not " })
1049
+ }),
1050
+ r,
1051
+ keyword,
1052
+ );
1053
+ ok &= present;
1054
+ } else if !present {
1055
+ return false;
1056
+ }
1057
+ }
1058
+ }
1059
+ if let Some(schema) = d.schema {
1060
+ let name = &d.name;
1061
+ let (m, _) = self.eval_in_place_child::<M, I>(
1062
+ schema,
1063
+ &|| format!("{keyword}/{}", encode_pointer_segment(name)),
1064
+ _x,
1065
+ bits.as_deref_mut(),
1066
+ true,
1067
+ true,
1068
+ );
1069
+ if M::COLLECT {
1070
+ self.col().evaluated_keyword_for_property(
1071
+ m,
1072
+ Message::Lazy(&|| {
1073
+ format!(
1074
+ "The value did match the schema applied because it contained the property '{name}'"
1075
+ )
1076
+ }),
1077
+ name,
1078
+ keyword,
1079
+ );
1080
+ ok &= m;
1081
+ } else if !m {
1082
+ return false;
1083
+ }
1084
+ }
1085
+ }
1086
+ }
1087
+ ok
1088
+ }
1089
+
1090
+ fn eval_unevaluated_properties<'x, M: Mode, I: Instance<'x>>(
1091
+ &mut self,
1092
+ n: &'p SchemaNode,
1093
+ o: I::Object,
1094
+ bits: &mut Bits,
1095
+ ) -> bool {
1096
+ let child = n.unevaluated_properties.unwrap();
1097
+ let mut ok = true;
1098
+ for (i, (k, v)) in o.iter().enumerate() {
1099
+ if bits.get(i) {
1100
+ continue;
1101
+ }
1102
+ bits.set(i);
1103
+ and!(
1104
+ M,
1105
+ ok,
1106
+ self.eval_at::<M, I>(child, &|| "unevaluatedProperties".to_string(), v, &|| encode_pointer_segment(k)
1107
+ .into_owned())
1108
+ );
1109
+ }
1110
+ if M::COLLECT {
1111
+ self.col().evaluated_keyword(ok, Message::None, "unevaluatedProperties");
1112
+ }
1113
+ ok
1114
+ }
1115
+
1116
+ // ----------------------------------------------------------------------------------------------------------------
1117
+ // Arrays
1118
+
1119
+ fn eval_array<'x, M: Mode, I: Instance<'x>>(
1120
+ &mut self,
1121
+ n: &'p SchemaNode,
1122
+ a: I::Array,
1123
+ mut bits: Option<&mut Bits>,
1124
+ ) -> bool {
1125
+ let mut ok = true;
1126
+ let len = a.len() as u64;
1127
+ if let Some(min) = n.min_items {
1128
+ check!(
1129
+ self,
1130
+ M,
1131
+ ok,
1132
+ len >= min,
1133
+ Message::Lazy(&|| format!("Expected the item count to be greater than or equal to '{min}'")),
1134
+ "minItems"
1135
+ );
1136
+ }
1137
+ if let Some(max) = n.max_items {
1138
+ check!(
1139
+ self,
1140
+ M,
1141
+ ok,
1142
+ len <= max,
1143
+ Message::Lazy(&|| format!("Expected the item count to be less than or equal to '{max}'")),
1144
+ "maxItems"
1145
+ );
1146
+ }
1147
+ if n.prefix_items.is_none() && n.items.is_none() && n.contains.is_none() && !n.unique_items {
1148
+ return ok;
1149
+ }
1150
+ let mut count = 0u64;
1151
+ let prefix_len = n.prefix_items.as_ref().map_or(0, Vec::len);
1152
+ for (i, item) in a.iter().enumerate() {
1153
+ if i < prefix_len {
1154
+ if let Some(b) = bits.as_deref_mut() {
1155
+ b.set(i);
1156
+ }
1157
+ let child = n.prefix_items.as_ref().unwrap()[i];
1158
+ let kw = n.prefix_keyword;
1159
+ and!(M, ok, self.eval_at::<M, I>(child, &|| format!("{kw}/{i}"), item, &|| i.to_string()));
1160
+ } else if let Some(items) = n.items {
1161
+ if let Some(b) = bits.as_deref_mut() {
1162
+ b.set(i);
1163
+ }
1164
+ let kw = n.items_keyword;
1165
+ and!(M, ok, self.eval_at::<M, I>(items, &|| kw.to_string(), item, &|| i.to_string()));
1166
+ }
1167
+ if let Some(contains) = n.contains {
1168
+ let matched = if M::COLLECT {
1169
+ let (target, suffix) = self.resolve(contains);
1170
+ let pointer = &self.node(target).pointer;
1171
+ let seg = format!("contains{suffix}");
1172
+ let doc = i.to_string();
1173
+ self.col().begin_child_context(Some(&seg), Some(pointer), Some(&doc));
1174
+ if self.eval_node::<Collect, I>(target, item, None) {
1175
+ self.col().commit_child_context(true, true, Message::Static(EVALUATED_SUBSCHEMA));
1176
+ true
1177
+ } else {
1178
+ self.col().pop_child_context();
1179
+ false
1180
+ }
1181
+ } else {
1182
+ self.fast(self.p.fast_target[contains as usize], item)
1183
+ };
1184
+ if matched {
1185
+ count += 1;
1186
+ if n.contains_marks_evaluated {
1187
+ if let Some(b) = bits.as_deref_mut() {
1188
+ b.set(i);
1189
+ }
1190
+ }
1191
+ }
1192
+ }
1193
+ }
1194
+ if n.unique_items {
1195
+ let unique = all_unique(a);
1196
+ check!(self, M, ok, unique, Message::Static(UNIQUE_ITEMS), "uniqueItems");
1197
+ }
1198
+ if n.contains.is_some() {
1199
+ let max = n.max_contains;
1200
+ let min = n.min_contains;
1201
+ let m = count >= min && max.is_none_or(|mx| count <= mx);
1202
+ let over = max.is_some_and(|mx| count > mx);
1203
+ check!(
1204
+ self,
1205
+ M,
1206
+ ok,
1207
+ m,
1208
+ Message::Lazy(&|| if over {
1209
+ format!("Expected the contains count to be less than or equal to '{}'", max.unwrap())
1210
+ } else {
1211
+ format!("Expected the contains count to be greater than or equal to '{min}'")
1212
+ }),
1213
+ "contains"
1214
+ );
1215
+ }
1216
+ ok
1217
+ }
1218
+
1219
+ fn eval_unevaluated_items<'x, M: Mode, I: Instance<'x>>(
1220
+ &mut self,
1221
+ n: &'p SchemaNode,
1222
+ a: I::Array,
1223
+ bits: &mut Bits,
1224
+ ) -> bool {
1225
+ let child = n.unevaluated_items.unwrap();
1226
+ let mut ok = true;
1227
+ for (i, item) in a.iter().enumerate() {
1228
+ if bits.get(i) {
1229
+ continue;
1230
+ }
1231
+ bits.set(i);
1232
+ and!(M, ok, self.eval_at::<M, I>(child, &|| "unevaluatedItems".to_string(), item, &|| i.to_string()));
1233
+ }
1234
+ if M::COLLECT {
1235
+ self.col().evaluated_keyword(ok, Message::None, "unevaluatedItems");
1236
+ }
1237
+ ok
1238
+ }
1239
+
1240
+ // ----------------------------------------------------------------------------------------------------------------
1241
+ // In-place applicators
1242
+
1243
+ fn can_mark<'x, I: Instance<'x>>(&self, id: NodeId, x: I) -> bool {
1244
+ let n = self.node(id);
1245
+ if is_object(x) { n.marks_properties } else { n.marks_items }
1246
+ }
1247
+
1248
+ /// Evaluates an in-place child: a new context at the same instance location, on a fresh scratch set of evaluated
1249
+ /// properties/items merged into the parent's on success. A failing child is committed or popped. Returns the
1250
+ /// result and the scratch bits (for oneOf, which merges only a single match).
1251
+ fn eval_in_place_child<'x, M: Mode, I: Instance<'x>>(
1252
+ &mut self,
1253
+ child: NodeId,
1254
+ path: &dyn Fn() -> String,
1255
+ x: I,
1256
+ bits: Option<&mut Bits>,
1257
+ commit_on_failure: bool,
1258
+ elide: bool,
1259
+ ) -> (bool, Option<Bits>) {
1260
+ let (target, suffix) = if !elide {
1261
+ (child, String::new())
1262
+ } else if M::COLLECT {
1263
+ self.resolve(child)
1264
+ } else {
1265
+ (self.p.fast_target[child as usize], String::new())
1266
+ };
1267
+ let mut scratch =
1268
+ if bits.is_some() && self.can_mark(child, x) { Some(Bits::new(container_len(x))) } else { None };
1269
+ let guarded = self.node(target).in_place_cycle;
1270
+ if guarded {
1271
+ self.depth += 1;
1272
+ if self.depth > self.p.max_depth {
1273
+ self.depth_exceeded = true;
1274
+ self.depth -= 1;
1275
+ return (false, None);
1276
+ }
1277
+ }
1278
+ let ok = if M::COLLECT {
1279
+ let pointer = &self.node(target).pointer;
1280
+ let seg = format!("{}{}", path(), suffix);
1281
+ self.col().begin_child_context(Some(&seg), Some(pointer), None);
1282
+ let ok = self.eval_node::<Collect, I>(target, x, scratch.as_mut());
1283
+ if ok || commit_on_failure {
1284
+ self.col().commit_child_context(ok, ok, Message::Static(EVALUATED_SUBSCHEMA));
1285
+ } else {
1286
+ self.col().pop_child_context();
1287
+ }
1288
+ ok
1289
+ } else if scratch.is_some() {
1290
+ self.eval_node::<Fast, I>(target, x, scratch.as_mut())
1291
+ } else {
1292
+ self.fast(target, x)
1293
+ };
1294
+ if guarded {
1295
+ self.depth -= 1;
1296
+ }
1297
+ if ok {
1298
+ if let (Some(s), Some(b)) = (&scratch, bits) {
1299
+ b.merge(s);
1300
+ }
1301
+ }
1302
+ (ok, scratch)
1303
+ }
1304
+
1305
+ fn resolve_dynamic(&self, d: &DynamicRefTarget) -> NodeId {
1306
+ for resource in &self.scope {
1307
+ if let Some(&(_, target)) = d.by_resource.iter().find(|(r, _)| r == resource) {
1308
+ return target;
1309
+ }
1310
+ }
1311
+ d.fallback
1312
+ }
1313
+
1314
+ fn eval_in_place<'x, M: Mode, I: Instance<'x>>(
1315
+ &mut self,
1316
+ n: &'p SchemaNode,
1317
+ x: I,
1318
+ mut bits: Option<&mut Bits>,
1319
+ ) -> bool {
1320
+ let mut ok = true;
1321
+ if let Some(r) = n.ref_ {
1322
+ let (m, _) =
1323
+ self.eval_in_place_child::<M, I>(r, &|| "$ref".to_string(), x, bits.as_deref_mut(), true, true);
1324
+ check!(self, M, ok, m, Message::Static(if m { MATCHED_ALL } else { DID_NOT_MATCH_ALL }), "$ref");
1325
+ }
1326
+ if let Some(r) = n.static_dynamic_ref {
1327
+ let keyword = n.static_dynamic_keyword.name();
1328
+ let (m, _) =
1329
+ self.eval_in_place_child::<M, I>(r, &|| keyword.to_string(), x, bits.as_deref_mut(), true, true);
1330
+ check!(self, M, ok, m, Message::Static(if m { MATCHED_ALL } else { DID_NOT_MATCH_ALL }), keyword);
1331
+ }
1332
+ if let Some(d) = &n.dynamic_ref {
1333
+ let keyword = if d.is_recursive { "$recursiveRef" } else { "$dynamicRef" };
1334
+ // The resolved target is elided, with no hops in the path.
1335
+ let dynamic_target = self.resolve_dynamic(d);
1336
+ let target =
1337
+ if M::COLLECT { self.resolve(dynamic_target).0 } else { self.p.fast_target[dynamic_target as usize] };
1338
+ let (m, _) =
1339
+ self.eval_in_place_child::<M, I>(target, &|| keyword.to_string(), x, bits.as_deref_mut(), true, false);
1340
+ check!(self, M, ok, m, Message::Static(if m { MATCHED_ALL } else { DID_NOT_MATCH_ALL }), keyword);
1341
+ }
1342
+ if let Some(list) = &n.all_of {
1343
+ let mut all = true;
1344
+ for (i, &b) in list.iter().enumerate() {
1345
+ let (m, _) =
1346
+ self.eval_in_place_child::<M, I>(b, &|| format!("allOf/{i}"), x, bits.as_deref_mut(), true, true);
1347
+ if !m {
1348
+ if !M::COLLECT {
1349
+ return false;
1350
+ }
1351
+ all = false;
1352
+ }
1353
+ }
1354
+ check!(self, M, ok, all, Message::Static(if all { MATCHED_ALL } else { DID_NOT_MATCH_ALL }), "allOf");
1355
+ }
1356
+ if let Some(list) = &n.any_of {
1357
+ let mut any = false;
1358
+ // Every branch runs when results are collected or evaluated properties/items are tracked.
1359
+ let exhaustive = M::COLLECT || bits.is_some();
1360
+ // Fail-fast evaluation only tries the branches a discriminator property can select.
1361
+ let selection = if exhaustive { Selection::All } else { select(n.any_of_discriminator.as_deref(), x) };
1362
+ for i in selection.indexes(list.len()) {
1363
+ let b = list[i];
1364
+ let (m, _) =
1365
+ self.eval_in_place_child::<M, I>(b, &|| format!("anyOf/{i}"), x, bits.as_deref_mut(), false, true);
1366
+ if m {
1367
+ any = true;
1368
+ if !exhaustive {
1369
+ break;
1370
+ }
1371
+ }
1372
+ }
1373
+ check!(
1374
+ self,
1375
+ M,
1376
+ ok,
1377
+ any,
1378
+ Message::Static(if any { MATCHED_AT_LEAST_ONE } else { DID_NOT_MATCH_AT_LEAST_ONE }),
1379
+ "anyOf"
1380
+ );
1381
+ }
1382
+ if let Some(list) = &n.one_of {
1383
+ let mut matched = 0;
1384
+ let mut only: Option<Bits> = None;
1385
+ let track = bits.is_some();
1386
+ // Branches a discriminator rules out cannot match, so fail-fast evaluation skips them.
1387
+ let selection =
1388
+ if M::COLLECT || track { Selection::All } else { select(n.one_of_discriminator.as_deref(), x) };
1389
+ for i in selection.indexes(list.len()) {
1390
+ let b = list[i];
1391
+ // Evaluated properties/items are merged only when exactly one branch matched, so collect them aside.
1392
+ let mut aside = if track { Some(Bits::new(container_len(x))) } else { None };
1393
+ let (m, scratch) =
1394
+ self.eval_in_place_child::<M, I>(b, &|| format!("oneOf/{i}"), x, aside.as_mut(), false, true);
1395
+ if m {
1396
+ matched += 1;
1397
+ only = if track { aside.or(scratch) } else { None };
1398
+ if !M::COLLECT && matched > 1 {
1399
+ return false;
1400
+ }
1401
+ }
1402
+ }
1403
+ if matched == 1 {
1404
+ if let (Some(b), Some(o)) = (bits.as_deref_mut(), &only) {
1405
+ b.merge(o);
1406
+ }
1407
+ }
1408
+ let message = match matched {
1409
+ 0 => MATCHED_NO_SCHEMA,
1410
+ 1 => MATCHED_EXACTLY_ONE,
1411
+ _ => MATCHED_MORE_THAN_ONE,
1412
+ };
1413
+ check!(self, M, ok, matched == 1, Message::Static(message), "oneOf");
1414
+ }
1415
+ if let Some(not) = n.not {
1416
+ // Not elided, never contributes results or evaluated properties/items.
1417
+ let inner = if M::COLLECT {
1418
+ let pointer = &self.node(not).pointer;
1419
+ self.col().begin_child_context(Some("not"), Some(pointer), None);
1420
+ let inner = self.eval_node::<Collect, I>(not, x, None);
1421
+ self.col().pop_child_context();
1422
+ inner
1423
+ } else {
1424
+ self.fast(self.p.fast_target[not as usize], x)
1425
+ };
1426
+ check!(self, M, ok, !inner, Message::Static(if inner { MATCHED_NOT } else { DID_NOT_MATCH_NOT }), "not");
1427
+ }
1428
+ if let Some(cond_node) = n.if_ {
1429
+ let (cond, _) =
1430
+ self.eval_in_place_child::<M, I>(cond_node, &|| "if".to_string(), x, bits.as_deref_mut(), false, true);
1431
+ if M::COLLECT {
1432
+ self.col().evaluated_keyword(
1433
+ true,
1434
+ Message::Static(if cond { MATCHED_IF_FOR_THEN } else { MATCHED_IF_FOR_ELSE }),
1435
+ "if",
1436
+ );
1437
+ }
1438
+ if cond {
1439
+ if let Some(t) = n.then {
1440
+ let (m, _) =
1441
+ self.eval_in_place_child::<M, I>(t, &|| "then".to_string(), x, bits.as_deref_mut(), true, true);
1442
+ check!(self, M, ok, m, Message::Static(if m { MATCHED_THEN } else { DID_NOT_MATCH_THEN }), "then");
1443
+ }
1444
+ } else if let Some(e) = n.else_ {
1445
+ let (m, _) = self.eval_in_place_child::<M, I>(e, &|| "else".to_string(), x, bits, true, true);
1446
+ check!(self, M, ok, m, Message::Static(if m { MATCHED_ELSE } else { DID_NOT_MATCH_ELSE }), "else");
1447
+ }
1448
+ }
1449
+ ok
1450
+ }
1451
+ }