1use crate::document::{DocumentError, DocumentResult, Value, keyed::KeyedList, path::parse_path};
4
5pub fn get_path_ref<'a>(
12 root: &'a Value,
13 path: &str,
14 keyed_lists: &[KeyedList<'_>],
15) -> DocumentResult<&'a Value> {
16 if path.is_empty() {
17 return Err(DocumentError::EmptyPath);
18 }
19
20 let segments = parse_path(path)?;
21 let mut current = root;
22 let mut accumulated_prefix = String::new();
23 let mut seg_idx = 0;
24
25 while seg_idx < segments.len() {
26 let current_seg = segments[seg_idx].as_str();
27
28 match current {
29 Value::Object(obj) => {
30 if let Some(next) = obj.get(current_seg) {
32 if !accumulated_prefix.is_empty() {
33 accumulated_prefix.push('.');
34 }
35 accumulated_prefix.push_str(current_seg);
36 current = next;
37 seg_idx += 1;
38 } else {
39 return Err(DocumentError::UnknownSegment {
40 path: path.to_string(),
41 segment: current_seg.to_string(),
42 });
43 }
44 }
45 Value::Array(arr) => {
46 if let Ok(arr_idx) = current_seg.parse::<usize>() {
48 let elem = arr
49 .get(arr_idx)
50 .ok_or_else(|| DocumentError::IndexOutOfBounds {
51 path: accumulated_prefix.clone(),
52 index: arr_idx,
53 len: arr.len(),
54 })?;
55 if !accumulated_prefix.is_empty() {
56 accumulated_prefix.push('.');
57 }
58 accumulated_prefix.push_str(current_seg);
59 current = elem;
60 seg_idx += 1;
61 } else {
62 let registration = keyed_lists
63 .iter()
64 .find(|kl| keyed_prefix_matches(kl, &accumulated_prefix))
65 .ok_or_else(|| DocumentError::UnregisteredArray {
66 path: accumulated_prefix.clone(),
67 })?;
68
69 let slug = current_seg;
70 let elem = arr
71 .iter()
72 .find(|e| {
73 if let Some(obj) = e.as_object() {
74 if let Some(Value::String(s)) = obj.get(registration.slug_field) {
75 s == slug
76 } else {
77 false
78 }
79 } else {
80 false
81 }
82 })
83 .ok_or_else(|| DocumentError::SlugNotFound {
84 prefix: accumulated_prefix.clone(),
85 slug: slug.to_string(),
86 })?;
87
88 current = elem;
89 accumulated_prefix.push('.');
90 accumulated_prefix.push_str(slug);
91 seg_idx += 1;
92 }
93 }
94 _ => {
95 return Err(DocumentError::NotTraversable {
96 path: path.to_string(),
97 got: format!("{:?}", current),
98 });
99 }
100 }
101 }
102
103 Ok(current)
104}
105
106pub fn get_path(root: &Value, path: &str, keyed_lists: &[KeyedList<'_>]) -> DocumentResult<Value> {
108 Ok(get_path_ref(root, path, keyed_lists)?.clone())
109}
110
111pub fn set_path(
118 root: &mut Value,
119 path: &str,
120 value: &Value,
121 keyed_lists: &[KeyedList<'_>],
122) -> DocumentResult<()> {
123 if path.is_empty() {
124 return Err(DocumentError::EmptyPath);
125 }
126
127 let segments = parse_path(path)?;
128 set_path_recursive(root, &segments, 0, &mut String::new(), keyed_lists, value)
129}
130
131fn set_path_recursive(
132 current: &mut Value,
133 segments: &[String],
134 idx: usize,
135 accumulated_prefix: &mut String,
136 keyed_lists: &[KeyedList<'_>],
137 value: &Value,
138) -> DocumentResult<()> {
139 if idx >= segments.len() {
140 return Err(DocumentError::EmptyPath);
141 }
142
143 let current_seg = segments[idx].as_str();
144 let is_last = idx == segments.len() - 1;
145
146 match current {
147 Value::Object(obj) => {
148 let key_to_use = current_seg.to_string();
150
151 let segments_to_consume = 1;
152
153 if is_last {
154 obj.insert(key_to_use, value.clone());
156 Ok(())
157 } else {
158 let next_idx = idx + segments_to_consume;
160 if next_idx >= segments.len() {
161 return Err(DocumentError::EmptyPath);
162 }
163
164 if !accumulated_prefix.is_empty() {
165 accumulated_prefix.push('.');
166 }
167 accumulated_prefix.push_str(&key_to_use);
168
169 use std::collections::btree_map::Entry;
171 match obj.entry(key_to_use) {
172 Entry::Occupied(mut ent) => set_path_recursive(
173 ent.get_mut(),
174 segments,
175 next_idx,
176 accumulated_prefix,
177 keyed_lists,
178 value,
179 ),
180 Entry::Vacant(ent) => {
181 let mut new_obj = Value::Object(Default::default());
182 let result = set_path_recursive(
183 &mut new_obj,
184 segments,
185 next_idx,
186 accumulated_prefix,
187 keyed_lists,
188 value,
189 );
190 if result.is_ok() {
191 ent.insert(new_obj);
192 }
193 result
194 }
195 }
196 }
197 }
198 Value::Array(arr) => {
199 if let Ok(arr_idx) = current_seg.parse::<usize>() {
201 if arr_idx >= arr.len() {
202 return Err(DocumentError::IndexOutOfBounds {
203 path: accumulated_prefix.clone(),
204 index: arr_idx,
205 len: arr.len(),
206 });
207 }
208 if is_last {
209 arr[arr_idx] = value.clone();
210 Ok(())
211 } else {
212 accumulated_prefix.push('.');
213 accumulated_prefix.push_str(current_seg);
214 set_path_recursive(
215 &mut arr[arr_idx],
216 segments,
217 idx + 1,
218 accumulated_prefix,
219 keyed_lists,
220 value,
221 )
222 }
223 } else {
224 let registration = keyed_lists
225 .iter()
226 .find(|kl| keyed_prefix_matches(kl, accumulated_prefix))
227 .ok_or_else(|| DocumentError::UnregisteredArray {
228 path: accumulated_prefix.clone(),
229 })?;
230
231 let slug = current_seg;
232 let elem_idx = arr
233 .iter()
234 .position(|e| {
235 if let Some(elem_obj) = e.as_object() {
236 if let Some(Value::String(s)) = elem_obj.get(registration.slug_field) {
237 s == slug
238 } else {
239 false
240 }
241 } else {
242 false
243 }
244 })
245 .ok_or_else(|| DocumentError::SlugNotFound {
246 prefix: accumulated_prefix.clone(),
247 slug: slug.to_string(),
248 })?;
249
250 if is_last {
251 Err(DocumentError::UnsupportedOperation {
252 format: "keyed list".to_string(),
253 operation: "set".to_string(),
254 detail:
255 "a keyed-list slug resolves to an element; set a child field instead"
256 .to_string(),
257 })
258 } else {
259 accumulated_prefix.push('.');
260 accumulated_prefix.push_str(slug);
261 set_path_recursive(
262 &mut arr[elem_idx],
263 segments,
264 idx + 1,
265 accumulated_prefix,
266 keyed_lists,
267 value,
268 )
269 }
270 }
271 }
272 _ => Err(DocumentError::NotTraversable {
273 path: accumulated_prefix.clone(),
274 got: format!("{:?}", current),
275 }),
276 }
277}
278
279fn keyed_prefix_matches(registration: &KeyedList<'_>, semantic_prefix: &str) -> bool {
280 registration.prefix == semantic_prefix
281 || crate::document::parse_path(registration.prefix)
282 .ok()
283 .is_some_and(|segments| segments.join(".") == semantic_prefix)
284}
285
286pub fn unset_path(root: &mut Value, path: &str) -> DocumentResult<()> {
291 if path.is_empty() {
292 return Err(DocumentError::EmptyPath);
293 }
294 let segments = parse_path(path)?;
295 unset_path_recursive(root, &segments, 0, &mut String::new())
296}
297
298fn unset_path_recursive(
299 current: &mut Value,
300 segments: &[String],
301 idx: usize,
302 accumulated_prefix: &mut String,
303) -> DocumentResult<()> {
304 if idx >= segments.len() {
305 return Err(DocumentError::EmptyPath);
306 }
307 let current_seg = segments[idx].as_str();
308 let is_last = idx == segments.len() - 1;
309
310 match current {
311 Value::Object(obj) => {
312 let key_to_use = current_seg.to_string();
313 let segments_to_consume = 1;
314
315 if is_last {
316 if obj.remove(&key_to_use).is_none() {
317 return Err(DocumentError::PathNotFound { path: key_to_use });
318 }
319 Ok(())
320 } else {
321 let next_idx = idx + segments_to_consume;
322 if !accumulated_prefix.is_empty() {
323 accumulated_prefix.push('.');
324 }
325 accumulated_prefix.push_str(&key_to_use);
326 if let Some(next) = obj.get_mut(&key_to_use) {
327 unset_path_recursive(next, segments, next_idx, accumulated_prefix)
328 } else {
329 Err(DocumentError::PathNotFound {
330 path: accumulated_prefix.clone(),
331 })
332 }
333 }
334 }
335 Value::Array(arr) => {
336 if let Ok(arr_idx) = current_seg.parse::<usize>() {
337 if arr_idx >= arr.len() {
338 return Err(DocumentError::IndexOutOfBounds {
339 path: accumulated_prefix.clone(),
340 index: arr_idx,
341 len: arr.len(),
342 });
343 }
344 if is_last {
345 arr.remove(arr_idx);
346 Ok(())
347 } else {
348 accumulated_prefix.push('.');
349 accumulated_prefix.push_str(current_seg);
350 unset_path_recursive(&mut arr[arr_idx], segments, idx + 1, accumulated_prefix)
351 }
352 } else {
353 Err(DocumentError::UnregisteredArray {
354 path: accumulated_prefix.clone(),
355 })
356 }
357 }
358 _ => Err(DocumentError::NotTraversable {
359 path: accumulated_prefix.clone(),
360 got: format!("{:?}", current),
361 }),
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 #![allow(
368 clippy::unwrap_used,
369 clippy::panic,
370 clippy::expect_used,
371 clippy::bool_assert_comparison
372 )]
373 use super::*;
374
375 fn make_test_object() -> Value {
376 let mut root = Value::Object(Default::default());
377 let mut imap = Value::Object(Default::default());
378 imap.as_object_mut().unwrap().insert(
379 "host".to_string(),
380 Value::String("mail.example.com".to_string()),
381 );
382 imap.as_object_mut()
383 .unwrap()
384 .insert("port".to_string(), Value::Integer(993));
385
386 root.as_object_mut()
387 .unwrap()
388 .insert("imap".to_string(), imap);
389
390 root
391 }
392
393 #[test]
394 fn test_get_path_simple() {
395 let root = make_test_object();
396 let result = get_path(&root, "imap.host", &[]).unwrap();
397 assert_eq!(result.as_str().unwrap(), "mail.example.com");
398 }
399
400 #[test]
401 fn test_get_path_integer() {
402 let root = make_test_object();
403 let result = get_path(&root, "imap.port", &[]).unwrap();
404 assert_eq!(result.as_integer().unwrap(), 993);
405 }
406
407 #[test]
408 fn test_set_path_new_key() {
409 let mut root = make_test_object();
410 set_path(&mut root, "imap.tls", &Value::Bool(true), &[]).unwrap();
411
412 let result = get_path(&root, "imap.tls", &[]).unwrap();
413 assert_eq!(result.as_bool().unwrap(), true);
414 }
415
416 #[test]
417 fn test_set_path_overwrite() {
418 let mut root = make_test_object();
419 set_path(&mut root, "imap.port", &Value::Integer(587), &[]).unwrap();
420
421 let result = get_path(&root, "imap.port", &[]).unwrap();
422 assert_eq!(result.as_integer().unwrap(), 587);
423 }
424
425 #[test]
426 fn test_set_path_array_value() {
427 let mut root = Value::Object(Default::default());
428 let value = Value::Array(vec![
429 Value::String("dev".to_string()),
430 Value::String("staging".to_string()),
431 ]);
432 set_path(&mut root, "tags", &value, &[]).unwrap();
433
434 let result = get_path(&root, "tags", &[]).unwrap();
435 let arr = result.as_array().unwrap();
436 assert_eq!(arr.len(), 2);
437 }
438
439 fn make_steps_object() -> Value {
440 let mut root = Value::Object(Default::default());
442 let mut s0 = Value::Object(Default::default());
443 s0.as_object_mut()
444 .unwrap()
445 .insert("name".to_string(), Value::String("a".to_string()));
446 s0.as_object_mut()
447 .unwrap()
448 .insert("port".to_string(), Value::Integer(1));
449 let mut s1 = Value::Object(Default::default());
450 s1.as_object_mut()
451 .unwrap()
452 .insert("name".to_string(), Value::String("b".to_string()));
453 s1.as_object_mut()
454 .unwrap()
455 .insert("port".to_string(), Value::Integer(2));
456 root.as_object_mut()
457 .unwrap()
458 .insert("steps".to_string(), Value::Array(vec![s0, s1]));
459 root
460 }
461
462 #[test]
463 fn test_get_path_numeric_index() {
464 let root = make_steps_object();
465 let name = get_path(&root, "steps.0.name", &[]).unwrap();
466 assert_eq!(name.as_str().unwrap(), "a");
467 let port = get_path(&root, "steps.1.port", &[]).unwrap();
468 assert_eq!(port.as_integer().unwrap(), 2);
469 }
470
471 #[test]
472 fn test_set_path_numeric_index() {
473 let mut root = make_steps_object();
474 set_path(&mut root, "steps.0.port", &Value::Integer(99), &[]).unwrap();
475 let result = get_path(&root, "steps.0.port", &[]).unwrap();
476 assert_eq!(result.as_integer().unwrap(), 99);
477 let other = get_path(&root, "steps.1.port", &[]).unwrap();
479 assert_eq!(other.as_integer().unwrap(), 2);
480 }
481
482 #[test]
483 fn test_get_path_index_out_of_bounds() {
484 let root = make_steps_object();
485 let err = get_path(&root, "steps.5.name", &[]).unwrap_err();
486 assert!(matches!(
487 err,
488 DocumentError::IndexOutOfBounds {
489 index: 5,
490 len: 2,
491 ..
492 }
493 ));
494 }
495
496 #[test]
497 fn test_remove_path_numeric_index() {
498 let mut root = make_steps_object();
499 unset_path(&mut root, "steps.0").unwrap();
500 let arr = get_path(&root, "steps", &[]).unwrap();
501 let arr = arr.as_array().unwrap();
502 assert_eq!(arr.len(), 1);
503 assert_eq!(arr[0].get("name").unwrap().as_str().unwrap(), "b");
504 }
505}