#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum PatchOp {
Add { path: String, value: String },
Remove { path: String },
Replace { path: String, value: String },
Move { from: String, path: String },
Copy { from: String, path: String },
Test { path: String, value: String },
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatchError {
InvalidOperation(String),
PathNotFound(String),
TestFailed { path: String, expected: String },
MissingField(String),
}
impl std::fmt::Display for PatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidOperation(s) => write!(f, "invalid operation: {s}"),
Self::PathNotFound(p) => write!(f, "path not found: {p}"),
Self::TestFailed { path, expected } => {
write!(f, "test failed at {path}: expected {expected}")
}
Self::MissingField(s) => write!(f, "missing required field: {s}"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct JsonPatch {
ops: Vec<PatchOp>,
}
impl JsonPatch {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, op: PatchOp) {
self.ops.push(op);
}
pub fn ops(&self) -> &[PatchOp] {
&self.ops
}
pub fn len(&self) -> usize {
self.ops.len()
}
pub fn is_empty(&self) -> bool {
self.ops.is_empty()
}
}
pub fn parse_op_kind(s: &str) -> Result<&'static str, PatchError> {
match s {
"add" => Ok("add"),
"remove" => Ok("remove"),
"replace" => Ok("replace"),
"move" => Ok("move"),
"copy" => Ok("copy"),
"test" => Ok("test"),
other => Err(PatchError::InvalidOperation(other.to_string())),
}
}
pub fn validate_path(path: &str) -> Result<(), PatchError> {
if path.is_empty() {
return Err(PatchError::PathNotFound(path.to_string()));
}
Ok(())
}
pub fn count_ops(patch: &JsonPatch) -> [usize; 6] {
let mut counts = [0usize; 6];
for op in patch.ops() {
let idx = match op {
PatchOp::Add { .. } => 0,
PatchOp::Remove { .. } => 1,
PatchOp::Replace { .. } => 2,
PatchOp::Move { .. } => 3,
PatchOp::Copy { .. } => 4,
PatchOp::Test { .. } => 5,
};
counts[idx] += 1;
}
counts
}
pub fn has_test_ops(patch: &JsonPatch) -> bool {
patch
.ops()
.iter()
.any(|op| matches!(op, PatchOp::Test { .. }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_patch() {
let p = JsonPatch::new();
assert!(p.is_empty());
assert_eq!(p.len(), 0);
}
#[test]
fn test_push_ops() {
let mut p = JsonPatch::new();
p.push(PatchOp::Add {
path: "/a".to_string(),
value: "1".to_string(),
});
p.push(PatchOp::Remove {
path: "/b".to_string(),
});
assert_eq!(p.len(), 2);
}
#[test]
fn test_count_ops() {
let mut p = JsonPatch::new();
p.push(PatchOp::Add {
path: "/x".to_string(),
value: "v".to_string(),
});
p.push(PatchOp::Add {
path: "/y".to_string(),
value: "v".to_string(),
});
p.push(PatchOp::Remove {
path: "/z".to_string(),
});
let counts = count_ops(&p);
assert_eq!(counts[0], 2);
assert_eq!(counts[1], 1);
}
#[test]
fn test_has_test_ops_false() {
let mut p = JsonPatch::new();
p.push(PatchOp::Replace {
path: "/a".to_string(),
value: "1".to_string(),
});
assert!(!has_test_ops(&p));
}
#[test]
fn test_has_test_ops_true() {
let mut p = JsonPatch::new();
p.push(PatchOp::Test {
path: "/a".to_string(),
value: "1".to_string(),
});
assert!(has_test_ops(&p));
}
#[test]
fn test_validate_path_ok() {
assert!(validate_path("/foo").is_ok());
}
#[test]
fn test_validate_path_empty() {
assert!(validate_path("").is_err());
}
#[test]
fn test_parse_op_kind_valid() {
assert!(parse_op_kind("add").is_ok());
assert!(parse_op_kind("remove").is_ok());
}
#[test]
fn test_parse_op_kind_invalid() {
assert!(parse_op_kind("upsert").is_err());
}
#[test]
fn test_ops_slice() {
let mut p = JsonPatch::new();
p.push(PatchOp::Remove {
path: "/k".to_string(),
});
assert_eq!(p.ops().len(), 1);
}
}