KeiSeiKit-1.0/_primitives/_rust/kei-diff/src/path.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

105 lines
2.8 KiB
Rust

//! RFC 6901 JSON Pointer path builder.
//!
//! Root is `""`. Segments join with `/`. Inside a segment, `~` encodes as
//! `~0` and `/` encodes as `~1`. Order matters: `~` must be escaped first
//! when encoding, and `~1` must be decoded before `~0`.
/// Incremental pointer builder. Use `push`/`pop` during recursive traversal;
/// `as_str` yields the current RFC 6901 pointer.
#[derive(Debug, Default, Clone)]
pub struct PathBuf {
segments: Vec<String>, // already-encoded segments (no leading '/')
}
impl PathBuf {
pub fn new() -> Self {
Self { segments: Vec::new() }
}
/// Push an object key. Performs RFC 6901 escaping.
pub fn push_key(&mut self, key: &str) {
self.segments.push(encode_segment(key));
}
/// Push an array index. Always emitted as decimal digits.
pub fn push_index(&mut self, idx: usize) {
self.segments.push(idx.to_string());
}
pub fn pop(&mut self) {
self.segments.pop();
}
/// Current pointer as a String. Empty string if at root.
pub fn as_string(&self) -> String {
if self.segments.is_empty() {
return String::new();
}
let mut out = String::with_capacity(self.segments.iter().map(|s| s.len() + 1).sum());
for seg in &self.segments {
out.push('/');
out.push_str(seg);
}
out
}
}
fn encode_segment(raw: &str) -> String {
// ~ must be escaped BEFORE / so we don't double-encode.
raw.replace('~', "~0").replace('/', "~1")
}
/// Parse an RFC 6901 pointer into decoded segments. `""` → `[]`.
/// Returns `None` if pointer is malformed (e.g. doesn't start with `/`
/// and is non-empty).
pub fn parse_pointer(ptr: &str) -> Option<Vec<String>> {
if ptr.is_empty() {
return Some(Vec::new());
}
if !ptr.starts_with('/') {
return None;
}
let segs = ptr[1..]
.split('/')
.map(decode_segment)
.collect::<Vec<_>>();
Some(segs)
}
fn decode_segment(raw: &str) -> String {
// ~1 must be decoded BEFORE ~0 per RFC 6901.
raw.replace("~1", "/").replace("~0", "~")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_is_empty_string() {
let p = PathBuf::new();
assert_eq!(p.as_string(), "");
}
#[test]
fn escape_tilde_and_slash() {
let mut p = PathBuf::new();
p.push_key("a/b");
p.push_key("c~d");
assert_eq!(p.as_string(), "/a~1b/c~0d");
}
#[test]
fn roundtrip_encode_decode() {
let mut p = PathBuf::new();
p.push_key("weird~/key");
let s = p.as_string();
let decoded = parse_pointer(&s).unwrap();
assert_eq!(decoded, vec!["weird~/key".to_string()]);
}
#[test]
fn parse_rejects_malformed() {
assert!(parse_pointer("no-leading-slash").is_none());
}
}