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,556 @@
|
|
|
1
|
+
// rust/json/fast_schema/compile.rs — Schema → AST compiler.
|
|
2
|
+
//
|
|
3
|
+
// Compiles a `serde_json::Value` schema into the zero-DOM `FastNode`
|
|
4
|
+
// representation. Returns `Err(())` for any keyword outside the supported
|
|
5
|
+
// subset so the caller can fall back to the jsonschema crate DOM path.
|
|
6
|
+
//
|
|
7
|
+
// Supported surface (draft-07 validation keywords): type, enum, const,
|
|
8
|
+
// multipleOf, minimum/maximum, exclusiveMinimum/exclusiveMaximum (numeric),
|
|
9
|
+
// minLength/maxLength, pattern, items (single + draft-07 tuple), additionalItems,
|
|
10
|
+
// minItems/maxItems, uniqueItems, contains, minProperties/maxProperties,
|
|
11
|
+
// required, properties, patternProperties, additionalProperties, dependencies
|
|
12
|
+
// (array-of-required-strings form), if/then/else, allOf, anyOf, oneOf, not,
|
|
13
|
+
// $ref (in-document JSON pointers: `#`, `#/definitions/*`, `#/$defs/*`, and any
|
|
14
|
+
// object/bool subschema), format: "email".
|
|
15
|
+
//
|
|
16
|
+
// Falls back (Err) for: propertyNames, dependencies with a schema value,
|
|
17
|
+
// draft-04/06 boolean exclusive bounds, formats != email, remote/dynamic/
|
|
18
|
+
// recursive/anchor `$ref`, non-draft-07 `$schema`, enum+const together, and any
|
|
19
|
+
// keyword we cannot honor under draft-07 semantics.
|
|
20
|
+
|
|
21
|
+
use rustc_hash::FxHashMap;
|
|
22
|
+
use serde_json::Value;
|
|
23
|
+
use std::collections::HashMap;
|
|
24
|
+
use std::sync::Arc;
|
|
25
|
+
|
|
26
|
+
use super::types::{
|
|
27
|
+
Additional, FastArray, FastComb, FastNode, FastNumber, FastObject, FastString, ValueConstraint,
|
|
28
|
+
T_ARR, T_BOOL, T_INT, T_NULL, T_NUM, T_OBJ, T_STR,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/// Compile a schema into the zero-DOM fast representation, or `Err(())` if the
|
|
32
|
+
/// schema uses a keyword outside the supported subset (caller falls back).
|
|
33
|
+
///
|
|
34
|
+
/// `Err(())` is a deliberate, internal-only signal (no user-facing error
|
|
35
|
+
/// payload); the caller maps it to the DOM fallback path.
|
|
36
|
+
#[allow(clippy::result_unit_err)]
|
|
37
|
+
pub fn compile(schema: &Value) -> Result<FastNode, ()> {
|
|
38
|
+
// `$schema` guard: the fast path implements draft-07 semantics. A schema
|
|
39
|
+
// declaring a different draft diverges (boolean exclusive bounds,
|
|
40
|
+
// prefixItems/unevaluated*, `$ref` siblings, …) — fall back. Without a
|
|
41
|
+
// `$schema` the reference defaults to draft-07, matching us.
|
|
42
|
+
if let Some(Value::String(ss)) = schema.as_object().and_then(|m| m.get("$schema")) {
|
|
43
|
+
if !ss.contains("draft-07") {
|
|
44
|
+
return Err(());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Index every object/bool subschema by JSON pointer for `$ref` resolution.
|
|
48
|
+
let mut refs: HashMap<String, &Value> = HashMap::new();
|
|
49
|
+
refs.insert(String::new(), schema);
|
|
50
|
+
collect_refs(schema, "", &mut refs);
|
|
51
|
+
let mut ctx = CompileCtx {
|
|
52
|
+
refs,
|
|
53
|
+
cache: HashMap::new(),
|
|
54
|
+
in_progress: Vec::new(),
|
|
55
|
+
};
|
|
56
|
+
compile_node(schema, "", &mut ctx)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// `$ref` resolution state: subschema index, compiled-target cache, and a
|
|
60
|
+
/// cycle-detection stack (cyclic `$ref` graphs fall back to the DOM path).
|
|
61
|
+
struct CompileCtx<'a> {
|
|
62
|
+
refs: HashMap<String, &'a Value>,
|
|
63
|
+
cache: HashMap<String, Arc<FastNode>>,
|
|
64
|
+
in_progress: Vec<String>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Register every object/bool value in the document at its JSON pointer.
|
|
68
|
+
fn collect_refs<'a>(v: &'a Value, ptr: &str, out: &mut HashMap<String, &'a Value>) {
|
|
69
|
+
match v {
|
|
70
|
+
Value::Object(map) => {
|
|
71
|
+
if !ptr.is_empty() {
|
|
72
|
+
out.insert(ptr.to_string(), v);
|
|
73
|
+
}
|
|
74
|
+
for (k, val) in map {
|
|
75
|
+
let child = if ptr.is_empty() {
|
|
76
|
+
format!("/{}", escape_pointer(k))
|
|
77
|
+
} else {
|
|
78
|
+
format!("{}/{}", ptr, escape_pointer(k))
|
|
79
|
+
};
|
|
80
|
+
collect_refs(val, &child, out);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
Value::Bool(_) => {
|
|
84
|
+
if !ptr.is_empty() {
|
|
85
|
+
out.insert(ptr.to_string(), v);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
Value::Array(arr) => {
|
|
89
|
+
for (i, val) in arr.iter().enumerate() {
|
|
90
|
+
collect_refs(val, &format!("{}/{}", ptr, i), out);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
_ => {}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// RFC 6901 pointer escaping for schema-path keys.
|
|
98
|
+
fn escape_pointer(s: &str) -> String {
|
|
99
|
+
s.replace('~', "~0").replace('/', "~1")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn compile_node(schema: &Value, path: &str, ctx: &mut CompileCtx<'_>) -> Result<FastNode, ()> {
|
|
103
|
+
match schema {
|
|
104
|
+
Value::Bool(true) => Ok(FastNode::any()),
|
|
105
|
+
Value::Bool(false) => Ok(FastNode::never()),
|
|
106
|
+
Value::Object(map) => {
|
|
107
|
+
// `$ref`: under draft-07 sibling keywords are ignored, so resolve
|
|
108
|
+
// and return the referenced node directly.
|
|
109
|
+
if let Some(Value::String(r)) = map.get("$ref") {
|
|
110
|
+
return compile_ref(r, ctx).map(|n| n.as_ref().clone());
|
|
111
|
+
}
|
|
112
|
+
// Keywords this fast path cannot honor under draft-07.
|
|
113
|
+
if map.contains_key("propertyNames") {
|
|
114
|
+
return Err(());
|
|
115
|
+
}
|
|
116
|
+
// `dependencies` schema form is unsupported; only array-of-strings.
|
|
117
|
+
if let Some(Value::Object(deps)) = map.get("dependencies") {
|
|
118
|
+
if deps.values().any(|v| !v.is_array()) {
|
|
119
|
+
return Err(());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// `format`: only "email" is byte-parity on the fast path.
|
|
123
|
+
if let Some(Value::String(f)) = map.get("format") {
|
|
124
|
+
if f != "email" {
|
|
125
|
+
return Err(());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Draft-07 exclusive bounds are numeric; boolean form is draft-04/06.
|
|
129
|
+
for k in ["exclusiveMinimum", "exclusiveMaximum"] {
|
|
130
|
+
if let Some(v) = map.get(k) {
|
|
131
|
+
if !v.is_number() {
|
|
132
|
+
return Err(());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// enum + const together: unusual combination, keep on the DOM path.
|
|
137
|
+
if map.contains_key("enum") && map.contains_key("const") {
|
|
138
|
+
return Err(());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let mut node = FastNode {
|
|
142
|
+
schema_path: Some(path.to_string()),
|
|
143
|
+
..FastNode::default()
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if let Some(t) = map.get("type") {
|
|
147
|
+
node.types = compile_type(t)?;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if [
|
|
151
|
+
"required",
|
|
152
|
+
"properties",
|
|
153
|
+
"patternProperties",
|
|
154
|
+
"additionalProperties",
|
|
155
|
+
"minProperties",
|
|
156
|
+
"maxProperties",
|
|
157
|
+
"dependencies",
|
|
158
|
+
]
|
|
159
|
+
.iter()
|
|
160
|
+
.any(|k| map.contains_key(*k))
|
|
161
|
+
{
|
|
162
|
+
node.obj = Some(Arc::new(compile_object_constraints(map, path, ctx)?));
|
|
163
|
+
}
|
|
164
|
+
if [
|
|
165
|
+
"items",
|
|
166
|
+
"additionalItems",
|
|
167
|
+
"minItems",
|
|
168
|
+
"maxItems",
|
|
169
|
+
"uniqueItems",
|
|
170
|
+
"contains",
|
|
171
|
+
]
|
|
172
|
+
.iter()
|
|
173
|
+
.any(|k| map.contains_key(*k))
|
|
174
|
+
{
|
|
175
|
+
node.arr = Some(Arc::new(compile_array_constraints(map, path, ctx)?));
|
|
176
|
+
}
|
|
177
|
+
if map.contains_key("minLength")
|
|
178
|
+
|| map.contains_key("maxLength")
|
|
179
|
+
|| map.contains_key("pattern")
|
|
180
|
+
|| map.contains_key("format")
|
|
181
|
+
{
|
|
182
|
+
node.str = Some(Arc::new(compile_string_constraints(map)?));
|
|
183
|
+
}
|
|
184
|
+
if [
|
|
185
|
+
"minimum",
|
|
186
|
+
"maximum",
|
|
187
|
+
"exclusiveMinimum",
|
|
188
|
+
"exclusiveMaximum",
|
|
189
|
+
"multipleOf",
|
|
190
|
+
]
|
|
191
|
+
.iter()
|
|
192
|
+
.any(|k| map.contains_key(*k))
|
|
193
|
+
{
|
|
194
|
+
node.num = Some(Arc::new(compile_number_constraints(map)?));
|
|
195
|
+
}
|
|
196
|
+
if ["allOf", "anyOf", "oneOf", "not", "if", "then", "else"]
|
|
197
|
+
.iter()
|
|
198
|
+
.any(|k| map.contains_key(*k))
|
|
199
|
+
{
|
|
200
|
+
node.comb = Some(Arc::new(compile_combinators(map, path, ctx)?));
|
|
201
|
+
}
|
|
202
|
+
if let Some(v) = map.get("enum") {
|
|
203
|
+
node.value = Some(compile_enum(v)?);
|
|
204
|
+
} else if let Some(v) = map.get("const") {
|
|
205
|
+
node.value = Some(ValueConstraint::Const(Arc::new(serde_to_sonic(v))));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Ok(node)
|
|
209
|
+
}
|
|
210
|
+
_ => Err(()),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Resolve an in-document `$ref`. Returns `Err` for external URIs, anchors, and
|
|
215
|
+
/// cyclic references (all handled by the DOM fallback).
|
|
216
|
+
fn compile_ref(r: &str, ctx: &mut CompileCtx<'_>) -> Result<Arc<FastNode>, ()> {
|
|
217
|
+
if !r.starts_with('#') || (r.len() > 1 && !r.starts_with("#/")) {
|
|
218
|
+
return Err(());
|
|
219
|
+
}
|
|
220
|
+
let pointer = &r[1..]; // strip '#'
|
|
221
|
+
if let Some(node) = ctx.cache.get(pointer) {
|
|
222
|
+
return Ok(node.clone());
|
|
223
|
+
}
|
|
224
|
+
if ctx.in_progress.iter().any(|p| p == pointer) {
|
|
225
|
+
return Err(());
|
|
226
|
+
}
|
|
227
|
+
let target = *ctx.refs.get(pointer).ok_or(())?;
|
|
228
|
+
ctx.in_progress.push(pointer.to_string());
|
|
229
|
+
let node = Arc::new(compile_node(target, pointer, ctx)?);
|
|
230
|
+
ctx.in_progress.pop();
|
|
231
|
+
ctx.cache.insert(pointer.to_string(), node.clone());
|
|
232
|
+
Ok(node)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn compile_type(v: &Value) -> Result<u8, ()> {
|
|
236
|
+
let mut mask = 0u8;
|
|
237
|
+
let mut add = |s: &str| -> Result<(), ()> {
|
|
238
|
+
let m = match s {
|
|
239
|
+
"null" => T_NULL,
|
|
240
|
+
"boolean" => T_BOOL,
|
|
241
|
+
"number" => T_NUM,
|
|
242
|
+
"integer" => T_INT,
|
|
243
|
+
"string" => T_STR,
|
|
244
|
+
"array" => T_ARR,
|
|
245
|
+
"object" => T_OBJ,
|
|
246
|
+
_ => return Err(()),
|
|
247
|
+
};
|
|
248
|
+
mask |= m;
|
|
249
|
+
Ok(())
|
|
250
|
+
};
|
|
251
|
+
match v {
|
|
252
|
+
Value::String(s) => add(s)?,
|
|
253
|
+
Value::Array(a) => {
|
|
254
|
+
for x in a {
|
|
255
|
+
let s = x.as_str().ok_or(())?;
|
|
256
|
+
add(s)?;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
_ => return Err(()),
|
|
260
|
+
}
|
|
261
|
+
Ok(mask)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn nonneg_usize(v: &Value) -> Result<usize, ()> {
|
|
265
|
+
v.as_u64().map(|x| x as usize).ok_or(())
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn number_f64(v: &Value) -> Result<f64, ()> {
|
|
269
|
+
v.as_f64().ok_or(())
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn compile_object_constraints(
|
|
273
|
+
map: &serde_json::Map<String, Value>,
|
|
274
|
+
path: &str,
|
|
275
|
+
ctx: &mut CompileCtx<'_>,
|
|
276
|
+
) -> Result<FastObject, ()> {
|
|
277
|
+
let mut o = FastObject {
|
|
278
|
+
props: FxHashMap::default(),
|
|
279
|
+
patterns: Vec::new(),
|
|
280
|
+
required: FxHashMap::default(),
|
|
281
|
+
required_count: 0,
|
|
282
|
+
additional: Additional::Allow,
|
|
283
|
+
min_props: None,
|
|
284
|
+
max_props: None,
|
|
285
|
+
dependent_required: Vec::new(),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if let Some(Value::Object(props)) = map.get("properties") {
|
|
289
|
+
for (name, sub) in props {
|
|
290
|
+
let sub_path = format!("{}/properties/{}", path, escape_pointer(name));
|
|
291
|
+
let n = compile_node(sub, &sub_path, ctx)?;
|
|
292
|
+
o.props
|
|
293
|
+
.insert(name.as_bytes().to_vec().into_boxed_slice(), Arc::new(n));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if let Some(Value::Object(pats)) = map.get("patternProperties") {
|
|
298
|
+
for (re_str, sub) in pats {
|
|
299
|
+
let re = fancy_regex::Regex::new(re_str).map_err(|_| ())?;
|
|
300
|
+
let sub_path = format!("{}/patternProperties/{}", path, escape_pointer(re_str));
|
|
301
|
+
let n = compile_node(sub, &sub_path, ctx)?;
|
|
302
|
+
o.patterns.push((re, Arc::new(n)));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if let Some(v) = map.get("required") {
|
|
307
|
+
let arr = v.as_array().ok_or(())?;
|
|
308
|
+
let mut idx = 0u32;
|
|
309
|
+
for item in arr {
|
|
310
|
+
let s = item.as_str().ok_or(())?;
|
|
311
|
+
let key: Box<[u8]> = s.as_bytes().to_vec().into_boxed_slice();
|
|
312
|
+
if let std::collections::hash_map::Entry::Vacant(e) = o.required.entry(key) {
|
|
313
|
+
e.insert(idx);
|
|
314
|
+
idx += 1;
|
|
315
|
+
if idx > 64 {
|
|
316
|
+
return Err(());
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
o.required_count = idx as usize;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if let Some(v) = map.get("additionalProperties") {
|
|
324
|
+
match v {
|
|
325
|
+
Value::Bool(true) => o.additional = Additional::Allow,
|
|
326
|
+
Value::Bool(false) => o.additional = Additional::Deny,
|
|
327
|
+
Value::Object(_) => {
|
|
328
|
+
o.additional = Additional::Schema(Arc::new(compile_node(
|
|
329
|
+
v,
|
|
330
|
+
&format!("{}/additionalProperties", path),
|
|
331
|
+
ctx,
|
|
332
|
+
)?))
|
|
333
|
+
}
|
|
334
|
+
_ => return Err(()),
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if let Some(v) = map.get("minProperties") {
|
|
339
|
+
o.min_props = Some(nonneg_usize(v)?);
|
|
340
|
+
}
|
|
341
|
+
if let Some(v) = map.get("maxProperties") {
|
|
342
|
+
o.max_props = Some(nonneg_usize(v)?);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if let Some(Value::Object(deps)) = map.get("dependencies") {
|
|
346
|
+
for (prop, reqs) in deps {
|
|
347
|
+
let arr = reqs.as_array().ok_or(())?;
|
|
348
|
+
let mut req_keys = Vec::with_capacity(arr.len());
|
|
349
|
+
for item in arr {
|
|
350
|
+
let s = item.as_str().ok_or(())?;
|
|
351
|
+
req_keys.push(s.as_bytes().to_vec().into_boxed_slice());
|
|
352
|
+
}
|
|
353
|
+
o.dependent_required
|
|
354
|
+
.push((prop.as_bytes().to_vec().into_boxed_slice(), req_keys));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
Ok(o)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fn compile_array_constraints(
|
|
362
|
+
map: &serde_json::Map<String, Value>,
|
|
363
|
+
path: &str,
|
|
364
|
+
ctx: &mut CompileCtx<'_>,
|
|
365
|
+
) -> Result<FastArray, ()> {
|
|
366
|
+
let mut a = FastArray {
|
|
367
|
+
items: None,
|
|
368
|
+
tuple_items: None,
|
|
369
|
+
additional_items: None,
|
|
370
|
+
min_items: None,
|
|
371
|
+
max_items: None,
|
|
372
|
+
unique: false,
|
|
373
|
+
contains: None,
|
|
374
|
+
};
|
|
375
|
+
if let Some(v) = map.get("items") {
|
|
376
|
+
if let Value::Array(tuple) = v {
|
|
377
|
+
// draft-07 tuple form: each element schema at `items/<i>`.
|
|
378
|
+
let mut nodes = Vec::with_capacity(tuple.len());
|
|
379
|
+
for (i, sub) in tuple.iter().enumerate() {
|
|
380
|
+
nodes.push(Arc::new(compile_node(
|
|
381
|
+
sub,
|
|
382
|
+
&format!("{}/items/{}", path, i),
|
|
383
|
+
ctx,
|
|
384
|
+
)?));
|
|
385
|
+
}
|
|
386
|
+
a.tuple_items = Some(nodes);
|
|
387
|
+
} else {
|
|
388
|
+
a.items = Some(Arc::new(compile_node(v, &format!("{}/items", path), ctx)?));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// `additionalItems` only applies alongside tuple `items` (draft-07).
|
|
392
|
+
if let Some(v) = map.get("additionalItems") {
|
|
393
|
+
if a.tuple_items.is_some() {
|
|
394
|
+
a.additional_items = Some(match v {
|
|
395
|
+
Value::Bool(true) => Additional::Allow,
|
|
396
|
+
Value::Bool(false) => Additional::Deny,
|
|
397
|
+
Value::Object(_) => Additional::Schema(Arc::new(compile_node(
|
|
398
|
+
v,
|
|
399
|
+
&format!("{}/additionalItems", path),
|
|
400
|
+
ctx,
|
|
401
|
+
)?)),
|
|
402
|
+
_ => return Err(()),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if let Some(v) = map.get("minItems") {
|
|
407
|
+
a.min_items = Some(nonneg_usize(v)?);
|
|
408
|
+
}
|
|
409
|
+
if let Some(v) = map.get("maxItems") {
|
|
410
|
+
a.max_items = Some(nonneg_usize(v)?);
|
|
411
|
+
}
|
|
412
|
+
if let Some(v) = map.get("uniqueItems") {
|
|
413
|
+
a.unique = match v {
|
|
414
|
+
Value::Bool(b) => *b,
|
|
415
|
+
_ => return Err(()),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if let Some(v) = map.get("contains") {
|
|
419
|
+
a.contains = Some(Arc::new(compile_node(
|
|
420
|
+
v,
|
|
421
|
+
&format!("{}/contains", path),
|
|
422
|
+
ctx,
|
|
423
|
+
)?));
|
|
424
|
+
}
|
|
425
|
+
Ok(a)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
fn compile_string_constraints(map: &serde_json::Map<String, Value>) -> Result<FastString, ()> {
|
|
429
|
+
let mut s = FastString {
|
|
430
|
+
min_len: None,
|
|
431
|
+
max_len: None,
|
|
432
|
+
pattern: None,
|
|
433
|
+
format_email: false,
|
|
434
|
+
};
|
|
435
|
+
if let Some(v) = map.get("minLength") {
|
|
436
|
+
s.min_len = Some(nonneg_usize(v)?);
|
|
437
|
+
}
|
|
438
|
+
if let Some(v) = map.get("maxLength") {
|
|
439
|
+
s.max_len = Some(nonneg_usize(v)?);
|
|
440
|
+
}
|
|
441
|
+
if let Some(Value::String(re_str)) = map.get("pattern") {
|
|
442
|
+
// fancy-regex = the jsonschema engine (ECMA-262 semantics, unanchored).
|
|
443
|
+
let re = fancy_regex::Regex::new(re_str).map_err(|_| ())?;
|
|
444
|
+
s.pattern = Some(Arc::new(re));
|
|
445
|
+
}
|
|
446
|
+
if let Some(Value::String(f)) = map.get("format") {
|
|
447
|
+
// Only `format: "email"` is supported on the zero-DOM path (see
|
|
448
|
+
// `email.rs`); every other format value falls back to the DOM path so
|
|
449
|
+
// byte-parity is preserved by construction.
|
|
450
|
+
if f == "email" {
|
|
451
|
+
s.format_email = true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
Ok(s)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
fn compile_number_constraints(map: &serde_json::Map<String, Value>) -> Result<FastNumber, ()> {
|
|
458
|
+
let mut n = FastNumber {
|
|
459
|
+
minimum: None,
|
|
460
|
+
maximum: None,
|
|
461
|
+
exclusive_min: None,
|
|
462
|
+
exclusive_max: None,
|
|
463
|
+
multiple_of: None,
|
|
464
|
+
};
|
|
465
|
+
if let Some(v) = map.get("minimum") {
|
|
466
|
+
n.minimum = Some(number_f64(v)?);
|
|
467
|
+
}
|
|
468
|
+
if let Some(v) = map.get("maximum") {
|
|
469
|
+
n.maximum = Some(number_f64(v)?);
|
|
470
|
+
}
|
|
471
|
+
// Only the numeric form of exclusive bounds is unambiguous across drafts;
|
|
472
|
+
// the boolean form (draft-04/06) triggers a fallback to the reference crate.
|
|
473
|
+
if let Some(v) = map.get("exclusiveMinimum") {
|
|
474
|
+
n.exclusive_min = Some(number_f64(v)?);
|
|
475
|
+
}
|
|
476
|
+
if let Some(v) = map.get("exclusiveMaximum") {
|
|
477
|
+
n.exclusive_max = Some(number_f64(v)?);
|
|
478
|
+
}
|
|
479
|
+
if let Some(v) = map.get("multipleOf") {
|
|
480
|
+
n.multiple_of = Some(number_f64(v)?);
|
|
481
|
+
}
|
|
482
|
+
Ok(n)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn compile_combinators(
|
|
486
|
+
map: &serde_json::Map<String, Value>,
|
|
487
|
+
path: &str,
|
|
488
|
+
ctx: &mut CompileCtx<'_>,
|
|
489
|
+
) -> Result<FastComb, ()> {
|
|
490
|
+
let mut c = FastComb {
|
|
491
|
+
all_of: Vec::new(),
|
|
492
|
+
any_of: Vec::new(),
|
|
493
|
+
one_of: Vec::new(),
|
|
494
|
+
not: None,
|
|
495
|
+
if_then_else: None,
|
|
496
|
+
};
|
|
497
|
+
if let Some(Value::Array(arr)) = map.get("allOf") {
|
|
498
|
+
for (i, sub) in arr.iter().enumerate() {
|
|
499
|
+
c.all_of.push(Arc::new(compile_node(
|
|
500
|
+
sub,
|
|
501
|
+
&format!("{}/allOf/{}", path, i),
|
|
502
|
+
ctx,
|
|
503
|
+
)?));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if let Some(Value::Array(arr)) = map.get("anyOf") {
|
|
507
|
+
for (i, sub) in arr.iter().enumerate() {
|
|
508
|
+
c.any_of.push(Arc::new(compile_node(
|
|
509
|
+
sub,
|
|
510
|
+
&format!("{}/anyOf/{}", path, i),
|
|
511
|
+
ctx,
|
|
512
|
+
)?));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if let Some(Value::Array(arr)) = map.get("oneOf") {
|
|
516
|
+
for (i, sub) in arr.iter().enumerate() {
|
|
517
|
+
c.one_of.push(Arc::new(compile_node(
|
|
518
|
+
sub,
|
|
519
|
+
&format!("{}/oneOf/{}", path, i),
|
|
520
|
+
ctx,
|
|
521
|
+
)?));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if let Some(v) = map.get("not") {
|
|
525
|
+
c.not = Some(Arc::new(compile_node(v, &format!("{}/not", path), ctx)?));
|
|
526
|
+
}
|
|
527
|
+
if let Some(ifv) = map.get("if") {
|
|
528
|
+
let if_node = Arc::new(compile_node(ifv, &format!("{}/if", path), ctx)?);
|
|
529
|
+
let then_node = map
|
|
530
|
+
.get("then")
|
|
531
|
+
.map(|v| compile_node(v, &format!("{}/then", path), ctx))
|
|
532
|
+
.transpose()?
|
|
533
|
+
.map(Arc::new);
|
|
534
|
+
let else_node = map
|
|
535
|
+
.get("else")
|
|
536
|
+
.map(|v| compile_node(v, &format!("{}/else", path), ctx))
|
|
537
|
+
.transpose()?
|
|
538
|
+
.map(Arc::new);
|
|
539
|
+
c.if_then_else = Some((if_node, then_node, else_node));
|
|
540
|
+
}
|
|
541
|
+
Ok(c)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/// Convert a `serde_json::Value` schema literal into a `sonic_rs::Value`.
|
|
545
|
+
/// Compile-time only: the enum/const literals are stored as compact sonic
|
|
546
|
+
/// values and compared against runtime doc values with the zero-DOM
|
|
547
|
+
/// `sonic_values_equal` (no per-document serde_json DOM on the fast path).
|
|
548
|
+
fn serde_to_sonic(v: &serde_json::Value) -> sonic_rs::Value {
|
|
549
|
+
sonic_rs::from_slice::<sonic_rs::Value>(&serde_json::to_vec(v).unwrap()).unwrap()
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
fn compile_enum(v: &Value) -> Result<ValueConstraint, ()> {
|
|
553
|
+
let arr = v.as_array().ok_or(())?;
|
|
554
|
+
let members = arr.iter().map(|m| Arc::new(serde_to_sonic(m))).collect();
|
|
555
|
+
Ok(ValueConstraint::Enum(members))
|
|
556
|
+
}
|