oxihuman_core/
json_patch.rs1#![allow(dead_code)]
4
5#[derive(Debug, Clone, PartialEq)]
9pub enum PatchOp {
10 Add { path: String, value: String },
11 Remove { path: String },
12 Replace { path: String, value: String },
13 Move { from: String, path: String },
14 Copy { from: String, path: String },
15 Test { path: String, value: String },
16}
17
18#[derive(Debug, Clone, PartialEq)]
20pub enum PatchError {
21 InvalidOperation(String),
22 PathNotFound(String),
23 TestFailed { path: String, expected: String },
24 MissingField(String),
25}
26
27impl std::fmt::Display for PatchError {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 Self::InvalidOperation(s) => write!(f, "invalid operation: {s}"),
31 Self::PathNotFound(p) => write!(f, "path not found: {p}"),
32 Self::TestFailed { path, expected } => {
33 write!(f, "test failed at {path}: expected {expected}")
34 }
35 Self::MissingField(s) => write!(f, "missing required field: {s}"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Default)]
42pub struct JsonPatch {
43 ops: Vec<PatchOp>,
44}
45
46impl JsonPatch {
47 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn push(&mut self, op: PatchOp) {
54 self.ops.push(op);
55 }
56
57 pub fn ops(&self) -> &[PatchOp] {
59 &self.ops
60 }
61
62 pub fn len(&self) -> usize {
64 self.ops.len()
65 }
66
67 pub fn is_empty(&self) -> bool {
69 self.ops.is_empty()
70 }
71}
72
73pub fn parse_op_kind(s: &str) -> Result<&'static str, PatchError> {
75 match s {
76 "add" => Ok("add"),
77 "remove" => Ok("remove"),
78 "replace" => Ok("replace"),
79 "move" => Ok("move"),
80 "copy" => Ok("copy"),
81 "test" => Ok("test"),
82 other => Err(PatchError::InvalidOperation(other.to_string())),
83 }
84}
85
86pub fn validate_path(path: &str) -> Result<(), PatchError> {
88 if path.is_empty() {
89 return Err(PatchError::PathNotFound(path.to_string()));
90 }
91 Ok(())
92}
93
94pub fn count_ops(patch: &JsonPatch) -> [usize; 6] {
96 let mut counts = [0usize; 6];
98 for op in patch.ops() {
99 let idx = match op {
100 PatchOp::Add { .. } => 0,
101 PatchOp::Remove { .. } => 1,
102 PatchOp::Replace { .. } => 2,
103 PatchOp::Move { .. } => 3,
104 PatchOp::Copy { .. } => 4,
105 PatchOp::Test { .. } => 5,
106 };
107 counts[idx] += 1;
108 }
109 counts
110}
111
112pub fn has_test_ops(patch: &JsonPatch) -> bool {
114 patch
115 .ops()
116 .iter()
117 .any(|op| matches!(op, PatchOp::Test { .. }))
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_empty_patch() {
126 let p = JsonPatch::new();
128 assert!(p.is_empty());
129 assert_eq!(p.len(), 0);
130 }
131
132 #[test]
133 fn test_push_ops() {
134 let mut p = JsonPatch::new();
136 p.push(PatchOp::Add {
137 path: "/a".to_string(),
138 value: "1".to_string(),
139 });
140 p.push(PatchOp::Remove {
141 path: "/b".to_string(),
142 });
143 assert_eq!(p.len(), 2);
144 }
145
146 #[test]
147 fn test_count_ops() {
148 let mut p = JsonPatch::new();
150 p.push(PatchOp::Add {
151 path: "/x".to_string(),
152 value: "v".to_string(),
153 });
154 p.push(PatchOp::Add {
155 path: "/y".to_string(),
156 value: "v".to_string(),
157 });
158 p.push(PatchOp::Remove {
159 path: "/z".to_string(),
160 });
161 let counts = count_ops(&p);
162 assert_eq!(counts[0], 2); assert_eq!(counts[1], 1); }
165
166 #[test]
167 fn test_has_test_ops_false() {
168 let mut p = JsonPatch::new();
170 p.push(PatchOp::Replace {
171 path: "/a".to_string(),
172 value: "1".to_string(),
173 });
174 assert!(!has_test_ops(&p));
175 }
176
177 #[test]
178 fn test_has_test_ops_true() {
179 let mut p = JsonPatch::new();
181 p.push(PatchOp::Test {
182 path: "/a".to_string(),
183 value: "1".to_string(),
184 });
185 assert!(has_test_ops(&p));
186 }
187
188 #[test]
189 fn test_validate_path_ok() {
190 assert!(validate_path("/foo").is_ok());
192 }
193
194 #[test]
195 fn test_validate_path_empty() {
196 assert!(validate_path("").is_err());
198 }
199
200 #[test]
201 fn test_parse_op_kind_valid() {
202 assert!(parse_op_kind("add").is_ok());
204 assert!(parse_op_kind("remove").is_ok());
205 }
206
207 #[test]
208 fn test_parse_op_kind_invalid() {
209 assert!(parse_op_kind("upsert").is_err());
211 }
212
213 #[test]
214 fn test_ops_slice() {
215 let mut p = JsonPatch::new();
217 p.push(PatchOp::Remove {
218 path: "/k".to_string(),
219 });
220 assert_eq!(p.ops().len(), 1);
221 }
222}