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,270 @@
|
|
|
1
|
+
// rust/json/patch/ops.rs — RFC 6902 operations over `sonic_rs::Value`.
|
|
2
|
+
//
|
|
3
|
+
// Parses a patch document into `PatchOp`s and applies them with RFC 6902
|
|
4
|
+
// semantics. DOM navigation helpers (`resolve`/`child` + mutable twins) live
|
|
5
|
+
// here next to the operations that use them. The bounded apply + batch
|
|
6
|
+
// orchestration is in `engine.rs`; the napi boundary is in `api.rs`.
|
|
7
|
+
|
|
8
|
+
use sonic_rs::{JsonContainerTrait, JsonType, JsonValueMutTrait, JsonValueTrait};
|
|
9
|
+
|
|
10
|
+
use super::engine::JsonPatchError;
|
|
11
|
+
use super::pointer::Pointer;
|
|
12
|
+
|
|
13
|
+
/// A parsed RFC 6902 patch operation.
|
|
14
|
+
#[derive(Debug)]
|
|
15
|
+
pub(crate) enum PatchOp {
|
|
16
|
+
Add {
|
|
17
|
+
path: Pointer,
|
|
18
|
+
value: sonic_rs::Value,
|
|
19
|
+
},
|
|
20
|
+
Remove {
|
|
21
|
+
path: Pointer,
|
|
22
|
+
},
|
|
23
|
+
Replace {
|
|
24
|
+
path: Pointer,
|
|
25
|
+
value: sonic_rs::Value,
|
|
26
|
+
},
|
|
27
|
+
Move {
|
|
28
|
+
from: Pointer,
|
|
29
|
+
path: Pointer,
|
|
30
|
+
},
|
|
31
|
+
Copy {
|
|
32
|
+
from: Pointer,
|
|
33
|
+
path: Pointer,
|
|
34
|
+
},
|
|
35
|
+
Test {
|
|
36
|
+
path: Pointer,
|
|
37
|
+
value: sonic_rs::Value,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Parse one patch operation object. `Err(())` → `InvalidPatch`.
|
|
42
|
+
fn parse_op(obj: &sonic_rs::Value) -> std::result::Result<PatchOp, ()> {
|
|
43
|
+
let op_str = obj.get("op").and_then(|v| v.as_str()).ok_or(())?;
|
|
44
|
+
let path = obj
|
|
45
|
+
.get("path")
|
|
46
|
+
.and_then(|v| v.as_str())
|
|
47
|
+
.ok_or(())
|
|
48
|
+
.and_then(Pointer::parse)?;
|
|
49
|
+
match op_str {
|
|
50
|
+
"add" | "replace" | "test" => {
|
|
51
|
+
let value = obj.get("value").cloned().ok_or(())?;
|
|
52
|
+
Ok(match op_str {
|
|
53
|
+
"add" => PatchOp::Add { path, value },
|
|
54
|
+
"replace" => PatchOp::Replace { path, value },
|
|
55
|
+
_ => PatchOp::Test { path, value },
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
"remove" => Ok(PatchOp::Remove { path }),
|
|
59
|
+
"move" | "copy" => {
|
|
60
|
+
let from = obj
|
|
61
|
+
.get("from")
|
|
62
|
+
.and_then(|v| v.as_str())
|
|
63
|
+
.ok_or(())
|
|
64
|
+
.and_then(Pointer::parse)?;
|
|
65
|
+
Ok(if op_str == "move" {
|
|
66
|
+
PatchOp::Move { from, path }
|
|
67
|
+
} else {
|
|
68
|
+
PatchOp::Copy { from, path }
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
_ => Err(()),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Parse a patch document into a list of operations (eager RFC 6901 pointer
|
|
76
|
+
/// validation). `Err` → `InvalidPatch`.
|
|
77
|
+
pub(crate) fn parse_patch(patch: &[u8]) -> std::result::Result<Vec<PatchOp>, JsonPatchError> {
|
|
78
|
+
let patch_val: sonic_rs::Value =
|
|
79
|
+
sonic_rs::from_slice(patch).map_err(|e| JsonPatchError::InvalidPatch(e.to_string()))?;
|
|
80
|
+
let arr = patch_val
|
|
81
|
+
.as_array()
|
|
82
|
+
.ok_or_else(|| JsonPatchError::InvalidPatch("patch must be an array".into()))?;
|
|
83
|
+
let mut ops = Vec::with_capacity(arr.len());
|
|
84
|
+
for op in arr {
|
|
85
|
+
let op = parse_op(op)
|
|
86
|
+
.map_err(|_| JsonPatchError::InvalidPatch("invalid patch operation".into()))?;
|
|
87
|
+
ops.push(op);
|
|
88
|
+
}
|
|
89
|
+
Ok(ops)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── DOM navigation ──────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/// Immutably resolve a value by path segments.
|
|
95
|
+
fn resolve<'a>(node: &'a sonic_rs::Value, segs: &[String]) -> Option<&'a sonic_rs::Value> {
|
|
96
|
+
let mut cur = node;
|
|
97
|
+
for seg in segs {
|
|
98
|
+
cur = child(cur, seg)?;
|
|
99
|
+
}
|
|
100
|
+
Some(cur)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn child<'a>(node: &'a sonic_rs::Value, seg: &str) -> Option<&'a sonic_rs::Value> {
|
|
104
|
+
match node.get_type() {
|
|
105
|
+
JsonType::Object => node.get(seg),
|
|
106
|
+
JsonType::Array => {
|
|
107
|
+
if seg == "-" {
|
|
108
|
+
return None;
|
|
109
|
+
}
|
|
110
|
+
let idx: usize = seg.parse().ok()?;
|
|
111
|
+
node.get(idx)
|
|
112
|
+
}
|
|
113
|
+
_ => None,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Mutably resolve a value by path segments.
|
|
118
|
+
fn resolve_mut<'a>(
|
|
119
|
+
node: &'a mut sonic_rs::Value,
|
|
120
|
+
segs: &[String],
|
|
121
|
+
) -> Option<&'a mut sonic_rs::Value> {
|
|
122
|
+
let mut cur = node;
|
|
123
|
+
for seg in segs {
|
|
124
|
+
cur = child_mut(cur, seg)?;
|
|
125
|
+
}
|
|
126
|
+
Some(cur)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn child_mut<'a>(node: &'a mut sonic_rs::Value, seg: &str) -> Option<&'a mut sonic_rs::Value> {
|
|
130
|
+
match node.get_type() {
|
|
131
|
+
JsonType::Object => node.get_mut(seg),
|
|
132
|
+
JsonType::Array => {
|
|
133
|
+
if seg == "-" {
|
|
134
|
+
return None;
|
|
135
|
+
}
|
|
136
|
+
let idx: usize = seg.parse().ok()?;
|
|
137
|
+
node.get_mut(idx)
|
|
138
|
+
}
|
|
139
|
+
_ => None,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Operations ──────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/// RFC 6902 `add` (also the write half of `move`/`copy`). Root replaces the
|
|
146
|
+
/// document; an object member is inserted/replaced; an array element is
|
|
147
|
+
/// inserted at `idx` (`0..=len`, `-` appends).
|
|
148
|
+
fn apply_add(
|
|
149
|
+
doc: &mut sonic_rs::Value,
|
|
150
|
+
path: &Pointer,
|
|
151
|
+
value: sonic_rs::Value,
|
|
152
|
+
) -> std::result::Result<(), String> {
|
|
153
|
+
if path.segments.is_empty() {
|
|
154
|
+
*doc = value;
|
|
155
|
+
return Ok(());
|
|
156
|
+
}
|
|
157
|
+
let last = path.segments.last().unwrap();
|
|
158
|
+
let parent = resolve_mut(doc, &path.segments[..path.segments.len() - 1])
|
|
159
|
+
.ok_or("add: target parent does not exist")?;
|
|
160
|
+
match parent.get_type() {
|
|
161
|
+
JsonType::Object => {
|
|
162
|
+
let obj = parent.as_object_mut().unwrap();
|
|
163
|
+
obj.insert(last.as_str(), value);
|
|
164
|
+
Ok(())
|
|
165
|
+
}
|
|
166
|
+
JsonType::Array => {
|
|
167
|
+
let arr = parent.as_array_mut().unwrap();
|
|
168
|
+
if last == "-" {
|
|
169
|
+
arr.push(value);
|
|
170
|
+
} else {
|
|
171
|
+
let idx: usize = last.parse().map_err(|_| "add: invalid array index")?;
|
|
172
|
+
if idx > arr.len() {
|
|
173
|
+
return Err("add: array index out of range".into());
|
|
174
|
+
}
|
|
175
|
+
arr.insert(idx, value);
|
|
176
|
+
}
|
|
177
|
+
Ok(())
|
|
178
|
+
}
|
|
179
|
+
_ => Err("add: target parent is not an object or array".into()),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// RFC 6902 `remove`.
|
|
184
|
+
fn apply_remove(doc: &mut sonic_rs::Value, path: &Pointer) -> std::result::Result<(), String> {
|
|
185
|
+
if path.segments.is_empty() {
|
|
186
|
+
return Err("remove: cannot remove the document root".into());
|
|
187
|
+
}
|
|
188
|
+
let last = path.segments.last().unwrap();
|
|
189
|
+
let parent = resolve_mut(doc, &path.segments[..path.segments.len() - 1])
|
|
190
|
+
.ok_or("remove: target parent does not exist")?;
|
|
191
|
+
match parent.get_type() {
|
|
192
|
+
JsonType::Object => {
|
|
193
|
+
let obj = parent.as_object_mut().unwrap();
|
|
194
|
+
obj.remove(last).ok_or("remove: member does not exist")?;
|
|
195
|
+
Ok(())
|
|
196
|
+
}
|
|
197
|
+
JsonType::Array => {
|
|
198
|
+
let idx: usize = last.parse().map_err(|_| "remove: invalid array index")?;
|
|
199
|
+
let arr = parent.as_array_mut().unwrap();
|
|
200
|
+
if idx >= arr.len() {
|
|
201
|
+
return Err("remove: array index out of range".into());
|
|
202
|
+
}
|
|
203
|
+
arr.remove(idx);
|
|
204
|
+
Ok(())
|
|
205
|
+
}
|
|
206
|
+
_ => Err("remove: target parent is not an object or array".into()),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// RFC 6902 `replace` (target must exist).
|
|
211
|
+
fn apply_replace(
|
|
212
|
+
doc: &mut sonic_rs::Value,
|
|
213
|
+
path: &Pointer,
|
|
214
|
+
value: sonic_rs::Value,
|
|
215
|
+
) -> std::result::Result<(), String> {
|
|
216
|
+
if path.segments.is_empty() {
|
|
217
|
+
*doc = value;
|
|
218
|
+
return Ok(());
|
|
219
|
+
}
|
|
220
|
+
let target =
|
|
221
|
+
resolve_mut(doc, &path.segments).ok_or("replace: target location does not exist")?;
|
|
222
|
+
*target = value;
|
|
223
|
+
Ok(())
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// RFC 6902 `move` — remove at `from`, then add at `path`. `from` MUST NOT be
|
|
227
|
+
/// a proper prefix of `path` (cannot move a node into one of its children).
|
|
228
|
+
fn apply_move(
|
|
229
|
+
doc: &mut sonic_rs::Value,
|
|
230
|
+
from: &Pointer,
|
|
231
|
+
path: &Pointer,
|
|
232
|
+
) -> std::result::Result<(), String> {
|
|
233
|
+
if from.segments.len() < path.segments.len() && path.segments.starts_with(&from.segments) {
|
|
234
|
+
return Err("move: 'from' is a prefix of 'path'".into());
|
|
235
|
+
}
|
|
236
|
+
let value = resolve(doc, &from.segments)
|
|
237
|
+
.cloned()
|
|
238
|
+
.ok_or("move: 'from' location does not exist")?;
|
|
239
|
+
apply_remove(doc, from)?;
|
|
240
|
+
apply_add(doc, path, value)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Apply a parsed patch to a document (RFC 6902 semantics over sonic Value).
|
|
244
|
+
pub(crate) fn apply_patch(
|
|
245
|
+
doc: &mut sonic_rs::Value,
|
|
246
|
+
ops: &[PatchOp],
|
|
247
|
+
) -> std::result::Result<(), String> {
|
|
248
|
+
for op in ops {
|
|
249
|
+
match op {
|
|
250
|
+
PatchOp::Add { path, value } => apply_add(doc, path, value.clone())?,
|
|
251
|
+
PatchOp::Remove { path } => apply_remove(doc, path)?,
|
|
252
|
+
PatchOp::Replace { path, value } => apply_replace(doc, path, value.clone())?,
|
|
253
|
+
PatchOp::Move { from, path } => apply_move(doc, from, path)?,
|
|
254
|
+
PatchOp::Copy { from, path } => {
|
|
255
|
+
let value = resolve(doc, &from.segments)
|
|
256
|
+
.cloned()
|
|
257
|
+
.ok_or("copy: 'from' location does not exist")?;
|
|
258
|
+
apply_add(doc, path, value)?;
|
|
259
|
+
}
|
|
260
|
+
PatchOp::Test { path, value } => {
|
|
261
|
+
let actual =
|
|
262
|
+
resolve(doc, &path.segments).ok_or("test: target location does not exist")?;
|
|
263
|
+
if actual != value {
|
|
264
|
+
return Err("test: value does not match target".into());
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
Ok(())
|
|
270
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// rust/json/patch/pointer.rs — RFC 6901 JSON Pointer (parsing only).
|
|
2
|
+
//
|
|
3
|
+
// Pure string handling: parse a pointer string into unescaped segments. The
|
|
4
|
+
// DOM navigation/resolution that *uses* a pointer lives in `ops.rs`; keeping
|
|
5
|
+
// the pointer syntax here makes the RFC 6901 escape rules independently
|
|
6
|
+
// testable and keeps `ops.rs` focused on applying operations.
|
|
7
|
+
|
|
8
|
+
/// A parsed RFC 6901 JSON Pointer (segments already `~0`/`~1`-unescaped).
|
|
9
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
10
|
+
pub(crate) struct Pointer {
|
|
11
|
+
pub(crate) segments: Vec<String>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl Pointer {
|
|
15
|
+
pub(crate) fn root() -> Self {
|
|
16
|
+
Pointer {
|
|
17
|
+
segments: Vec::new(),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Parse an RFC 6901 pointer string. `""` = root; any non-empty pointer
|
|
22
|
+
/// must start with `/`. `~0`→`~`, `~1`→`/`; any other `~x` (including a
|
|
23
|
+
/// trailing `~`) is an invalid escape → `Err` (the caller maps to
|
|
24
|
+
/// `InvalidPatch`, matching the json-patch crate's eager validation).
|
|
25
|
+
pub(crate) fn parse(s: &str) -> std::result::Result<Self, ()> {
|
|
26
|
+
if s.is_empty() {
|
|
27
|
+
return Ok(Self::root());
|
|
28
|
+
}
|
|
29
|
+
if !s.starts_with('/') {
|
|
30
|
+
return Err(());
|
|
31
|
+
}
|
|
32
|
+
let mut segments = Vec::new();
|
|
33
|
+
let mut cur = String::new();
|
|
34
|
+
let mut chars = s[1..].chars();
|
|
35
|
+
while let Some(c) = chars.next() {
|
|
36
|
+
match c {
|
|
37
|
+
'/' => segments.push(std::mem::take(&mut cur)),
|
|
38
|
+
'~' => match chars.next() {
|
|
39
|
+
Some('0') => cur.push('~'),
|
|
40
|
+
Some('1') => cur.push('/'),
|
|
41
|
+
_ => return Err(()),
|
|
42
|
+
},
|
|
43
|
+
other => cur.push(other),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
segments.push(cur);
|
|
47
|
+
Ok(Pointer { segments })
|
|
48
|
+
}
|
|
49
|
+
}
|