1use serde_json::Value;
11
12pub fn apply_json_patch(doc: &mut Value, patch: &Value) -> Result<(), String> {
14 let ops = patch
15 .as_array()
16 .ok_or_else(|| "patch document must be a JSON array".to_string())?;
17 for op in ops {
18 apply_one(doc, op)?;
19 }
20 Ok(())
21}
22
23fn apply_one(doc: &mut Value, op: &Value) -> Result<(), String> {
24 let kind = op
25 .get("op")
26 .and_then(|v| v.as_str())
27 .ok_or_else(|| "patch operation missing 'op'".to_string())?;
28 match kind {
29 "add" => {
30 let path = path_of(op)?;
31 let value = require_value(op)?;
32 add(doc, &path, value)
33 }
34 "remove" => {
35 let path = path_of(op)?;
36 remove(doc, &path).map(|_| ())
37 }
38 "replace" => {
39 let path = path_of(op)?;
40 let value = require_value(op)?;
41 get(doc, &path)
43 .ok_or_else(|| format!("replace target does not exist: /{}", path.join("/")))?;
44 remove(doc, &path)?;
45 add(doc, &path, value)
46 }
47 "move" => {
48 let from = pointer_of(op, "from")?;
49 let path = path_of(op)?;
50 let value = remove(doc, &from)?;
51 add(doc, &path, value)
52 }
53 "copy" => {
54 let from = pointer_of(op, "from")?;
55 let path = path_of(op)?;
56 let value = get(doc, &from)
57 .cloned()
58 .ok_or_else(|| format!("copy source does not exist: /{}", from.join("/")))?;
59 add(doc, &path, value)
60 }
61 "test" => {
62 let path = path_of(op)?;
63 let expected = require_value(op)?;
64 let actual = get(doc, &path)
65 .ok_or_else(|| format!("test target does not exist: /{}", path.join("/")))?;
66 if *actual != expected {
67 return Err(format!("test failed at /{}", path.join("/")));
68 }
69 Ok(())
70 }
71 other => Err(format!("unsupported patch op: {other}")),
72 }
73}
74
75fn require_value(op: &Value) -> Result<Value, String> {
78 op.get("value")
79 .cloned()
80 .ok_or_else(|| "patch operation missing 'value'".to_string())
81}
82
83fn path_of(op: &Value) -> Result<Vec<String>, String> {
84 pointer_of(op, "path")
85}
86
87fn pointer_of(op: &Value, field: &str) -> Result<Vec<String>, String> {
88 let raw = op
89 .get(field)
90 .and_then(|v| v.as_str())
91 .ok_or_else(|| format!("patch operation missing '{field}'"))?;
92 parse_pointer(raw)
93}
94
95fn parse_pointer(pointer: &str) -> Result<Vec<String>, String> {
100 if pointer.is_empty() {
101 return Ok(Vec::new());
102 }
103 if !pointer.starts_with('/') {
104 return Err(format!(
105 "invalid JSON Pointer '{pointer}': must be empty or begin with '/'"
106 ));
107 }
108 pointer
109 .split('/')
110 .skip(1) .map(unescape_token)
112 .collect()
113}
114
115fn unescape_token(token: &str) -> Result<String, String> {
118 let mut out = String::with_capacity(token.len());
119 let mut chars = token.chars();
120 while let Some(c) = chars.next() {
121 if c == '~' {
122 match chars.next() {
123 Some('0') => out.push('~'),
124 Some('1') => out.push('/'),
125 other => {
126 return Err(format!(
127 "invalid JSON Pointer escape '~{}'",
128 other.map(String::from).unwrap_or_default()
129 ));
130 }
131 }
132 } else {
133 out.push(c);
134 }
135 }
136 Ok(out)
137}
138
139fn get<'a>(doc: &'a Value, path: &[String]) -> Option<&'a Value> {
140 let mut cur = doc;
141 for token in path {
142 cur = match cur {
143 Value::Object(map) => map.get(token)?,
144 Value::Array(arr) => arr.get(token.parse::<usize>().ok()?)?,
145 _ => return None,
146 };
147 }
148 Some(cur)
149}
150
151fn add(doc: &mut Value, path: &[String], value: Value) -> Result<(), String> {
152 if path.is_empty() {
153 *doc = value;
154 return Ok(());
155 }
156 let (last, parents) = path.split_last().unwrap();
157 let target = get_mut(doc, parents)
158 .ok_or_else(|| format!("add parent path does not exist: /{}", parents.join("/")))?;
159 match target {
160 Value::Object(map) => {
161 map.insert(last.clone(), value);
162 Ok(())
163 }
164 Value::Array(arr) => {
165 if last == "-" {
166 arr.push(value);
167 return Ok(());
168 }
169 let idx = last
170 .parse::<usize>()
171 .map_err(|_| format!("invalid array index: {last}"))?;
172 if idx > arr.len() {
173 return Err(format!("array index out of bounds: {idx}"));
174 }
175 arr.insert(idx, value);
176 Ok(())
177 }
178 _ => Err(format!(
179 "cannot add into non-container at /{}",
180 parents.join("/")
181 )),
182 }
183}
184
185fn remove(doc: &mut Value, path: &[String]) -> Result<Value, String> {
186 let (last, parents) = path
187 .split_last()
188 .ok_or_else(|| "cannot remove the whole document".to_string())?;
189 let target = get_mut(doc, parents)
190 .ok_or_else(|| format!("remove parent path does not exist: /{}", parents.join("/")))?;
191 match target {
192 Value::Object(map) => map
193 .remove(last)
194 .ok_or_else(|| format!("remove target does not exist: {last}")),
195 Value::Array(arr) => {
196 let idx = last
197 .parse::<usize>()
198 .map_err(|_| format!("invalid array index: {last}"))?;
199 if idx >= arr.len() {
200 return Err(format!("array index out of bounds: {idx}"));
201 }
202 Ok(arr.remove(idx))
203 }
204 _ => Err(format!(
205 "cannot remove from non-container at /{}",
206 parents.join("/")
207 )),
208 }
209}
210
211fn get_mut<'a>(doc: &'a mut Value, path: &[String]) -> Option<&'a mut Value> {
212 let mut cur = doc;
213 for token in path {
214 cur = match cur {
215 Value::Object(map) => map.get_mut(token)?,
216 Value::Array(arr) => arr.get_mut(token.parse::<usize>().ok()?)?,
217 _ => return None,
218 };
219 }
220 Some(cur)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use serde_json::json;
227
228 #[test]
229 fn replace_scalar() {
230 let mut doc = json!({"BucketName": "old", "Tags": []});
231 apply_json_patch(
232 &mut doc,
233 &json!([{"op":"replace","path":"/BucketName","value":"new"}]),
234 )
235 .unwrap();
236 assert_eq!(doc["BucketName"], "new");
237 }
238
239 #[test]
240 fn add_and_remove() {
241 let mut doc = json!({"A": 1});
242 apply_json_patch(&mut doc, &json!([{"op":"add","path":"/B","value":2}])).unwrap();
243 assert_eq!(doc["B"], 2);
244 apply_json_patch(&mut doc, &json!([{"op":"remove","path":"/A"}])).unwrap();
245 assert!(doc.get("A").is_none());
246 }
247
248 #[test]
249 fn array_append_and_index() {
250 let mut doc = json!({"L": [1, 2]});
251 apply_json_patch(&mut doc, &json!([{"op":"add","path":"/L/-","value":3}])).unwrap();
252 assert_eq!(doc["L"], json!([1, 2, 3]));
253 apply_json_patch(&mut doc, &json!([{"op":"remove","path":"/L/0"}])).unwrap();
254 assert_eq!(doc["L"], json!([2, 3]));
255 }
256
257 #[test]
258 fn move_and_copy() {
259 let mut doc = json!({"A": {"X": 1}, "B": {}});
260 apply_json_patch(
261 &mut doc,
262 &json!([{"op":"move","from":"/A/X","path":"/B/Y"}]),
263 )
264 .unwrap();
265 assert_eq!(doc["B"]["Y"], 1);
266 assert!(doc["A"].get("X").is_none());
267 apply_json_patch(
268 &mut doc,
269 &json!([{"op":"copy","from":"/B/Y","path":"/B/Z"}]),
270 )
271 .unwrap();
272 assert_eq!(doc["B"]["Z"], 1);
273 }
274
275 #[test]
276 fn test_op_mismatch_errors() {
277 let mut doc = json!({"A": 1});
278 assert!(apply_json_patch(&mut doc, &json!([{"op":"test","path":"/A","value":2}])).is_err());
279 }
280
281 #[test]
282 fn missing_value_is_rejected() {
283 let mut doc = json!({"A": 1});
286 assert!(apply_json_patch(&mut doc, &json!([{"op":"add","path":"/B"}])).is_err());
287 assert!(apply_json_patch(&mut doc, &json!([{"op":"replace","path":"/A"}])).is_err());
288 assert!(apply_json_patch(&mut doc, &json!([{"op":"test","path":"/A"}])).is_err());
289 assert_eq!(doc, json!({"A": 1}));
291 }
292
293 #[test]
294 fn malformed_pointer_is_rejected() {
295 let mut doc = json!({"BucketName": "old"});
298 assert!(apply_json_patch(
299 &mut doc,
300 &json!([{"op":"replace","path":"BucketName","value":"new"}])
301 )
302 .is_err());
303 assert_eq!(doc, json!({"BucketName": "old"}));
304 }
305
306 #[test]
307 fn escaped_pointer_tokens() {
308 let mut doc = json!({"a/b": 1, "c~d": 2});
309 apply_json_patch(
310 &mut doc,
311 &json!([{"op":"replace","path":"/a~1b","value":9}]),
312 )
313 .unwrap();
314 assert_eq!(doc["a/b"], 9);
315 apply_json_patch(
316 &mut doc,
317 &json!([{"op":"replace","path":"/c~0d","value":8}]),
318 )
319 .unwrap();
320 assert_eq!(doc["c~d"], 8);
321 }
322
323 #[test]
324 fn invalid_pointer_escape_is_rejected() {
325 let mut doc = json!({"A": 1});
328 assert!(
329 apply_json_patch(&mut doc, &json!([{"op":"add","path":"/A~2B","value":9}])).is_err()
330 );
331 assert!(apply_json_patch(&mut doc, &json!([{"op":"add","path":"/A~","value":9}])).is_err());
332 assert_eq!(doc, json!({"A": 1}));
333 }
334}