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,336 @@
|
|
|
1
|
+
// rust/json/fast_schema/capture.rs — One-pass derive: capture scalar values at
|
|
2
|
+
// JSON-pointer paths DURING the same validation walk (no second pass, no DOM).
|
|
3
|
+
//
|
|
4
|
+
// The `SchemaValidator.derive` napi entry (json_schema.rs) uses this to power
|
|
5
|
+
// "validate + extract" routes: the response is derived from a handful of body
|
|
6
|
+
// fields (e.g. `/api/orders` needs `lineItems.length` + `totalCents`), so
|
|
7
|
+
// Rust validates the schema AND captures those values in a single zero-DOM
|
|
8
|
+
// pass — replacing `JSON.parse` (DOM build + GC) + Ajv entirely on the happy
|
|
9
|
+
// path, and rejecting invalid bodies in microseconds.
|
|
10
|
+
//
|
|
11
|
+
// Capture is OFF by default (`Ctx::capture == None`): the bool/detailed hot
|
|
12
|
+
// paths are byte-for-byte unchanged. When active, `Capture` records, per
|
|
13
|
+
// target path, either the raw byte range of a scalar value (`Value`) or the
|
|
14
|
+
// element count of an array (`Length`, path terminal `/-`).
|
|
15
|
+
//
|
|
16
|
+
// Safety: capture fires only on the ROOT walk. Combinator / pattern / enum
|
|
17
|
+
// re-scans create fresh Cursors over sub-slices, and `Ctx` compares the
|
|
18
|
+
// cursor's data pointer against the root input pointer, so sub-scan offsets
|
|
19
|
+
// (which are relative to a sub-slice, not the input) can never corrupt
|
|
20
|
+
// captured ranges. `with_suppressed` branch checks are also excluded.
|
|
21
|
+
|
|
22
|
+
use std::collections::HashMap;
|
|
23
|
+
|
|
24
|
+
/// What to capture at a target path.
|
|
25
|
+
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
26
|
+
pub enum CaptureKind {
|
|
27
|
+
/// The raw scalar value at the path (string / number / bool / null).
|
|
28
|
+
Value,
|
|
29
|
+
/// The element count of the ARRAY at the path (path terminal `/-`).
|
|
30
|
+
Length,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// A single target: an object-key path (no array-index steps) + a capture kind.
|
|
34
|
+
#[derive(Clone, Debug)]
|
|
35
|
+
pub struct TargetPath {
|
|
36
|
+
pub steps: Vec<Vec<u8>>,
|
|
37
|
+
pub kind: CaptureKind,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Parse an RFC 6901 JSON pointer into a `TargetPath`.
|
|
41
|
+
///
|
|
42
|
+
/// `"/totalCents"` → `Value` at steps `["totalCents"]`.
|
|
43
|
+
/// `"/lineItems/-"` → `Length` at steps `["lineItems"]`.
|
|
44
|
+
/// `"/"` (root) or a pointer with array-index steps → `None`.
|
|
45
|
+
pub fn parse_target(pointer: &str) -> Option<TargetPath> {
|
|
46
|
+
if pointer.is_empty() || !pointer.starts_with('/') {
|
|
47
|
+
return None;
|
|
48
|
+
}
|
|
49
|
+
let raw_tokens: Vec<&str> = pointer[1..].split('/').collect();
|
|
50
|
+
if raw_tokens.is_empty() || raw_tokens[0].is_empty() {
|
|
51
|
+
// "/" root or "//" empty first key — unsupported.
|
|
52
|
+
return None;
|
|
53
|
+
}
|
|
54
|
+
let last = raw_tokens[raw_tokens.len() - 1];
|
|
55
|
+
let (kind, body) = if last == "-" {
|
|
56
|
+
(CaptureKind::Length, &raw_tokens[..raw_tokens.len() - 1])
|
|
57
|
+
} else {
|
|
58
|
+
(CaptureKind::Value, &raw_tokens[..])
|
|
59
|
+
};
|
|
60
|
+
if body.is_empty() {
|
|
61
|
+
// A bare "-" ("/-") or a root-length target — unsupported.
|
|
62
|
+
return None;
|
|
63
|
+
}
|
|
64
|
+
let mut steps = Vec::with_capacity(body.len());
|
|
65
|
+
for tok in body {
|
|
66
|
+
// Array-index steps are unsupported: a token that is `-` or all ASCII
|
|
67
|
+
// digits is treated as an array index (JSON-pointer convention) and
|
|
68
|
+
// rejected. (A rare all-digit OBJECT key is thereby excluded too.)
|
|
69
|
+
let is_index = !tok.is_empty() && (*tok == "-" || tok.bytes().all(|b| b.is_ascii_digit()));
|
|
70
|
+
if is_index {
|
|
71
|
+
return None;
|
|
72
|
+
}
|
|
73
|
+
// Decode JSON-pointer escapes: ~1 -> '/', ~0 -> '~'.
|
|
74
|
+
let mut step = Vec::with_capacity(tok.len());
|
|
75
|
+
let bytes = tok.as_bytes();
|
|
76
|
+
let mut i = 0;
|
|
77
|
+
while i < bytes.len() {
|
|
78
|
+
if bytes[i] == b'~' && i + 1 < bytes.len() {
|
|
79
|
+
match bytes[i + 1] {
|
|
80
|
+
b'1' => step.push(b'/'),
|
|
81
|
+
b'0' => step.push(b'~'),
|
|
82
|
+
_ => step.push(bytes[i]),
|
|
83
|
+
}
|
|
84
|
+
i += 2;
|
|
85
|
+
} else {
|
|
86
|
+
step.push(bytes[i]);
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
steps.push(step);
|
|
91
|
+
}
|
|
92
|
+
Some(TargetPath { steps, kind })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// One trie node. `children` maps an object key to the next node; `value_target`
|
|
96
|
+
/// / `length_target` are set when this node is the terminal of a `Value` /
|
|
97
|
+
/// `Length` target.
|
|
98
|
+
struct TrieNode {
|
|
99
|
+
children: HashMap<Box<[u8]>, usize>,
|
|
100
|
+
value_target: Option<usize>,
|
|
101
|
+
length_target: Option<usize>,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl TrieNode {
|
|
105
|
+
fn new() -> Self {
|
|
106
|
+
Self {
|
|
107
|
+
children: HashMap::new(),
|
|
108
|
+
value_target: None,
|
|
109
|
+
length_target: None,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// In-progress capture state, threaded through `Ctx` during one validation walk.
|
|
115
|
+
///
|
|
116
|
+
/// Targets are compiled into a trie once per call; during the walk the active
|
|
117
|
+
/// node is tracked by `(node, alive)` pairs pushed/popped per OBJECT nesting
|
|
118
|
+
/// level — NO per-member key cloning (the detailed-error path stack is
|
|
119
|
+
/// untouched), so capture overhead on a valid 5000-item document is a few hash
|
|
120
|
+
/// lookups per member, not allocations. Dead subtrees (no remaining target
|
|
121
|
+
/// key) cost nothing.
|
|
122
|
+
pub struct Capture {
|
|
123
|
+
arena: Vec<TrieNode>,
|
|
124
|
+
/// Active trie-node stack: `(node_idx, alive)` per object nesting level.
|
|
125
|
+
stack: Vec<(usize, bool)>,
|
|
126
|
+
matched: Vec<bool>,
|
|
127
|
+
/// per-target captured raw value byte ranges (Value targets only)
|
|
128
|
+
ranges: Vec<(usize, usize)>,
|
|
129
|
+
/// per-target array lengths (Length targets only)
|
|
130
|
+
lengths: Vec<usize>,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
impl Capture {
|
|
134
|
+
pub fn new(targets: Vec<TargetPath>) -> Self {
|
|
135
|
+
let n = targets.len();
|
|
136
|
+
let mut arena = vec![TrieNode::new()]; // node 0 = root
|
|
137
|
+
for (ti, t) in targets.iter().enumerate() {
|
|
138
|
+
let mut node = 0usize;
|
|
139
|
+
for step in &t.steps {
|
|
140
|
+
let key: Box<[u8]> = step.clone().into_boxed_slice();
|
|
141
|
+
let next = match arena[node].children.get(&key) {
|
|
142
|
+
Some(&idx) => idx,
|
|
143
|
+
None => {
|
|
144
|
+
let idx = arena.len();
|
|
145
|
+
arena.push(TrieNode::new());
|
|
146
|
+
arena[node].children.insert(key, idx);
|
|
147
|
+
idx
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
node = next;
|
|
151
|
+
}
|
|
152
|
+
match t.kind {
|
|
153
|
+
CaptureKind::Value => arena[node].value_target = Some(ti),
|
|
154
|
+
CaptureKind::Length => arena[node].length_target = Some(ti),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
Self {
|
|
158
|
+
arena,
|
|
159
|
+
stack: vec![(0, true)],
|
|
160
|
+
matched: vec![false; n],
|
|
161
|
+
ranges: vec![(0, 0); n],
|
|
162
|
+
lengths: vec![0; n],
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Descend the trie for an object member key. Returns false when the
|
|
167
|
+
/// subtree is dead (no target can match below).
|
|
168
|
+
#[inline]
|
|
169
|
+
pub fn enter_key(&mut self, key: &[u8]) -> bool {
|
|
170
|
+
let (node, alive) = *self.stack.last().unwrap_or(&(0, true));
|
|
171
|
+
if !alive {
|
|
172
|
+
self.stack.push((node, false));
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
match self.arena[node].children.get(key) {
|
|
176
|
+
Some(&next) => {
|
|
177
|
+
self.stack.push((next, true));
|
|
178
|
+
true
|
|
179
|
+
}
|
|
180
|
+
None => {
|
|
181
|
+
self.stack.push((node, false));
|
|
182
|
+
false
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Leave the current object nesting level.
|
|
188
|
+
#[inline]
|
|
189
|
+
pub fn exit_key(&mut self) {
|
|
190
|
+
if self.stack.len() > 1 {
|
|
191
|
+
self.stack.pop();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Record a captured scalar value at the current node (Value leaf).
|
|
196
|
+
#[inline]
|
|
197
|
+
pub fn record_value(&mut self, start: usize, end: usize) {
|
|
198
|
+
let (node, alive) = *self.stack.last().unwrap_or(&(0, false));
|
|
199
|
+
if !alive {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if let Some(ti) = self.arena[node].value_target {
|
|
203
|
+
if !self.matched[ti] {
|
|
204
|
+
self.matched[ti] = true;
|
|
205
|
+
self.ranges[ti] = (start, end);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Record a captured array length at the current node (Length leaf).
|
|
211
|
+
#[inline]
|
|
212
|
+
pub fn record_length(&mut self, count: usize) {
|
|
213
|
+
let (node, alive) = *self.stack.last().unwrap_or(&(0, false));
|
|
214
|
+
if !alive {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if let Some(ti) = self.arena[node].length_target {
|
|
218
|
+
if !self.matched[ti] {
|
|
219
|
+
self.matched[ti] = true;
|
|
220
|
+
self.lengths[ti] = count;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
pub fn target_count(&self) -> usize {
|
|
226
|
+
self.matched.len()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Captured raw value byte range for target `i` (valid body, path present).
|
|
230
|
+
pub fn value_range(&self, i: usize) -> Option<(usize, usize)> {
|
|
231
|
+
if self.matched.get(i) == Some(&true) {
|
|
232
|
+
Some(self.ranges[i])
|
|
233
|
+
} else {
|
|
234
|
+
None
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Captured array length for target `i` (valid body, path present).
|
|
239
|
+
pub fn length(&self, i: usize) -> Option<usize> {
|
|
240
|
+
if self.matched.get(i) == Some(&true) {
|
|
241
|
+
Some(self.lengths[i])
|
|
242
|
+
} else {
|
|
243
|
+
None
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[cfg(test)]
|
|
249
|
+
mod tests {
|
|
250
|
+
use super::*;
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn parse_value_pointer() {
|
|
254
|
+
let t = parse_target("/totalCents").unwrap();
|
|
255
|
+
assert_eq!(t.kind, CaptureKind::Value);
|
|
256
|
+
assert_eq!(t.steps, vec![b"totalCents".to_vec()]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn parse_length_pointer() {
|
|
261
|
+
let t = parse_target("/lineItems/-").unwrap();
|
|
262
|
+
assert_eq!(t.kind, CaptureKind::Length);
|
|
263
|
+
assert_eq!(t.steps, vec![b"lineItems".to_vec()]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[test]
|
|
267
|
+
fn parse_nested() {
|
|
268
|
+
let t = parse_target("/customer/email").unwrap();
|
|
269
|
+
assert_eq!(t.kind, CaptureKind::Value);
|
|
270
|
+
assert_eq!(t.steps, vec![b"customer".to_vec(), b"email".to_vec()]);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#[test]
|
|
274
|
+
fn parse_escapes() {
|
|
275
|
+
let t = parse_target("/a~1b/c~0d").unwrap();
|
|
276
|
+
assert_eq!(t.steps, vec![b"a/b".to_vec(), b"c~d".to_vec()]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[test]
|
|
280
|
+
fn parse_rejects_root_and_indexes() {
|
|
281
|
+
assert!(parse_target("").is_none());
|
|
282
|
+
assert!(parse_target("/").is_none());
|
|
283
|
+
assert!(parse_target("/lineItems/0").is_none());
|
|
284
|
+
assert!(parse_target("lineItems").is_none());
|
|
285
|
+
assert!(parse_target("/-").is_none());
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[test]
|
|
289
|
+
fn trie_matches_object_and_length() {
|
|
290
|
+
let mut cap = Capture::new(vec![
|
|
291
|
+
parse_target("/lineItems/-").unwrap(),
|
|
292
|
+
parse_target("/totalCents").unwrap(),
|
|
293
|
+
]);
|
|
294
|
+
// root object member "lineItems" (array): descend, then array close.
|
|
295
|
+
cap.enter_key(b"lineItems");
|
|
296
|
+
cap.record_length(5000);
|
|
297
|
+
cap.exit_key();
|
|
298
|
+
// root object member "totalCents": descend, then record scalar value.
|
|
299
|
+
cap.enter_key(b"totalCents");
|
|
300
|
+
cap.record_value(100, 110);
|
|
301
|
+
cap.exit_key();
|
|
302
|
+
assert_eq!(cap.length(0), Some(5000));
|
|
303
|
+
assert_eq!(cap.value_range(1), Some((100, 110)));
|
|
304
|
+
// a non-target key goes dead and records nothing
|
|
305
|
+
cap.enter_key(b"other");
|
|
306
|
+
cap.record_length(9);
|
|
307
|
+
cap.record_value(1, 2);
|
|
308
|
+
cap.exit_key();
|
|
309
|
+
assert_eq!(cap.length(0), Some(5000));
|
|
310
|
+
assert_eq!(cap.value_range(1), Some((100, 110)));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[test]
|
|
314
|
+
fn trie_dead_subtree_is_free() {
|
|
315
|
+
let mut cap = Capture::new(vec![parse_target("/totalCents").unwrap()]);
|
|
316
|
+
// entering a non-target object key kills the subtree
|
|
317
|
+
cap.enter_key(b"lineItems");
|
|
318
|
+
let alive = cap.enter_key(b"sku"); // inside a dead subtree
|
|
319
|
+
assert!(!alive);
|
|
320
|
+
cap.record_value(1, 2); // no-op
|
|
321
|
+
cap.exit_key();
|
|
322
|
+
cap.exit_key();
|
|
323
|
+
assert!(cap.value_range(0).is_none());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[test]
|
|
327
|
+
fn trie_nested_value() {
|
|
328
|
+
let mut cap = Capture::new(vec![parse_target("/customer/email").unwrap()]);
|
|
329
|
+
cap.enter_key(b"customer");
|
|
330
|
+
cap.enter_key(b"email");
|
|
331
|
+
cap.record_value(50, 60);
|
|
332
|
+
cap.exit_key();
|
|
333
|
+
cap.exit_key();
|
|
334
|
+
assert_eq!(cap.value_range(0), Some((50, 60)));
|
|
335
|
+
}
|
|
336
|
+
}
|