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