1use crate::document::{DocumentError, DocumentResult, Value, join_path};
4
5#[derive(Debug, Clone, Copy)]
11pub struct KeyedList<'a> {
12 pub prefix: &'a str,
13 pub slug_field: &'a str,
14}
15
16#[derive(Debug, Clone, Copy, Default)]
35pub struct Addressing<'a> {
36 pub keyed_lists: &'a [KeyedList<'a>],
37 pub array_rule: Option<ArrayRule<'a>>,
38}
39
40impl<'a> Addressing<'a> {
41 pub const INDEX_ONLY: Addressing<'static> = Addressing {
43 keyed_lists: &[],
44 array_rule: None,
45 };
46
47 #[must_use]
49 pub const fn keyed(keyed_lists: &'a [KeyedList<'a>]) -> Self {
50 Addressing {
51 keyed_lists,
52 array_rule: None,
53 }
54 }
55
56 #[must_use]
58 pub const fn with_array_rule(self, array_rule: Option<ArrayRule<'a>>) -> Self {
59 Addressing { array_rule, ..self }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct ArrayRule<'a> {
67 pub field: &'a str,
70 pub match_kind: MatchKind,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum MatchKind {
76 Exact,
78 Contains,
88}
89
90impl ArrayRule<'_> {
91 #[must_use]
93 pub fn matches(&self, element: &Value, segment: &str) -> bool {
94 let Some(field) = element.get(self.field).and_then(Value::as_str) else {
95 return false;
96 };
97 match self.match_kind {
98 MatchKind::Exact => field == segment,
99 MatchKind::Contains if segment.is_empty() => false,
107 MatchKind::Contains => field.to_lowercase().contains(&segment.to_lowercase()),
108 }
109 }
110}
111
112fn keyed_prefix_segments(prefix: &str) -> DocumentResult<Vec<String>> {
120 if prefix.is_empty() {
121 return Ok(Vec::new());
122 }
123 crate::document::parse_path(prefix)
124}
125
126pub fn add_keyed(
133 root: &mut Value,
134 prefix: &str,
135 slug: &str,
136 keyed_lists: &[KeyedList<'_>],
137 seed: Option<&Value>,
138 fields: &[(String, Value)],
139) -> DocumentResult<()> {
140 let segments = keyed_prefix_segments(prefix)?;
143 let registered = keyed_lists
144 .iter()
145 .any(|list| crate::document::keyed_prefix_matches(list, &segments));
146 if !registered {
147 return Err(DocumentError::UnregisteredArray {
148 path: prefix.to_string(),
149 });
150 }
151
152 add_keyed_segments(root, &segments, 0, slug, seed, fields, keyed_lists)
153}
154
155pub fn remove_keyed(
160 root: &mut Value,
161 prefix: &str,
162 slug: &str,
163 keyed_lists: &[KeyedList<'_>],
164) -> DocumentResult<usize> {
165 let segments = keyed_prefix_segments(prefix)?;
166 let registered = keyed_lists
167 .iter()
168 .any(|list| crate::document::keyed_prefix_matches(list, &segments));
169 if !registered {
170 return Err(DocumentError::UnregisteredArray {
171 path: prefix.to_string(),
172 });
173 }
174
175 remove_keyed_segments(root, &segments, 0, slug, keyed_lists)
176}
177
178fn add_keyed_segments(
179 current: &mut Value,
180 segments: &[String],
181 index: usize,
182 slug: &str,
183 seed: Option<&Value>,
184 fields: &[(String, Value)],
185 keyed_lists: &[KeyedList<'_>],
186) -> DocumentResult<()> {
187 if index + 1 < segments.len() {
188 let Value::Object(object) = current else {
189 return Err(DocumentError::NotTraversable {
190 path: join_path(&segments[..=index]),
191 got: current.kind_name().to_string(),
192 });
193 };
194 let next = object
195 .entry(segments[index].clone())
196 .or_insert_with(|| Value::Object(Default::default()));
197 return add_keyed_segments(next, segments, index + 1, slug, seed, fields, keyed_lists);
198 }
199 let array = if segments.is_empty() {
200 current
201 } else {
202 let Value::Object(object) = current else {
203 return Err(DocumentError::NotTraversable {
204 path: join_path(segments),
205 got: current.kind_name().to_string(),
206 });
207 };
208 object
209 .entry(segments[index].clone())
210 .or_insert_with(|| Value::Array(Vec::new()))
211 };
212 let Value::Array(array) = array else {
213 return Err(DocumentError::NotTraversable {
214 path: join_path(segments),
215 got: array.kind_name().to_string(),
216 });
217 };
218 let registration = keyed_lists
219 .iter()
220 .find(|list| crate::document::keyed_prefix_matches(list, segments))
221 .ok_or_else(|| DocumentError::UnregisteredArray {
222 path: join_path(segments),
223 })?;
224 if array.iter().any(|entry| {
225 entry
226 .as_object()
227 .and_then(|object| object.get(registration.slug_field))
228 .and_then(Value::as_str)
229 == Some(slug)
230 }) {
231 return Err(DocumentError::SlugAlreadyExists {
232 prefix: join_path(segments),
233 slug: slug.to_string(),
234 });
235 }
236 let mut element = Value::Object(Default::default());
237 let object = element
238 .as_object_mut()
239 .ok_or_else(|| DocumentError::NotTraversable {
240 path: join_path(segments),
241 got: "failed to create object".to_string(),
242 })?;
243 if let Some(seed) = seed.and_then(Value::as_object) {
244 for (key, value) in seed {
245 if key != registration.slug_field {
246 object.insert(key.clone(), value.clone());
247 }
248 }
249 }
250 object.insert(
251 registration.slug_field.to_string(),
252 Value::String(slug.to_string()),
253 );
254 for (key, value) in fields {
255 if key == registration.slug_field {
256 return Err(DocumentError::InvalidArgument {
257 detail: format!("field `{key}` cannot override slug field"),
258 });
259 }
260 object.insert(key.clone(), value.clone());
261 }
262 array.push(element);
263 Ok(())
264}
265
266fn remove_keyed_segments(
267 current: &mut Value,
268 segments: &[String],
269 index: usize,
270 slug: &str,
271 keyed_lists: &[KeyedList<'_>],
272) -> DocumentResult<usize> {
273 if index + 1 < segments.len() {
274 let Value::Object(object) = current else {
275 return Err(DocumentError::NotTraversable {
276 path: join_path(&segments[..=index]),
277 got: current.kind_name().to_string(),
278 });
279 };
280 let next = object
281 .get_mut(&segments[index])
282 .ok_or_else(|| DocumentError::PathNotFound {
283 path: join_path(segments),
284 })?;
285 return remove_keyed_segments(next, segments, index + 1, slug, keyed_lists);
286 }
287 let target = if segments.is_empty() {
288 current
289 } else {
290 let Value::Object(object) = current else {
291 return Err(DocumentError::NotTraversable {
292 path: join_path(segments),
293 got: current.kind_name().to_string(),
294 });
295 };
296 object
297 .get_mut(&segments[index])
298 .ok_or_else(|| DocumentError::PathNotFound {
299 path: join_path(segments),
300 })?
301 };
302 let Value::Array(array) = target else {
303 return Err(DocumentError::NotTraversable {
304 path: join_path(segments),
305 got: target.kind_name().to_string(),
306 });
307 };
308 let registration = keyed_lists
309 .iter()
310 .find(|list| crate::document::keyed_prefix_matches(list, segments))
311 .ok_or_else(|| DocumentError::UnregisteredArray {
312 path: join_path(segments),
313 })?;
314 let matches: Vec<usize> = array
315 .iter()
316 .enumerate()
317 .filter(|(_, entry)| {
318 entry
319 .as_object()
320 .and_then(|object| object.get(registration.slug_field))
321 .and_then(Value::as_str)
322 == Some(slug)
323 })
324 .map(|(index, _)| index)
325 .collect();
326 let index = match matches.as_slice() {
327 [] => {
328 return Err(DocumentError::SlugNotFound {
329 prefix: join_path(segments),
330 slug: slug.to_string(),
331 });
332 }
333 [index] => *index,
334 _ => {
335 return Err(DocumentError::AmbiguousMatch {
336 prefix: join_path(segments),
337 segment: slug.to_string(),
338 indices: matches,
339 });
340 }
341 };
342 array.remove(index);
343 Ok(index)
344}
345
346#[cfg(test)]
347mod tests {
348 #![allow(clippy::unwrap_used, clippy::panic)]
349 use super::*;
350
351 #[test]
352 fn test_add_keyed() {
353 let mut root = Value::Object(Default::default());
354 let keyed = [KeyedList {
355 prefix: "identities",
356 slug_field: "identity",
357 }];
358
359 root.as_object_mut()
360 .unwrap()
361 .insert("identities".to_string(), Value::Array(vec![]));
362
363 add_keyed(
364 &mut root,
365 "identities",
366 "me",
367 &keyed,
368 None,
369 &[
370 (
371 "email".to_string(),
372 Value::String("me@example.com".to_string()),
373 ),
374 ("name".to_string(), Value::String("Me".to_string())),
375 ],
376 )
377 .unwrap();
378
379 let arr = root.get("identities").unwrap().as_array().unwrap();
380 assert_eq!(arr.len(), 1);
381
382 let elem = &arr[0];
383 assert_eq!(elem.get("identity").unwrap().as_str().unwrap(), "me");
384 assert_eq!(
385 elem.get("email").unwrap().as_str().unwrap(),
386 "me@example.com"
387 );
388 }
389
390 #[test]
391 fn test_add_keyed_with_seed() {
392 let mut root = Value::Object(Default::default());
393 let keyed = [KeyedList {
394 prefix: "identities",
395 slug_field: "identity",
396 }];
397 root.as_object_mut()
398 .unwrap()
399 .insert("identities".to_string(), Value::Array(vec![]));
400
401 let mut seed_obj = std::collections::BTreeMap::new();
402 seed_obj.insert("enabled".to_string(), Value::Bool(true));
403 seed_obj.insert("role".to_string(), Value::String("user".to_string()));
404 seed_obj.insert(
405 "email".to_string(),
406 Value::String("default@example.com".to_string()),
407 );
408 let seed = Value::Object(seed_obj);
409
410 add_keyed(
411 &mut root,
412 "identities",
413 "alice",
414 &keyed,
415 Some(&seed),
416 &[(
417 "email".to_string(),
418 Value::String("alice@example.com".to_string()),
419 )], )
421 .unwrap();
422
423 let elem = &root.get("identities").unwrap().as_array().unwrap()[0];
424 assert_eq!(elem.get("identity").unwrap().as_str().unwrap(), "alice");
425 assert_eq!(elem.get("role").unwrap().as_str().unwrap(), "user"); assert!(elem.get("enabled").unwrap().as_bool().unwrap()); assert_eq!(
428 elem.get("email").unwrap().as_str().unwrap(),
429 "alice@example.com"
430 ); }
432
433 #[test]
434 fn test_remove_keyed() {
435 let mut root = Value::Object(Default::default());
436 let keyed = [KeyedList {
437 prefix: "identities",
438 slug_field: "identity",
439 }];
440
441 let mut elem1 = Value::Object(Default::default());
442 elem1
443 .as_object_mut()
444 .unwrap()
445 .insert("identity".to_string(), Value::String("me".to_string()));
446
447 let mut elem2 = Value::Object(Default::default());
448 elem2
449 .as_object_mut()
450 .unwrap()
451 .insert("identity".to_string(), Value::String("other".to_string()));
452
453 root.as_object_mut()
454 .unwrap()
455 .insert("identities".to_string(), Value::Array(vec![elem1, elem2]));
456
457 let removed_index = remove_keyed(&mut root, "identities", "me", &keyed).unwrap();
458
459 assert_eq!(removed_index, 0);
460 let arr = root.get("identities").unwrap().as_array().unwrap();
461 assert_eq!(arr.len(), 1);
462 assert_eq!(arr[0].get("identity").unwrap().as_str().unwrap(), "other");
463 }
464
465 #[test]
466 fn test_remove_keyed_refuses_duplicate_slug_without_mutating() {
467 let item = |email: &str| {
468 let mut value = Value::Object(Default::default());
469 let object = value.as_object_mut().unwrap();
470 object.insert("identity".to_string(), Value::String("me".to_string()));
471 object.insert("email".to_string(), Value::String(email.to_string()));
472 value
473 };
474 let mut original = Value::Object(Default::default());
475 original.as_object_mut().unwrap().insert(
476 "identities".to_string(),
477 Value::Array(vec![item("first"), item("second")]),
478 );
479 let mut root = original.clone();
480 let keyed = [KeyedList {
481 prefix: "identities",
482 slug_field: "identity",
483 }];
484
485 let error = remove_keyed(&mut root, "identities", "me", &keyed).unwrap_err();
486
487 assert!(matches!(
488 error,
489 DocumentError::AmbiguousMatch {
490 prefix,
491 segment,
492 indices
493 } if prefix == "identities"
494 && segment == "me"
495 && indices == vec![0, 1]
496 ));
497 assert_eq!(root, original);
498 }
499
500 #[test]
501 fn test_add_and_remove_keyed_nested_dotted_prefix() {
502 let mut root = Value::Object(Default::default());
505 let keyed = [KeyedList {
506 prefix: "cfg.users",
507 slug_field: "uid",
508 }];
509
510 add_keyed(
511 &mut root,
512 "cfg.users",
513 "bob",
514 &keyed,
515 None,
516 &[("role".to_string(), Value::String("dev".to_string()))],
517 )
518 .unwrap();
519
520 let arr = root
521 .get("cfg")
522 .unwrap()
523 .get("users")
524 .unwrap()
525 .as_array()
526 .unwrap();
527 assert_eq!(arr.len(), 1);
528 assert_eq!(arr[0].get("uid").unwrap().as_str().unwrap(), "bob");
529 assert_eq!(arr[0].get("role").unwrap().as_str().unwrap(), "dev");
530
531 remove_keyed(&mut root, "cfg.users", "bob", &keyed).unwrap();
532 let arr = root
533 .get("cfg")
534 .unwrap()
535 .get("users")
536 .unwrap()
537 .as_array()
538 .unwrap();
539 assert!(arr.is_empty());
540 }
541}
542
543#[cfg(test)]
544mod root_array_tests {
545 #![allow(clippy::unwrap_used)]
546 use super::*;
547 use crate::document::{Addressing, get_path};
548 use std::collections::BTreeMap;
549
550 fn element(id: &str) -> Value {
551 Value::Object(BTreeMap::from([(
552 "id".to_string(),
553 Value::String(id.to_string()),
554 )]))
555 }
556
557 #[test]
558 fn a_root_array_reads_and_edits_through_the_same_registration() {
559 let lists = [KeyedList {
564 prefix: "",
565 slug_field: "id",
566 }];
567 let mut root = Value::Array(vec![element("a"), element("b")]);
568
569 assert_eq!(
570 get_path(&root, "a.id", Addressing::keyed(&lists)).unwrap(),
571 Value::String("a".to_string())
572 );
573 assert_eq!(remove_keyed(&mut root, "", "a", &lists).unwrap(), 0);
574 assert_eq!(root.as_array().map(Vec::len), Some(1));
575
576 add_keyed(&mut root, "", "c", &lists, None, &[]).unwrap();
577 assert_eq!(
578 get_path(&root, "c.id", Addressing::keyed(&lists)).unwrap(),
579 Value::String("c".to_string())
580 );
581
582 let mut bare = Value::Array(vec![element("a")]);
584 assert_eq!(
585 remove_keyed(&mut bare, "", "a", &[]).unwrap_err().code(),
586 "document_path_not_found"
587 );
588 }
589}