castrum 0.9.2 → 0.9.4
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.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
|
@@ -0,0 +1,1143 @@
|
|
|
1
|
+
// rust/json/fast_schema/tests.rs — Fast-path vs jsonschema-crate parity tests.
|
|
2
|
+
//
|
|
3
|
+
// Every supported construct is checked for exact agreement with the reference
|
|
4
|
+
// `jsonschema` crate (the authoritative DOM path); unsupported keywords must
|
|
5
|
+
// fail to compile so the caller falls back.
|
|
6
|
+
|
|
7
|
+
use super::*;
|
|
8
|
+
use serde_json::{json, Value};
|
|
9
|
+
|
|
10
|
+
fn assert_parity(schema: Value, docs: &[&str]) {
|
|
11
|
+
let fast = compile(&schema).expect("schema must compile on the fast path");
|
|
12
|
+
// The fast path implements draft-07; pin the reference so parity is
|
|
13
|
+
// meaningful (jsonschema 0.48's default draft is 2020-12, whose `items`/
|
|
14
|
+
// `$ref`-sibling/boolean-exclusive semantics differ from draft-07).
|
|
15
|
+
let v = jsonschema::options()
|
|
16
|
+
.with_draft(jsonschema::Draft::Draft7)
|
|
17
|
+
.should_validate_formats(true)
|
|
18
|
+
.build(&schema)
|
|
19
|
+
.expect("schema must compile on reference");
|
|
20
|
+
for &doc in docs {
|
|
21
|
+
let expected = serde_json::from_str::<Value>(doc)
|
|
22
|
+
.map(|value| v.is_valid(&value))
|
|
23
|
+
.unwrap_or(false);
|
|
24
|
+
let got = fast.is_valid_bytes(doc.as_bytes());
|
|
25
|
+
assert_eq!(got, expected, "schema={schema} doc={doc}");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn bench_schema() -> Value {
|
|
30
|
+
json!({
|
|
31
|
+
"type": "object",
|
|
32
|
+
"required": ["id", "name", "active", "score", "tags", "nested"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"id": { "type": "number" },
|
|
35
|
+
"name": { "type": "string", "minLength": 1 },
|
|
36
|
+
"active": { "type": "boolean" },
|
|
37
|
+
"score": { "type": "number" },
|
|
38
|
+
"tags": { "type": "array", "items": { "type": "string" }, "maxItems": 20 },
|
|
39
|
+
"nested": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["version", "createdAt"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"version": { "type": "number" },
|
|
44
|
+
"createdAt": { "type": "string" }
|
|
45
|
+
},
|
|
46
|
+
"additionalProperties": false
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"additionalProperties": false
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[test]
|
|
54
|
+
fn bench_schema_parity() {
|
|
55
|
+
let schema = bench_schema();
|
|
56
|
+
let docs = [
|
|
57
|
+
// valid
|
|
58
|
+
r#"{"id":1,"name":"user_1","active":true,"score":1.25,"tags":["alpha","beta"],"nested":{"version":1,"createdAt":"2026-01-01T00:00:00Z"}}"#,
|
|
59
|
+
r#"{"id":2,"name":"b","active":false,"score":0,"tags":[],"nested":{"version":2,"createdAt":"x"}}"#,
|
|
60
|
+
// wrong root type
|
|
61
|
+
r#"[]"#,
|
|
62
|
+
r#"42"#,
|
|
63
|
+
r#""hi""#,
|
|
64
|
+
r#"null"#,
|
|
65
|
+
// wrong property types
|
|
66
|
+
r#"{"id":"x","name":"a","active":true,"score":1,"tags":[],"nested":{"version":1,"createdAt":"c"}}"#,
|
|
67
|
+
r#"{"id":1,"name":"a","active":"yes","score":1,"tags":[],"nested":{"version":1,"createdAt":"c"}}"#,
|
|
68
|
+
r#"{"id":1,"name":"a","active":true,"score":1,"tags":["a",1],"nested":{"version":1,"createdAt":"c"}}"#,
|
|
69
|
+
// missing required
|
|
70
|
+
r#"{"id":1,"name":"a","active":true,"score":1,"tags":[]}"#,
|
|
71
|
+
// extra property (additionalProperties:false)
|
|
72
|
+
r#"{"id":1,"name":"a","active":true,"score":1,"tags":[],"extra":1,"nested":{"version":1,"createdAt":"c"}}"#,
|
|
73
|
+
// nested extra property
|
|
74
|
+
r#"{"id":1,"name":"a","active":true,"score":1,"tags":[],"nested":{"version":1,"createdAt":"c","z":2}}"#,
|
|
75
|
+
// nested missing required
|
|
76
|
+
r#"{"id":1,"name":"a","active":true,"score":1,"tags":[],"nested":{"version":1}}"#,
|
|
77
|
+
// minLength violation + maxItems violation
|
|
78
|
+
r#"{"id":1,"name":"","active":true,"score":1,"tags":[],"nested":{"version":1,"createdAt":"c"}}"#,
|
|
79
|
+
r#"{"id":1,"name":"a","active":true,"score":1,"tags":["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u"],"nested":{"version":1,"createdAt":"c"}}"#,
|
|
80
|
+
// malformed JSON
|
|
81
|
+
r#"{"id":1,"name":"a""#,
|
|
82
|
+
r#"not json"#,
|
|
83
|
+
r#"{"id":1,}"#,
|
|
84
|
+
];
|
|
85
|
+
assert_parity(schema, &docs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#[test]
|
|
89
|
+
fn simple_type_parity() {
|
|
90
|
+
let schemas = [
|
|
91
|
+
json!({"type":"object"}),
|
|
92
|
+
json!({"type":"array"}),
|
|
93
|
+
json!({"type":"string"}),
|
|
94
|
+
json!({"type":"number"}),
|
|
95
|
+
json!({"type":"integer"}),
|
|
96
|
+
json!({"type":"boolean"}),
|
|
97
|
+
json!({"type":"null"}),
|
|
98
|
+
json!({"type":["string","number"]}),
|
|
99
|
+
json!({}),
|
|
100
|
+
json!(true),
|
|
101
|
+
json!(false),
|
|
102
|
+
];
|
|
103
|
+
let docs = [
|
|
104
|
+
"{}",
|
|
105
|
+
"[]",
|
|
106
|
+
"\"x\"",
|
|
107
|
+
"42",
|
|
108
|
+
"42.5",
|
|
109
|
+
"1e2",
|
|
110
|
+
"true",
|
|
111
|
+
"false",
|
|
112
|
+
"null",
|
|
113
|
+
"{\"a\":1}",
|
|
114
|
+
"[1,2]",
|
|
115
|
+
"\"\"",
|
|
116
|
+
"-0",
|
|
117
|
+
"0.1",
|
|
118
|
+
];
|
|
119
|
+
for s in &schemas {
|
|
120
|
+
assert_parity(s.clone(), &docs);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[test]
|
|
125
|
+
fn string_length_parity() {
|
|
126
|
+
let schemas = [
|
|
127
|
+
json!({"type":"string","minLength":2}),
|
|
128
|
+
json!({"type":"string","maxLength":4}),
|
|
129
|
+
json!({"type":"string","minLength":1,"maxLength":3}),
|
|
130
|
+
];
|
|
131
|
+
let docs = [
|
|
132
|
+
r#""ab""#,
|
|
133
|
+
r#""a""#,
|
|
134
|
+
r#""abcd""#,
|
|
135
|
+
r#""abcde""#,
|
|
136
|
+
r#""""#,
|
|
137
|
+
// escapes count as decoded length
|
|
138
|
+
r#""\u0041""#, // "A" -> len 1
|
|
139
|
+
r#""a\u0041b""#, // "aAb" -> len 3
|
|
140
|
+
r#""\n\t""#, // 2 chars
|
|
141
|
+
r#""\uD83D\uDE00""#, // 😀 -> len 1
|
|
142
|
+
];
|
|
143
|
+
for s in &schemas {
|
|
144
|
+
assert_parity(s.clone(), &docs);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[test]
|
|
149
|
+
fn number_bounds_parity() {
|
|
150
|
+
let schemas = [
|
|
151
|
+
json!({"type":"number","minimum":0,"maximum":10}),
|
|
152
|
+
json!({"type":"integer","minimum":1}),
|
|
153
|
+
json!({"type":"number","exclusiveMinimum":0.5}),
|
|
154
|
+
json!({"type":"number","exclusiveMaximum":5}),
|
|
155
|
+
json!({"type":"integer"}),
|
|
156
|
+
];
|
|
157
|
+
let docs = [
|
|
158
|
+
"0",
|
|
159
|
+
"5",
|
|
160
|
+
"10",
|
|
161
|
+
"11",
|
|
162
|
+
"-1",
|
|
163
|
+
"0.5",
|
|
164
|
+
"0.5000001",
|
|
165
|
+
"1e1",
|
|
166
|
+
"1.5",
|
|
167
|
+
"2.0",
|
|
168
|
+
"-0",
|
|
169
|
+
"0.1",
|
|
170
|
+
"100",
|
|
171
|
+
];
|
|
172
|
+
for s in &schemas {
|
|
173
|
+
assert_parity(s.clone(), &docs);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn array_parity() {
|
|
179
|
+
let schemas = [
|
|
180
|
+
json!({"type":"array","items":{"type":"string"}}),
|
|
181
|
+
json!({"type":"array","minItems":1,"maxItems":3}),
|
|
182
|
+
json!({"type":"array","items":{"type":"integer"},"minItems":2}),
|
|
183
|
+
];
|
|
184
|
+
let docs = [
|
|
185
|
+
"[]",
|
|
186
|
+
"[\"a\"]",
|
|
187
|
+
"[\"a\",\"b\",\"c\"]",
|
|
188
|
+
"[\"a\",1]",
|
|
189
|
+
"[1]",
|
|
190
|
+
"[1,2,3]",
|
|
191
|
+
"[1,2]",
|
|
192
|
+
"[1.5]",
|
|
193
|
+
"[\"a\",\"b\",\"c\",\"d\"]",
|
|
194
|
+
"{}",
|
|
195
|
+
"null",
|
|
196
|
+
];
|
|
197
|
+
for s in &schemas {
|
|
198
|
+
assert_parity(s.clone(), &docs);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn object_required_additional_parity() {
|
|
204
|
+
let schemas = [
|
|
205
|
+
json!({"required":["a"]}),
|
|
206
|
+
json!({"properties":{"a":{"type":"string"}}}),
|
|
207
|
+
json!({"properties":{"a":{"type":"string"}},"additionalProperties":false}),
|
|
208
|
+
json!({"properties":{"a":{"type":"string"}},"additionalProperties":{"type":"number"}}),
|
|
209
|
+
json!({"type":"object","minProperties":1,"maxProperties":2}),
|
|
210
|
+
json!({"required":["a","b"]}),
|
|
211
|
+
];
|
|
212
|
+
let docs = [
|
|
213
|
+
"{}",
|
|
214
|
+
"{\"a\":1}",
|
|
215
|
+
"{\"a\":\"x\"}",
|
|
216
|
+
"{\"b\":1}",
|
|
217
|
+
"{\"a\":1,\"b\":2}",
|
|
218
|
+
"{\"a\":1,\"c\":true}",
|
|
219
|
+
"{\"c\":true}",
|
|
220
|
+
"{\"a\":\"x\",\"c\":\"s\"}",
|
|
221
|
+
"{\"a\":\"x\",\"c\":1}",
|
|
222
|
+
"{\"a\":\"x\",\"b\":\"y\"}",
|
|
223
|
+
"\"str\"",
|
|
224
|
+
"[]",
|
|
225
|
+
// duplicate keys (reference dedups, last wins)
|
|
226
|
+
"{\"a\":1,\"a\":2}",
|
|
227
|
+
"{\"x\":1,\"x\":2}",
|
|
228
|
+
// escaped keys
|
|
229
|
+
"{\"\\u0061\":\"x\"}", // key "a"
|
|
230
|
+
];
|
|
231
|
+
for s in &schemas {
|
|
232
|
+
assert_parity(s.clone(), &docs);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#[test]
|
|
237
|
+
fn min_max_properties_distinct_parity() {
|
|
238
|
+
let schemas = [
|
|
239
|
+
json!({"type":"object","minProperties":1}),
|
|
240
|
+
json!({"type":"object","maxProperties":2}),
|
|
241
|
+
json!({"type":"object","minProperties":1,"maxProperties":2}),
|
|
242
|
+
// These force the inline → heap distinct-key migration (>16 keys or a
|
|
243
|
+
// >48-byte key) — the heap path must stay byte-parity with the crate.
|
|
244
|
+
json!({"type":"object","minProperties":16,"maxProperties":17}),
|
|
245
|
+
json!({"type":"object","minProperties":1,"maxProperties":1}),
|
|
246
|
+
];
|
|
247
|
+
let docs = [
|
|
248
|
+
"{}",
|
|
249
|
+
"{\"a\":1}",
|
|
250
|
+
"{\"a\":1,\"b\":2}",
|
|
251
|
+
"{\"a\":1,\"b\":2,\"c\":3}",
|
|
252
|
+
// Duplicate keys: the distinct count differs from the member count.
|
|
253
|
+
"{\"a\":1,\"a\":2}",
|
|
254
|
+
// 17 distinct keys → exceeds the 16-slot inline tracker.
|
|
255
|
+
"{\"k0\":0,\"k1\":1,\"k2\":2,\"k3\":3,\"k4\":4,\"k5\":5,\"k6\":6,\"k7\":7,\"k8\":8,\"k9\":9,\"k10\":10,\"k11\":11,\"k12\":12,\"k13\":13,\"k14\":14,\"k15\":15,\"k16\":16}",
|
|
256
|
+
// Key longer than the 48-byte inline key buffer.
|
|
257
|
+
"{\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\":1}",
|
|
258
|
+
];
|
|
259
|
+
for s in &schemas {
|
|
260
|
+
assert_parity(s.clone(), &docs);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[test]
|
|
265
|
+
fn nested_composite_parity() {
|
|
266
|
+
let schemas = [json!({
|
|
267
|
+
"type": "object",
|
|
268
|
+
"properties": {
|
|
269
|
+
"data": {
|
|
270
|
+
"type": "object",
|
|
271
|
+
"required": ["x"],
|
|
272
|
+
"properties": { "x": { "type": "integer" } },
|
|
273
|
+
"additionalProperties": false
|
|
274
|
+
},
|
|
275
|
+
"list": {
|
|
276
|
+
"type": "array",
|
|
277
|
+
"items": { "type": "object", "properties": { "n": { "type": "number" } }, "required": ["n"] }
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"required": ["data"]
|
|
281
|
+
})];
|
|
282
|
+
let docs = [
|
|
283
|
+
r#"{"data":{"x":1}}"#,
|
|
284
|
+
r#"{"data":{"x":"bad"}}"#,
|
|
285
|
+
r#"{"data":{"x":1,"y":2}}"#,
|
|
286
|
+
r#"{"data":{"x":1},"list":[{"n":1},{"n":2}]}"#,
|
|
287
|
+
r#"{"data":{"x":1},"list":[{"n":1},{"m":2}]}"#,
|
|
288
|
+
r#"{"data":{"x":1},"list":[]}"#,
|
|
289
|
+
r#"{}"#,
|
|
290
|
+
r#"{"data":{}}"#,
|
|
291
|
+
];
|
|
292
|
+
assert_parity(schemas[0].clone(), &docs);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn unsupported_keywords_fall_back() {
|
|
297
|
+
// These must NOT compile on the fast path (caller falls back to DOM).
|
|
298
|
+
let unsupported = [
|
|
299
|
+
json!({"propertyNames":{"type":"string"}}),
|
|
300
|
+
json!({"format":"uri"}), // only format:email is fast-path
|
|
301
|
+
json!({"format":"date-time"}),
|
|
302
|
+
json!({"exclusiveMinimum":true}), // draft-04/06 boolean form
|
|
303
|
+
json!({"$ref":"other.json#/definitions/x"}), // external ref
|
|
304
|
+
json!({"$ref":"#anchor"}), // anchor ref (unsupported)
|
|
305
|
+
json!({"$schema":"http://json-schema.org/draft-04/schema#"}),
|
|
306
|
+
json!({"dependencies":{"a":{"type":"string"}}}), // schema form
|
|
307
|
+
json!({"enum":[1,2],"const":1}), // enum + const together
|
|
308
|
+
];
|
|
309
|
+
for s in &unsupported {
|
|
310
|
+
assert!(compile(s).is_err(), "should fall back: {s}");
|
|
311
|
+
}
|
|
312
|
+
// Sanity: the supported bench schema DOES compile.
|
|
313
|
+
assert!(compile(&bench_schema()).is_ok());
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── extended draft-07 keyword parity (fast vs jsonschema crate) ──────────
|
|
317
|
+
|
|
318
|
+
#[test]
|
|
319
|
+
fn pattern_parity() {
|
|
320
|
+
let schemas = [
|
|
321
|
+
json!({"type":"string","pattern":"^a"}),
|
|
322
|
+
json!({"type":"string","pattern":"\\d{3}-\\d{4}"}),
|
|
323
|
+
json!({"type":"string","pattern":"a.c"}),
|
|
324
|
+
// ECMA-262 lookahead (fancy-regex only; the regex crate would reject it)
|
|
325
|
+
json!({"type":"string","pattern":"foo(?=bar)"}),
|
|
326
|
+
json!({"type":"string","pattern":"^(a|b)+$"}),
|
|
327
|
+
];
|
|
328
|
+
let docs = [
|
|
329
|
+
r#""abc""#,
|
|
330
|
+
r#""a""#,
|
|
331
|
+
r#""xabc""#,
|
|
332
|
+
r#""123-4567""#,
|
|
333
|
+
r#""12-34""#,
|
|
334
|
+
r#""foobar""#,
|
|
335
|
+
r#""foo""#,
|
|
336
|
+
r#""foobaz""#,
|
|
337
|
+
r#""ab""#,
|
|
338
|
+
r#""baba""#,
|
|
339
|
+
r#""ccc""#,
|
|
340
|
+
"42",
|
|
341
|
+
"null",
|
|
342
|
+
r#""\u0061bc""#, // "abc"
|
|
343
|
+
];
|
|
344
|
+
for s in &schemas {
|
|
345
|
+
assert_parity(s.clone(), &docs);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn pattern_properties_parity() {
|
|
351
|
+
let schemas = [
|
|
352
|
+
json!({"type":"object","patternProperties":{"^x":{"type":"integer"}}}),
|
|
353
|
+
json!({
|
|
354
|
+
"type":"object",
|
|
355
|
+
"properties":{"foo":{"type":"string"}},
|
|
356
|
+
"patternProperties":{"^[a-z]+$":{"type":"number"}},
|
|
357
|
+
"additionalProperties":false
|
|
358
|
+
}),
|
|
359
|
+
json!({
|
|
360
|
+
"type":"object",
|
|
361
|
+
"patternProperties":{"S_":{"type":"string"}},
|
|
362
|
+
"additionalProperties":{"type":"number"}
|
|
363
|
+
}),
|
|
364
|
+
];
|
|
365
|
+
let docs = [
|
|
366
|
+
r#"{"x1":1,"x2":2}"#,
|
|
367
|
+
r#"{"x1":"str"}"#,
|
|
368
|
+
r#"{"y1":1}"#,
|
|
369
|
+
r#"{"foo":"a","bar":1}"#,
|
|
370
|
+
r#"{"foo":"a","bar":"s"}"#,
|
|
371
|
+
r#"{"FOO":1}"#,
|
|
372
|
+
r#"{"S_a":"ok","other":1}"#,
|
|
373
|
+
r#"{"S_a":1}"#,
|
|
374
|
+
r#"{"other":"str"}"#,
|
|
375
|
+
r#"{}"#,
|
|
376
|
+
];
|
|
377
|
+
for s in &schemas {
|
|
378
|
+
assert_parity(s.clone(), &docs);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn enum_const_parity() {
|
|
384
|
+
let schemas = [
|
|
385
|
+
json!({"enum":[1,2,3]}),
|
|
386
|
+
json!({"enum":["a","b","c"]}),
|
|
387
|
+
json!({"enum":[1,1.0,"1",true,null]}), // 1 and 1.0 are equal per JSON Schema
|
|
388
|
+
json!({"const":5}),
|
|
389
|
+
json!({"const":{"a":1,"b":2}}),
|
|
390
|
+
json!({"type":"integer","enum":[1,2]}),
|
|
391
|
+
json!({"enum":[{"x":[1,2]},{"x":[2,1]}]}),
|
|
392
|
+
];
|
|
393
|
+
let docs = [
|
|
394
|
+
"1",
|
|
395
|
+
"2",
|
|
396
|
+
"3",
|
|
397
|
+
"4",
|
|
398
|
+
"1.0",
|
|
399
|
+
"1e0",
|
|
400
|
+
r#""a""#,
|
|
401
|
+
r#""b""#,
|
|
402
|
+
r#""d""#,
|
|
403
|
+
r#""""#,
|
|
404
|
+
"true",
|
|
405
|
+
"false",
|
|
406
|
+
"null",
|
|
407
|
+
r#"{"a":1,"b":2}"#,
|
|
408
|
+
r#"{"b":2,"a":1}"#,
|
|
409
|
+
r#"{"a":2,"b":2}"#,
|
|
410
|
+
r#"{"x":[1,2]}"#,
|
|
411
|
+
r#"{"x":[2,1]}"#,
|
|
412
|
+
r#"[1,2]"#,
|
|
413
|
+
];
|
|
414
|
+
for s in &schemas {
|
|
415
|
+
assert_parity(s.clone(), &docs);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn multiple_of_parity() {
|
|
421
|
+
let schemas = [
|
|
422
|
+
json!({"type":"number","multipleOf":2}),
|
|
423
|
+
json!({"type":"number","multipleOf":0.01}),
|
|
424
|
+
json!({"type":"number","multipleOf":0.1}),
|
|
425
|
+
json!({"type":"integer","multipleOf":3}),
|
|
426
|
+
json!({"multipleOf":1.5}),
|
|
427
|
+
];
|
|
428
|
+
let docs = [
|
|
429
|
+
"0", "2", "4", "3", "-2", "2.5", "1", "0.5", "0.01", "0.02", "0.03", "0.1", "1.23", "100",
|
|
430
|
+
"1.5", "3.0", "3.1", "4.5", "1e1", "1.25", "2.25", r#""x""#, "null", "{}",
|
|
431
|
+
];
|
|
432
|
+
for s in &schemas {
|
|
433
|
+
assert_parity(s.clone(), &docs);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[test]
|
|
438
|
+
fn multiple_of_non_finite_no_panic() {
|
|
439
|
+
// `1e999` parses to f64::INFINITY in `Cursor::number`. `is_multiple_of`
|
|
440
|
+
// must not panic (a panic through the raw C ABI crashes the host process)
|
|
441
|
+
// and must agree with the DOM validator, whose serde parse of `1e999`
|
|
442
|
+
// fails → invalid.
|
|
443
|
+
let schemas = [
|
|
444
|
+
json!({"type":"number","multipleOf":2}),
|
|
445
|
+
json!({"type":"number","multipleOf":0.1}),
|
|
446
|
+
json!({"type":"integer","multipleOf":3}),
|
|
447
|
+
];
|
|
448
|
+
let docs = ["1e999", "-1e999", "1e308", "1.7976931348623157e308", "2"];
|
|
449
|
+
for s in &schemas {
|
|
450
|
+
assert_parity(s.clone(), &docs);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
#[test]
|
|
455
|
+
fn combinators_parity() {
|
|
456
|
+
let schemas = [
|
|
457
|
+
json!({"allOf":[{"type":"number"},{"minimum":0}]}),
|
|
458
|
+
json!({"anyOf":[{"type":"string"},{"type":"integer"}]}),
|
|
459
|
+
json!({"oneOf":[{"type":"number"},{"minimum":0}]}),
|
|
460
|
+
json!({"not":{"type":"string"}}),
|
|
461
|
+
json!({"anyOf":[{"type":"null"},{"type":"array"}]}),
|
|
462
|
+
json!({"oneOf":[{"enum":[1,2]},{"minimum":1}]}),
|
|
463
|
+
json!({"allOf":[{"properties":{"a":{"type":"integer"}}},{"required":["a"]}]}),
|
|
464
|
+
];
|
|
465
|
+
let docs = [
|
|
466
|
+
"5", "-5", r#""5""#, "0", "0.5", "null", r#""s""#, "1", "1.5", "[]", "{}", "2", "3", "-1",
|
|
467
|
+
];
|
|
468
|
+
for s in &schemas {
|
|
469
|
+
assert_parity(s.clone(), &docs);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
#[test]
|
|
474
|
+
fn if_then_else_parity() {
|
|
475
|
+
let schemas = [
|
|
476
|
+
json!({"type":"number","if":{"minimum":0},"then":{"multipleOf":2}}),
|
|
477
|
+
json!({
|
|
478
|
+
"if":{"properties":{"type":{"const":"a"}}},
|
|
479
|
+
"then":{"required":["a"]},
|
|
480
|
+
"else":{"required":["b"]}
|
|
481
|
+
}),
|
|
482
|
+
json!({"if":{"type":"string"},"else":{"type":"integer"}}),
|
|
483
|
+
json!({"if":false,"then":{"type":"string"}}),
|
|
484
|
+
];
|
|
485
|
+
let docs = [
|
|
486
|
+
"2",
|
|
487
|
+
"3",
|
|
488
|
+
"-2",
|
|
489
|
+
"-3",
|
|
490
|
+
"0",
|
|
491
|
+
r#"{"type":"a","a":1}"#,
|
|
492
|
+
r#"{"type":"a"}"#,
|
|
493
|
+
r#"{"type":"b","b":1}"#,
|
|
494
|
+
r#"{"type":"b"}"#,
|
|
495
|
+
r#""hi""#,
|
|
496
|
+
"1",
|
|
497
|
+
"1.5",
|
|
498
|
+
"true",
|
|
499
|
+
"null",
|
|
500
|
+
];
|
|
501
|
+
for s in &schemas {
|
|
502
|
+
assert_parity(s.clone(), &docs);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[test]
|
|
507
|
+
fn unique_items_parity() {
|
|
508
|
+
let schemas = [
|
|
509
|
+
json!({"type":"array","uniqueItems":true}),
|
|
510
|
+
json!({"type":"array","uniqueItems":true,"items":{"type":"integer"}}),
|
|
511
|
+
json!({"type":"array","uniqueItems":true,"items":{"type":"object"}}),
|
|
512
|
+
];
|
|
513
|
+
let docs = [
|
|
514
|
+
r#"[1,2,3]"#,
|
|
515
|
+
r#"[1,1]"#,
|
|
516
|
+
r#"[1,1.0]"#,
|
|
517
|
+
r#"[1,2,2.0]"#,
|
|
518
|
+
r#"["a","b"]"#,
|
|
519
|
+
r#"["a","a"]"#,
|
|
520
|
+
r#"[{"a":1},{"a":1}]"#,
|
|
521
|
+
r#"[{"a":1},{"b":1}]"#,
|
|
522
|
+
r#"[{"a":1,"b":2},{"b":2,"a":1}]"#,
|
|
523
|
+
r#"[true,false,true]"#,
|
|
524
|
+
r#"[[1],[1]]"#,
|
|
525
|
+
r#"[[1],[2]]"#,
|
|
526
|
+
r#"[]"#,
|
|
527
|
+
];
|
|
528
|
+
for s in &schemas {
|
|
529
|
+
assert_parity(s.clone(), &docs);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
#[test]
|
|
534
|
+
fn unique_items_bignum_parity() {
|
|
535
|
+
// Exercises the exact cross-type number comparison (num_cmp) — serde_json's
|
|
536
|
+
// Value PartialEq would wrongly treat these as equal via f64 casting.
|
|
537
|
+
let schema = json!({"type":"array","uniqueItems":true});
|
|
538
|
+
let docs = [
|
|
539
|
+
r#"[9007199254740993,9007199254740992]"#, // distinct large ints
|
|
540
|
+
r#"[9007199254740993,9007199254740993]"#, // duplicate
|
|
541
|
+
r#"[9007199254740993,9007199254740992.0]"#, // exact: NOT equal
|
|
542
|
+
r#"[1.0,1]"#,
|
|
543
|
+
r#"[18446744073709551615,18446744073709551615]"#, // u64 max duplicate
|
|
544
|
+
];
|
|
545
|
+
assert_parity(schema, &docs);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#[test]
|
|
549
|
+
fn tuple_items_parity() {
|
|
550
|
+
let schemas = [
|
|
551
|
+
json!({"type":"array","items":[{"type":"string"},{"type":"integer"}]}),
|
|
552
|
+
json!({"type":"array","items":[{"type":"string"}],"additionalItems":false}),
|
|
553
|
+
json!({
|
|
554
|
+
"type":"array",
|
|
555
|
+
"items":[{"type":"string"},{"type":"integer"}],
|
|
556
|
+
"additionalItems":{"type":"boolean"}
|
|
557
|
+
}),
|
|
558
|
+
json!({"type":"array","items":[{"type":"string"}],"minItems":1}),
|
|
559
|
+
];
|
|
560
|
+
let docs = [
|
|
561
|
+
r#"["a",1]"#,
|
|
562
|
+
r#"["a"]"#,
|
|
563
|
+
r#"[1,"a"]"#,
|
|
564
|
+
r#"["a","b"]"#,
|
|
565
|
+
r#"["a","b","c"]"#,
|
|
566
|
+
r#"["a",1,true]"#,
|
|
567
|
+
r#"["a",1,2]"#,
|
|
568
|
+
r#"[]"#,
|
|
569
|
+
r#"["a","b",1]"#,
|
|
570
|
+
];
|
|
571
|
+
for s in &schemas {
|
|
572
|
+
assert_parity(s.clone(), &docs);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#[test]
|
|
577
|
+
fn contains_parity() {
|
|
578
|
+
let schemas = [
|
|
579
|
+
json!({"type":"array","contains":{"type":"integer"}}),
|
|
580
|
+
json!({"type":"array","contains":{"const":5}}),
|
|
581
|
+
json!({"type":"array","contains":{"type":"string"},"minItems":1}),
|
|
582
|
+
];
|
|
583
|
+
let docs = [
|
|
584
|
+
r#"[1]"#,
|
|
585
|
+
r#"[1.5]"#,
|
|
586
|
+
r#"["a",1]"#,
|
|
587
|
+
r#"["a","b"]"#,
|
|
588
|
+
r#"[5]"#,
|
|
589
|
+
r#"[4,6]"#,
|
|
590
|
+
r#"[]"#,
|
|
591
|
+
r#"[{}]"#,
|
|
592
|
+
];
|
|
593
|
+
for s in &schemas {
|
|
594
|
+
assert_parity(s.clone(), &docs);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
#[test]
|
|
599
|
+
fn dependencies_parity() {
|
|
600
|
+
let schemas = [
|
|
601
|
+
json!({"type":"object","dependencies":{"a":["b"]}}),
|
|
602
|
+
json!({"type":"object","dependencies":{"a":["b","c"]}}),
|
|
603
|
+
json!({"type":"object","dependencies":{"a":[],"b":["a"]}}),
|
|
604
|
+
];
|
|
605
|
+
let docs = [
|
|
606
|
+
r#"{"a":1,"b":2}"#,
|
|
607
|
+
r#"{"a":1}"#,
|
|
608
|
+
r#"{"b":2}"#,
|
|
609
|
+
r#"{}"#,
|
|
610
|
+
r#"{"a":1,"b":2,"c":3}"#,
|
|
611
|
+
r#"{"a":1,"c":3}"#,
|
|
612
|
+
r#"{"b":1,"a":2}"#,
|
|
613
|
+
];
|
|
614
|
+
for s in &schemas {
|
|
615
|
+
assert_parity(s.clone(), &docs);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
#[test]
|
|
620
|
+
fn ref_parity() {
|
|
621
|
+
let schemas = [
|
|
622
|
+
json!({
|
|
623
|
+
"definitions": {"positive": {"type":"number","minimum":0}},
|
|
624
|
+
"$ref":"#/definitions/positive"
|
|
625
|
+
}),
|
|
626
|
+
json!({
|
|
627
|
+
"$defs": {
|
|
628
|
+
"user": {
|
|
629
|
+
"type":"object",
|
|
630
|
+
"required":["id"],
|
|
631
|
+
"properties":{"id":{"type":"integer"}}
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
"type":"object",
|
|
635
|
+
"properties":{
|
|
636
|
+
"owner":{"$ref":"#/$defs/user"},
|
|
637
|
+
"admin":{"$ref":"#/$defs/user"}
|
|
638
|
+
}
|
|
639
|
+
}),
|
|
640
|
+
json!({
|
|
641
|
+
"definitions": {
|
|
642
|
+
"a": {"type":"string"},
|
|
643
|
+
"b": {"allOf":[{"$ref":"#/definitions/a"},{"minLength":2}]}
|
|
644
|
+
},
|
|
645
|
+
"$ref":"#/definitions/b"
|
|
646
|
+
}),
|
|
647
|
+
json!({
|
|
648
|
+
"type":"object",
|
|
649
|
+
"properties":{"x":{"type":"object","properties":{"y":{"type":"string"}}}},
|
|
650
|
+
"$ref":"#/properties/x/properties/y"
|
|
651
|
+
}),
|
|
652
|
+
];
|
|
653
|
+
let docs = [
|
|
654
|
+
"1",
|
|
655
|
+
"-1",
|
|
656
|
+
"0",
|
|
657
|
+
"5.5",
|
|
658
|
+
r#""s""#,
|
|
659
|
+
r#"{"id":1}"#,
|
|
660
|
+
r#"{"id":"x"}"#,
|
|
661
|
+
r#"{"owner":{"id":1},"admin":{"id":2}}"#,
|
|
662
|
+
r#"{"owner":{"id":1},"admin":{}}"#,
|
|
663
|
+
r#""abc""#,
|
|
664
|
+
r#""a""#,
|
|
665
|
+
r#""""#,
|
|
666
|
+
r#""x""#,
|
|
667
|
+
"123",
|
|
668
|
+
];
|
|
669
|
+
for s in &schemas {
|
|
670
|
+
assert_parity(s.clone(), &docs);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
#[test]
|
|
675
|
+
fn dollar_schema_guard() {
|
|
676
|
+
// Non-draft-07 `$schema` declarations fall back (semantics diverge).
|
|
677
|
+
for ss in [
|
|
678
|
+
"http://json-schema.org/draft-04/schema#",
|
|
679
|
+
"http://json-schema.org/draft-06/schema#",
|
|
680
|
+
"https://json-schema.org/draft/2020-12/schema",
|
|
681
|
+
"https://json-schema.org/draft/2019-09/schema",
|
|
682
|
+
] {
|
|
683
|
+
let schema = json!({"$schema": ss, "type": "object"});
|
|
684
|
+
assert!(compile(&schema).is_err(), "$schema={ss} must fall back");
|
|
685
|
+
}
|
|
686
|
+
// draft-07 (with or without the URL form) compiles.
|
|
687
|
+
for ss in [
|
|
688
|
+
"http://json-schema.org/draft-07/schema#",
|
|
689
|
+
"https://json-schema.org/draft-07/schema",
|
|
690
|
+
] {
|
|
691
|
+
let schema = json!({"$schema": ss, "type": "object"});
|
|
692
|
+
assert!(compile(&schema).is_ok(), "$schema={ss} must compile");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
#[test]
|
|
697
|
+
fn draft07_unknown_keywords_ignored() {
|
|
698
|
+
// 2020-12-only keywords are UNKNOWN to draft-07, so the reference ignores
|
|
699
|
+
// them; the fast path must too (compile + behave identically).
|
|
700
|
+
for schema in [
|
|
701
|
+
json!({"type":"object","prefixItems":[{"type":"string"}]}),
|
|
702
|
+
json!({"type":"array","unevaluatedItems":false}),
|
|
703
|
+
json!({"type":"object","unevaluatedProperties":false}),
|
|
704
|
+
json!({"type":"object","dependentRequired":{"a":["b"]}}),
|
|
705
|
+
json!({"type":"array","minContains":1}),
|
|
706
|
+
] {
|
|
707
|
+
assert!(
|
|
708
|
+
compile(&schema).is_ok(),
|
|
709
|
+
"should compile (ignored keyword): {schema}"
|
|
710
|
+
);
|
|
711
|
+
assert_parity(schema, &[r#"{}"#, r#"[]"#, r#"[1,2]"#, r#"{"a":1}"#]);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ── detailed error reporting (fast path) ────────────────────────────────
|
|
716
|
+
|
|
717
|
+
#[test]
|
|
718
|
+
fn error_reporting_paths_and_keywords() {
|
|
719
|
+
let schema = json!({
|
|
720
|
+
"type":"object",
|
|
721
|
+
"required":["id","name"],
|
|
722
|
+
"properties":{
|
|
723
|
+
"id":{"type":"integer"},
|
|
724
|
+
"name":{"type":"string","minLength":2,"pattern":"^[a-z]+$"},
|
|
725
|
+
"tags":{"type":"array","items":{"type":"string"},"uniqueItems":true}
|
|
726
|
+
},
|
|
727
|
+
"additionalProperties":false
|
|
728
|
+
});
|
|
729
|
+
let fast = compile(&schema).unwrap();
|
|
730
|
+
|
|
731
|
+
// valid -> no errors
|
|
732
|
+
assert!(fast
|
|
733
|
+
.validate_errors(br#"{"id":1,"name":"ab","tags":["x","y"]}"#, usize::MAX)
|
|
734
|
+
.is_empty());
|
|
735
|
+
|
|
736
|
+
// missing required -> keyword required, root pointer
|
|
737
|
+
let errs = fast.validate_errors(br#"{"id":1}"#, usize::MAX);
|
|
738
|
+
let req = errs.iter().find(|e| e.keyword == "required").unwrap();
|
|
739
|
+
assert_eq!(req.instance_path, "");
|
|
740
|
+
assert!(req.message.contains("name"));
|
|
741
|
+
|
|
742
|
+
// wrong type at /id
|
|
743
|
+
let errs = fast.validate_errors(br#"{"id":"x","name":"ab"}"#, usize::MAX);
|
|
744
|
+
let t = errs.iter().find(|e| e.keyword == "type").unwrap();
|
|
745
|
+
assert_eq!(t.instance_path, "/id");
|
|
746
|
+
|
|
747
|
+
// pattern at /name
|
|
748
|
+
let errs = fast.validate_errors(br#"{"id":1,"name":"AB"}"#, usize::MAX);
|
|
749
|
+
let p = errs.iter().find(|e| e.keyword == "pattern").unwrap();
|
|
750
|
+
assert_eq!(p.instance_path, "/name");
|
|
751
|
+
assert!(p.schema_path.ends_with("/properties/name/pattern"));
|
|
752
|
+
|
|
753
|
+
// additionalProperties at the offending key
|
|
754
|
+
let errs = fast.validate_errors(br#"{"id":1,"name":"ab","extra":true}"#, usize::MAX);
|
|
755
|
+
let ap = errs
|
|
756
|
+
.iter()
|
|
757
|
+
.find(|e| e.keyword == "additionalProperties")
|
|
758
|
+
.unwrap();
|
|
759
|
+
assert_eq!(ap.instance_path, "/extra");
|
|
760
|
+
|
|
761
|
+
// uniqueItems at /tags
|
|
762
|
+
let errs = fast.validate_errors(br#"{"id":1,"name":"ab","tags":["x","x"]}"#, usize::MAX);
|
|
763
|
+
let u = errs.iter().find(|e| e.keyword == "uniqueItems").unwrap();
|
|
764
|
+
assert_eq!(u.instance_path, "/tags");
|
|
765
|
+
|
|
766
|
+
// multi-error collection: every failing property is reported
|
|
767
|
+
let errs = fast.validate_errors(br#"{"id":"x","name":"AB","extra":1}"#, usize::MAX);
|
|
768
|
+
let kw: Vec<&str> = errs.iter().map(|e| e.keyword.as_str()).collect();
|
|
769
|
+
assert!(
|
|
770
|
+
kw.contains(&"type") && kw.contains(&"pattern") && kw.contains(&"additionalProperties"),
|
|
771
|
+
"expected type+pattern+additionalProperties errors, got {kw:?}"
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
// max_errors=1 returns exactly the first error
|
|
775
|
+
let first = fast.validate_errors(br#"{"id":"x","name":""}"#, 1);
|
|
776
|
+
assert_eq!(first.len(), 1);
|
|
777
|
+
|
|
778
|
+
// RFC 6901 escaping: a key with a slash / tilde in the pointer
|
|
779
|
+
let schema2 = json!({
|
|
780
|
+
"type":"object",
|
|
781
|
+
"properties":{"a/b":{"type":"integer"},"c~d":{"type":"integer"}}
|
|
782
|
+
});
|
|
783
|
+
let fast2 = compile(&schema2).unwrap();
|
|
784
|
+
let errs = fast2.validate_errors(br#"{"a/b":"x"}"#, usize::MAX);
|
|
785
|
+
assert!(errs.iter().any(|e| e.instance_path == "/a~1b"));
|
|
786
|
+
let errs = fast2.validate_errors(br#"{"c~d":"x"}"#, usize::MAX);
|
|
787
|
+
assert!(errs.iter().any(|e| e.instance_path == "/c~0d"));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
#[test]
|
|
791
|
+
fn error_reporting_matches_validation_bool() {
|
|
792
|
+
// validate_errors(empty) must agree exactly with is_valid_bytes.
|
|
793
|
+
let schemas = [
|
|
794
|
+
json!({
|
|
795
|
+
"type":"object",
|
|
796
|
+
"required":["a"],
|
|
797
|
+
"properties":{"a":{"type":"number","multipleOf":2}},
|
|
798
|
+
"additionalProperties":false
|
|
799
|
+
}),
|
|
800
|
+
json!({"anyOf":[{"type":"string"},{"type":"integer"}]}),
|
|
801
|
+
json!({"type":"array","items":{"type":"string"},"minItems":1}),
|
|
802
|
+
];
|
|
803
|
+
let docs = [
|
|
804
|
+
r#"{"a":4}"#,
|
|
805
|
+
r#"{"a":3}"#,
|
|
806
|
+
r#"{"a":"x"}"#,
|
|
807
|
+
r#"{"b":1}"#,
|
|
808
|
+
r#""s""#,
|
|
809
|
+
"1",
|
|
810
|
+
r#"["a"]"#,
|
|
811
|
+
"[]",
|
|
812
|
+
r#"["a",1]"#,
|
|
813
|
+
"not json",
|
|
814
|
+
];
|
|
815
|
+
for s in &schemas {
|
|
816
|
+
let fast = compile(s).unwrap();
|
|
817
|
+
for doc in &docs {
|
|
818
|
+
let ok = fast.is_valid_bytes(doc.as_bytes());
|
|
819
|
+
let errs = fast.validate_errors(doc.as_bytes(), usize::MAX);
|
|
820
|
+
assert_eq!(ok, errs.is_empty(), "schema={s} doc={doc}");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
#[test]
|
|
826
|
+
fn invalid_utf8_and_malformed_bytes_return_false() {
|
|
827
|
+
// The fast path is byte-oriented; it must reject structurally invalid or
|
|
828
|
+
// non-UTF-8-leading documents without panicking (matching the DOM path's
|
|
829
|
+
// parse failure → false).
|
|
830
|
+
let fast = compile(&bench_schema()).unwrap();
|
|
831
|
+
|
|
832
|
+
// Non-UTF-8 leading bytes cannot start any JSON value.
|
|
833
|
+
assert!(!fast.is_valid_bytes(&[0xFF, 0xFE]));
|
|
834
|
+
assert!(!fast.is_valid_bytes(&[0x80]));
|
|
835
|
+
assert!(!fast.is_valid_bytes(&[0xC0, 0xAF]));
|
|
836
|
+
|
|
837
|
+
// Truncated structural input.
|
|
838
|
+
assert!(!fast.is_valid_bytes(b"{\"id\":1"));
|
|
839
|
+
assert!(!fast.is_valid_bytes(b"{\"id\":"));
|
|
840
|
+
assert!(!fast.is_valid_bytes(b"[1,2"));
|
|
841
|
+
|
|
842
|
+
// Trailing garbage after a valid document (must consume ALL bytes).
|
|
843
|
+
assert!(!fast.is_valid_bytes(b"{\"id\":1}\x00"));
|
|
844
|
+
assert!(!fast.is_valid_bytes(
|
|
845
|
+
b"{\"id\":1,\"name\":\"a\",\"active\":true,\"score\":1,\"tags\":[],\"nested\":{\"version\":1,\"createdAt\":\"c\"}} garbage"
|
|
846
|
+
));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
#[test]
|
|
850
|
+
fn rfc8259_strictness_matches_sonic_gate() {
|
|
851
|
+
// The fast path now serves as the RFC-8259 well-formedness gate for
|
|
852
|
+
// fast-path schemas in the ingress pipeline (pipeline.rs stage 6): a pass
|
|
853
|
+
// means BOTH well-formed JSON AND schema-conforming, so the separate sonic
|
|
854
|
+
// `json_valid_bytes` pass is skipped on the happy path. The safety
|
|
855
|
+
// property this pins: the fast path must NEVER accept a body the strict
|
|
856
|
+
// sonic gate rejects (`!sonic ⇒ !fast`). The one gap the walk closes is
|
|
857
|
+
// raw control bytes (< 0x20) in strings; sonic is lenient on exactly what
|
|
858
|
+
// the walk is lenient on (bad `\uXXXX` hex, lone surrogates, invalid
|
|
859
|
+
// UTF-8, numeric overflow) — so this corpus must not diverge.
|
|
860
|
+
let fast = compile(&bench_schema()).unwrap();
|
|
861
|
+
|
|
862
|
+
let corpus: &[&[u8]] = &[
|
|
863
|
+
// Raw control bytes in string VALUES (sonic rejects → fast must too).
|
|
864
|
+
b"{\"id\":1,\"name\":\"a\x01b\"}",
|
|
865
|
+
b"{\"id\":1,\"name\":\"a\x00\"}",
|
|
866
|
+
b"{\"id\":1,\"name\":\"\x1f\"}",
|
|
867
|
+
b"{\"id\":1,\"name\":\"a\tb\"}", // raw tab inside a string
|
|
868
|
+
// Raw control bytes in KEYS (both route through raw_string).
|
|
869
|
+
b"{\"id\x09\":1}",
|
|
870
|
+
b"{\"\x00\":1}",
|
|
871
|
+
// Other malformed-JSON classes sonic rejects.
|
|
872
|
+
b"{\"id\":1,}",
|
|
873
|
+
b"[1,]",
|
|
874
|
+
b"{\"id\":1",
|
|
875
|
+
b"{\"id\":}",
|
|
876
|
+
b"-01",
|
|
877
|
+
b"1.",
|
|
878
|
+
b"01",
|
|
879
|
+
b"",
|
|
880
|
+
b"{\"id\":1} {\"id\":2}",
|
|
881
|
+
b"{\"id\":1}\x00",
|
|
882
|
+
// Sonic-lenient inputs the walk must ALSO accept (parity, not strict).
|
|
883
|
+
b"{\"id\":1,\"name\":\"\\uZZZZ\"}", // bad \u hex
|
|
884
|
+
b"{\"id\":1,\"name\":\"\\uD800\"}", // lone surrogate
|
|
885
|
+
b"{\"id\":1,\"name\":\"a\xFFb\"}", // invalid UTF-8 in a string
|
|
886
|
+
b"1e999", // numeric overflow
|
|
887
|
+
// Valid AND schema-conforming positive control (bench_schema requires
|
|
888
|
+
// id/name/active/score/tags/nested — the other two- and three-field
|
|
889
|
+
// docs above are well-formed but non-conforming, which the pipeline
|
|
890
|
+
// splits as 422 via the sonic re-check).
|
|
891
|
+
b"{\"id\":1,\"name\":\"user_1\",\"active\":true,\"score\":1.25,\"tags\":[\"alpha\",\"beta\"],\"nested\":{\"version\":1,\"createdAt\":\"2026-01-01T00:00:00Z\"}}",
|
|
892
|
+
b"\t\r\n {}", // whitespace control OUTSIDE strings is valid
|
|
893
|
+
];
|
|
894
|
+
|
|
895
|
+
for &doc in corpus {
|
|
896
|
+
let sonic = crate::json::json_ops::json_valid_bytes(doc);
|
|
897
|
+
let got = fast.is_valid_bytes(doc);
|
|
898
|
+
if !sonic {
|
|
899
|
+
assert!(
|
|
900
|
+
!got,
|
|
901
|
+
"fast path accepted a body the sonic gate rejects: {:?}",
|
|
902
|
+
String::from_utf8_lossy(doc)
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// Positive control: well-formed + conforming must pass the fast path.
|
|
907
|
+
assert!(fast.is_valid_bytes(
|
|
908
|
+
b"{\"id\":1,\"name\":\"user_1\",\"active\":true,\"score\":1.25,\"tags\":[\"alpha\",\"beta\"],\"nested\":{\"version\":1,\"createdAt\":\"2026-01-01T00:00:00Z\"}}"
|
|
909
|
+
));
|
|
910
|
+
// Well-formed but non-conforming (missing required field) may reject —
|
|
911
|
+
// the pipeline splits that as 422 via the sonic re-check.
|
|
912
|
+
assert!(!fast.is_valid_bytes(b"{\"id\":1}"));
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ── format: "email" (zero-DOM fast path) ─────────────────────────
|
|
916
|
+
|
|
917
|
+
#[test]
|
|
918
|
+
fn email_format_matches_reference_corpus() {
|
|
919
|
+
let schema = json!({
|
|
920
|
+
"type": "object",
|
|
921
|
+
"required": ["email"],
|
|
922
|
+
"properties": {
|
|
923
|
+
"email": { "type": "string", "format": "email" }
|
|
924
|
+
},
|
|
925
|
+
"additionalProperties": false
|
|
926
|
+
});
|
|
927
|
+
let docs = [
|
|
928
|
+
// ── valid ──
|
|
929
|
+
r#"{"email":"a@b.com"}"#,
|
|
930
|
+
r#"{"email":"john.doe+tag@example.co.uk"}"#,
|
|
931
|
+
r#"{"email":"user@sub.example.com"}"#,
|
|
932
|
+
r#"{"email":"user@[IPv6:2001:db8::1]"}"#,
|
|
933
|
+
r#"{"email":"user@[192.168.0.1]"}"#,
|
|
934
|
+
r#"{"email":"user@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]"}"#,
|
|
935
|
+
r#"{"email":"user@xn--bcher-kva.com"}"#, // valid punycode domain
|
|
936
|
+
r#"{"email":"a.b.c.d.e.f@example.com"}"#,
|
|
937
|
+
r#"{"email":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com"}"#, // 64-char local
|
|
938
|
+
r#"{"email":"user@xn--11b2ezcw70k"}"#, // ZWJ after virama (valid punycode)
|
|
939
|
+
r#"{"email":"user@xn--ll-0ea"}"#, // punycode with valid middle dot context
|
|
940
|
+
// ── invalid: domain shape ──
|
|
941
|
+
r#"{"email":"not-an-email"}"#,
|
|
942
|
+
r#"{"email":"a@"}"#,
|
|
943
|
+
r#"{"email":"@b.com"}"#,
|
|
944
|
+
r#"{"email":"a b@c.com"}"#,
|
|
945
|
+
r#"{"email":"a@b c.com"}"#,
|
|
946
|
+
r#"{"email":"a@-b.com"}"#,
|
|
947
|
+
r#"{"email":"a@b-.com"}"#,
|
|
948
|
+
r#"{"email":"a@b..com"}"#,
|
|
949
|
+
r#"{"email":"a@b.com."}"#,
|
|
950
|
+
r#"{"email":"a@ex--ample.com"}"#, // hyphens 3rd+4th, not punycode
|
|
951
|
+
r#"{"email":"a@xn--example.com"}"#, // invalid punycode
|
|
952
|
+
r#"{"email":"a@xn--x"}"#, // too-short punycode
|
|
953
|
+
r#"{"email":"a@[IPv6:zzzz::1]"}"#,
|
|
954
|
+
r#"{"email":"a@[999.999.999.999]"}"#,
|
|
955
|
+
r#"{"email":"a@[not-a-literal]"}"#,
|
|
956
|
+
r#"{"email":"a@xn--hello-zed"}"#, // punycode beginning with nonspacing mark
|
|
957
|
+
r#"{"email":"a@XN--aa---o47jg78q"}"#, // uppercase punycode prefix rejected
|
|
958
|
+
r#"{"email":"a@example-.com"}"#,
|
|
959
|
+
// ── invalid: local part / escapes ──
|
|
960
|
+
r#"{"email":"a\u0040b.com"}"#, // JSON escape decodes to a@b.com (valid) — decode path
|
|
961
|
+
r#"{"email":"a\\@b.com"}"#, // backslash in local part
|
|
962
|
+
r#"{"email":"héllo@example.com"}"#, // non-ASCII local part rejected (format:email)
|
|
963
|
+
// wrong root type
|
|
964
|
+
r#"42"#,
|
|
965
|
+
];
|
|
966
|
+
assert_parity(schema, &docs);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
#[test]
|
|
970
|
+
fn email_only_schema_compiles_and_validates() {
|
|
971
|
+
// A schema with ONLY format:email (no minLength/maxLength) must still
|
|
972
|
+
// engage the fast path's string node.
|
|
973
|
+
let schema = json!({ "type": "string", "format": "email" });
|
|
974
|
+
let fast = compile(&schema).expect("format:email must compile on the fast path");
|
|
975
|
+
let v = jsonschema::options()
|
|
976
|
+
.with_draft(jsonschema::Draft::Draft7)
|
|
977
|
+
.should_validate_formats(true)
|
|
978
|
+
.build(&schema)
|
|
979
|
+
.unwrap();
|
|
980
|
+
for doc in [r#""a@b.com""#, r#""not-an-email""#, r#"42"#] {
|
|
981
|
+
let expected = serde_json::from_str::<Value>(doc)
|
|
982
|
+
.map(|x| v.is_valid(&x))
|
|
983
|
+
.unwrap_or(false);
|
|
984
|
+
assert_eq!(fast.is_valid_bytes(doc.as_bytes()), expected, "doc={doc}");
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
#[test]
|
|
989
|
+
fn other_formats_and_unknown_formats_fall_back() {
|
|
990
|
+
for f in [
|
|
991
|
+
"idn-email",
|
|
992
|
+
"date-time",
|
|
993
|
+
"uri",
|
|
994
|
+
"uuid",
|
|
995
|
+
"ipv4",
|
|
996
|
+
"regex",
|
|
997
|
+
"unknown",
|
|
998
|
+
] {
|
|
999
|
+
let schema = json!({ "type": "string", "format": f });
|
|
1000
|
+
assert!(
|
|
1001
|
+
compile(&schema).is_err(),
|
|
1002
|
+
"format {f:?} must fall back to the DOM path"
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
#[test]
|
|
1008
|
+
fn email_format_property_parity_with_reference() {
|
|
1009
|
+
// Deterministic pseudo-random emails: cross-check the fast path against the
|
|
1010
|
+
// authoritative jsonschema DOM validator. ANY divergence fails — this is
|
|
1011
|
+
// the byte-parity gate that justifies engaging the fast path for the
|
|
1012
|
+
// ingress schema (which uses format:email).
|
|
1013
|
+
let schema = json!({ "type": "string", "format": "email" });
|
|
1014
|
+
let fast = compile(&schema).unwrap();
|
|
1015
|
+
let v = jsonschema::options()
|
|
1016
|
+
.with_draft(jsonschema::Draft::Draft7)
|
|
1017
|
+
.should_validate_formats(true)
|
|
1018
|
+
.build(&schema)
|
|
1019
|
+
.unwrap();
|
|
1020
|
+
|
|
1021
|
+
// Simple LCG (no rand dep needed in tests).
|
|
1022
|
+
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
|
|
1023
|
+
let mut next = move || {
|
|
1024
|
+
state = state
|
|
1025
|
+
.wrapping_mul(6364136223846793005)
|
|
1026
|
+
.wrapping_add(1442695040888963407);
|
|
1027
|
+
(state >> 32) as u32
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
let alphabet_local: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789._%+-";
|
|
1031
|
+
let alphabet_domain: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789-.";
|
|
1032
|
+
let alphabet_bad: &[u8] = b" @!?*\\/()";
|
|
1033
|
+
|
|
1034
|
+
let mut checked = 0u32;
|
|
1035
|
+
for _ in 0..3000 {
|
|
1036
|
+
// Local part: 0..=24 chars, mostly valid + occasional bad char.
|
|
1037
|
+
let mut local = String::new();
|
|
1038
|
+
let llen = (next() % 26) as usize;
|
|
1039
|
+
for _ in 0..llen {
|
|
1040
|
+
let pick = next() % 100;
|
|
1041
|
+
let b = if pick < 88 {
|
|
1042
|
+
alphabet_local[(next() % alphabet_local.len() as u32) as usize]
|
|
1043
|
+
} else {
|
|
1044
|
+
alphabet_bad[(next() % alphabet_bad.len() as u32) as usize]
|
|
1045
|
+
};
|
|
1046
|
+
local.push(b as char);
|
|
1047
|
+
}
|
|
1048
|
+
// Domain: 0..=40 chars, occasionally a space or trailing dot.
|
|
1049
|
+
let mut domain = String::new();
|
|
1050
|
+
let dlen = (next() % 42) as usize;
|
|
1051
|
+
for _ in 0..dlen {
|
|
1052
|
+
let pick = next() % 100;
|
|
1053
|
+
let b = if pick < 92 {
|
|
1054
|
+
alphabet_domain[(next() % alphabet_domain.len() as u32) as usize]
|
|
1055
|
+
} else {
|
|
1056
|
+
b' '
|
|
1057
|
+
};
|
|
1058
|
+
domain.push(b as char);
|
|
1059
|
+
}
|
|
1060
|
+
let mut email = local;
|
|
1061
|
+
email.push('@');
|
|
1062
|
+
email.push_str(&domain);
|
|
1063
|
+
|
|
1064
|
+
let doc = serde_json::to_string(&email).unwrap();
|
|
1065
|
+
let expected = serde_json::from_str::<Value>(&doc)
|
|
1066
|
+
.map(|x| v.is_valid(&x))
|
|
1067
|
+
.unwrap_or(false);
|
|
1068
|
+
let got = fast.is_valid_bytes(doc.as_bytes());
|
|
1069
|
+
assert_eq!(got, expected, "email={email:?}");
|
|
1070
|
+
checked += 1;
|
|
1071
|
+
}
|
|
1072
|
+
assert!(checked >= 3000);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ── hostile nesting depth (stack-overflow guard) ─────────────────
|
|
1076
|
+
|
|
1077
|
+
#[test]
|
|
1078
|
+
// The repeated `push` of the same structural literal is deliberate — it
|
|
1079
|
+
// builds a deeply-nested document to exercise the stack-overflow guard.
|
|
1080
|
+
#[allow(clippy::same_item_push)]
|
|
1081
|
+
fn deeply_nested_documents_are_bounded_not_crashing() {
|
|
1082
|
+
// A ~10K-deep document must be REJECTED, not recursed into a stack overflow
|
|
1083
|
+
// (a stack overflow aborts the whole process — panic=unwind + napi
|
|
1084
|
+
// catch_unwind do NOT catch it). The any-schema walks every nested value
|
|
1085
|
+
// via Cursor::skip_value, which is exactly the recursion path a hostile
|
|
1086
|
+
// body would drive.
|
|
1087
|
+
let any = FastNode::any();
|
|
1088
|
+
|
|
1089
|
+
let mut deep_arr = Vec::with_capacity(20_000);
|
|
1090
|
+
for _ in 0..10_000 {
|
|
1091
|
+
deep_arr.push(b'[');
|
|
1092
|
+
}
|
|
1093
|
+
deep_arr.push(b'0');
|
|
1094
|
+
for _ in 0..10_000 {
|
|
1095
|
+
deep_arr.push(b']');
|
|
1096
|
+
}
|
|
1097
|
+
assert!(!any.is_valid_bytes(&deep_arr));
|
|
1098
|
+
|
|
1099
|
+
let mut deep_obj = Vec::with_capacity(60_000);
|
|
1100
|
+
for _ in 0..10_000 {
|
|
1101
|
+
deep_obj.extend_from_slice(b"{\"a\":");
|
|
1102
|
+
}
|
|
1103
|
+
deep_obj.push(b'0');
|
|
1104
|
+
for _ in 0..10_000 {
|
|
1105
|
+
deep_obj.push(b'}');
|
|
1106
|
+
}
|
|
1107
|
+
assert!(!any.is_valid_bytes(&deep_obj));
|
|
1108
|
+
|
|
1109
|
+
// Sanity: an ordinary payload is still valid — the cap must not reject
|
|
1110
|
+
// normal documents.
|
|
1111
|
+
assert!(any.is_valid_bytes(b"{\"a\":[1,{\"b\":2}]}"));
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
#[test]
|
|
1115
|
+
fn deep_nesting_parity_with_reference() {
|
|
1116
|
+
// The fast path caps at MAX_DEPTH (128), matching sonic-rs (the ingress
|
|
1117
|
+
// gate) and serde_json (the DOM reference): deeper documents parse-fail in
|
|
1118
|
+
// the reference too, so validity must agree.
|
|
1119
|
+
let schema = json!({ "type": "object", "additionalProperties": true });
|
|
1120
|
+
let fast = compile(&schema).unwrap();
|
|
1121
|
+
let v = jsonschema::options()
|
|
1122
|
+
.with_draft(jsonschema::Draft::Draft7)
|
|
1123
|
+
.build(&schema)
|
|
1124
|
+
.unwrap();
|
|
1125
|
+
|
|
1126
|
+
// 100-deep (below the cap) and 200-deep (above it) — both must agree with
|
|
1127
|
+
// the reference.
|
|
1128
|
+
for depth in [100usize, 200] {
|
|
1129
|
+
let mut doc = String::new();
|
|
1130
|
+
for _ in 0..depth {
|
|
1131
|
+
doc.push_str("{\"a\":");
|
|
1132
|
+
}
|
|
1133
|
+
doc.push('0');
|
|
1134
|
+
for _ in 0..depth {
|
|
1135
|
+
doc.push('}');
|
|
1136
|
+
}
|
|
1137
|
+
let expected = serde_json::from_str::<Value>(&doc)
|
|
1138
|
+
.map(|x| v.is_valid(&x))
|
|
1139
|
+
.unwrap_or(false);
|
|
1140
|
+
let got = fast.is_valid_bytes(doc.as_bytes());
|
|
1141
|
+
assert_eq!(got, expected, "depth={depth}");
|
|
1142
|
+
}
|
|
1143
|
+
}
|