tbm-git-repo-service/src/tx/patcher.rs
2026-02-17 14:23:31 +07:00

119 lines
4.3 KiB
Rust

use serde_json::Value;
use crate::tx::types::PatchOp;
pub fn json_pointer_parent<'a>(
root: &'a mut Value,
path: &str,
) -> Result<(&'a mut Value, String), String> {
if !path.starts_with('/') && path != "" {
return Err("path must be JSON pointer starting with '/'".into());
}
let part: Vec<String> = path
.split('/')
.skip(1)
.map(|x| x.replace("~1", "/").replace("~0", "~"))
.collect();
if path.is_empty() {
return Ok((root, "".into()));
}
let last = part.last().unwrap().clone();
let mut cur = root;
for key in &part[..part.len() - 1] {
match cur {
Value::Object(map) => {
cur = map
.get_mut(key)
.ok_or_else(|| format!("missing object key {key}"))?;
}
Value::Array(arr) => {
let idx: usize = key.parse().map_err(|_| format!("bad array idx {key}"))?;
cur = arr
.get_mut(idx)
.ok_or_else(|| format!("array index out of bounds {idx}"))?;
}
_ => return Err("cannot traverse non-container".into()),
}
}
Ok((cur, last))
}
pub fn apply_ops(mut state: Value, ops: &[PatchOp]) -> Result<Value, String> {
for op in ops {
match op {
PatchOp::Replace { path, value } => {
if path == "" || path == "/" {
state = value.clone();
continue;
}
let (parent, last) = json_pointer_parent(&mut state, path)?;
match parent {
Value::Object(map) => {
if !map.contains_key(&last) {
return Err(format!("replace target missing key {last}"));
}
map.insert(last, value.clone());
}
Value::Array(arr) => {
let idx: usize = last
.parse()
.map_err(|_| format!("bad array index {last}"))?;
if idx >= arr.len() {
return Err(format!("replace index out of bounds {idx}"));
}
arr[idx] = value.clone();
}
_ => return Err("replace parent not container".into()),
}
}
PatchOp::Add { path, value } => {
let (parent, last) = json_pointer_parent(&mut state, path)?;
match parent {
Value::Object(map) => {
map.insert(last, value.clone());
}
Value::Array(arr) => {
if last == "-" {
arr.push(value.clone());
} else {
let idx: usize = last
.parse()
.map_err(|_| format!("bad array index {last}"))?;
if idx > arr.len() {
return Err(format!("add index out of bounds {idx}"));
}
arr.insert(idx, value.clone());
}
}
_ => return Err("add parent not container".into()),
}
}
PatchOp::Remove { path } => {
let (parent, last) = json_pointer_parent(&mut state, path)?;
match parent {
Value::Object(map) => {
map.remove(&last)
.ok_or_else(|| format!("remove missing key {last}"))?;
}
Value::Array(arr) => {
let idx: usize = last
.parse()
.map_err(|_| format!("bad array index {last}"))?;
if idx > arr.len() {
return Err(format!("remove index out of bounds {idx}"));
}
arr.remove(idx);
}
_ => return Err("remove parent not container".into()),
}
}
}
}
Ok(state)
}