agent_first_data/document/
keyed.rs1use crate::document::{DocumentError, DocumentResult, Value};
4
5#[derive(Debug, Clone, Copy)]
11pub struct KeyedList<'a> {
12 pub prefix: &'a str,
13 pub slug_field: &'a str,
14}
15
16pub fn add_keyed(
23 root: &mut Value,
24 prefix: &str,
25 slug: &str,
26 keyed_lists: &[KeyedList<'_>],
27 seed: Option<&Value>,
28 fields: &[(String, Value)],
29) -> DocumentResult<()> {
30 let segments = crate::document::parse_path(prefix)?;
33 let registered = keyed_lists.iter().any(|list| {
34 crate::document::parse_path(list.prefix).ok().as_deref() == Some(segments.as_slice())
35 });
36 if !registered {
37 return Err(DocumentError::UnregisteredArray {
38 path: prefix.to_string(),
39 });
40 }
41
42 if slug.contains('.') {
44 return Err(DocumentError::ParseError {
45 format: "slug".to_string(),
46 detail: format!("slug `{slug}` must not contain '.' (path separator)"),
47 });
48 }
49
50 add_keyed_segments(root, &segments, 0, slug, seed, fields, keyed_lists)
51}
52
53pub fn remove_keyed(
55 root: &mut Value,
56 prefix: &str,
57 slug: &str,
58 keyed_lists: &[KeyedList<'_>],
59) -> DocumentResult<()> {
60 let segments = crate::document::parse_path(prefix)?;
61 let registered = keyed_lists.iter().any(|list| {
62 crate::document::parse_path(list.prefix).ok().as_deref() == Some(segments.as_slice())
63 });
64 if !registered {
65 return Err(DocumentError::UnregisteredArray {
66 path: prefix.to_string(),
67 });
68 }
69
70 remove_keyed_segments(root, &segments, 0, slug, keyed_lists)
71}
72
73fn add_keyed_segments(
74 current: &mut Value,
75 segments: &[String],
76 index: usize,
77 slug: &str,
78 seed: Option<&Value>,
79 fields: &[(String, Value)],
80 keyed_lists: &[KeyedList<'_>],
81) -> DocumentResult<()> {
82 if index + 1 < segments.len() {
83 let Value::Object(object) = current else {
84 return Err(DocumentError::NotTraversable {
85 path: segments[..=index].join("."),
86 got: "not an object".to_string(),
87 });
88 };
89 let next = object
90 .entry(segments[index].clone())
91 .or_insert_with(|| Value::Object(Default::default()));
92 return add_keyed_segments(next, segments, index + 1, slug, seed, fields, keyed_lists);
93 }
94 let Value::Object(object) = current else {
95 return Err(DocumentError::NotTraversable {
96 path: segments.join("."),
97 got: "not an object".to_string(),
98 });
99 };
100 let array = object
101 .entry(segments[index].clone())
102 .or_insert_with(|| Value::Array(Vec::new()));
103 let Value::Array(array) = array else {
104 return Err(DocumentError::NotTraversable {
105 path: segments.join("."),
106 got: "not an array".to_string(),
107 });
108 };
109 let registration = keyed_lists
110 .iter()
111 .find(|list| crate::document::parse_path(list.prefix).ok().as_deref() == Some(segments))
112 .ok_or_else(|| DocumentError::UnregisteredArray {
113 path: segments.join("."),
114 })?;
115 if array.iter().any(|entry| {
116 entry
117 .as_object()
118 .and_then(|object| object.get(registration.slug_field))
119 .and_then(Value::as_str)
120 == Some(slug)
121 }) {
122 return Err(DocumentError::SlugAlreadyExists {
123 prefix: segments.join("."),
124 slug: slug.to_string(),
125 });
126 }
127 let mut element = Value::Object(Default::default());
128 let object = element
129 .as_object_mut()
130 .ok_or_else(|| DocumentError::NotTraversable {
131 path: segments.join("."),
132 got: "failed to create object".to_string(),
133 })?;
134 if let Some(seed) = seed.and_then(Value::as_object) {
135 for (key, value) in seed {
136 if key != registration.slug_field {
137 object.insert(key.clone(), value.clone());
138 }
139 }
140 }
141 object.insert(
142 registration.slug_field.to_string(),
143 Value::String(slug.to_string()),
144 );
145 for (key, value) in fields {
146 if key == registration.slug_field {
147 return Err(DocumentError::ParseError {
148 format: "keyed list".to_string(),
149 detail: format!("field `{key}` cannot override slug field"),
150 });
151 }
152 object.insert(key.clone(), value.clone());
153 }
154 array.push(element);
155 Ok(())
156}
157
158fn remove_keyed_segments(
159 current: &mut Value,
160 segments: &[String],
161 index: usize,
162 slug: &str,
163 keyed_lists: &[KeyedList<'_>],
164) -> DocumentResult<()> {
165 if index + 1 < segments.len() {
166 let Value::Object(object) = current else {
167 return Err(DocumentError::NotTraversable {
168 path: segments[..=index].join("."),
169 got: "not an object".to_string(),
170 });
171 };
172 let next = object
173 .get_mut(&segments[index])
174 .ok_or_else(|| DocumentError::PathNotFound {
175 path: segments.join("."),
176 })?;
177 return remove_keyed_segments(next, segments, index + 1, slug, keyed_lists);
178 }
179 let Value::Object(object) = current else {
180 return Err(DocumentError::NotTraversable {
181 path: segments.join("."),
182 got: "not an object".to_string(),
183 });
184 };
185 let array = object
186 .get_mut(&segments[index])
187 .and_then(Value::as_array_mut)
188 .ok_or_else(|| DocumentError::PathNotFound {
189 path: segments.join("."),
190 })?;
191 let registration = keyed_lists
192 .iter()
193 .find(|list| crate::document::parse_path(list.prefix).ok().as_deref() == Some(segments))
194 .ok_or_else(|| DocumentError::UnregisteredArray {
195 path: segments.join("."),
196 })?;
197 let before = array.len();
198 array.retain(|entry| {
199 entry
200 .as_object()
201 .and_then(|object| object.get(registration.slug_field))
202 .and_then(Value::as_str)
203 != Some(slug)
204 });
205 if before == array.len() {
206 return Err(DocumentError::SlugNotFound {
207 prefix: segments.join("."),
208 slug: slug.to_string(),
209 });
210 }
211 Ok(())
212}
213
214#[cfg(test)]
215mod tests {
216 #![allow(clippy::unwrap_used, clippy::panic)]
217 use super::*;
218
219 #[test]
220 fn test_add_keyed() {
221 let mut root = Value::Object(Default::default());
222 let keyed = [KeyedList {
223 prefix: "identities",
224 slug_field: "identity",
225 }];
226
227 root.as_object_mut()
228 .unwrap()
229 .insert("identities".to_string(), Value::Array(vec![]));
230
231 add_keyed(
232 &mut root,
233 "identities",
234 "me",
235 &keyed,
236 None,
237 &[
238 (
239 "email".to_string(),
240 Value::String("me@example.com".to_string()),
241 ),
242 ("name".to_string(), Value::String("Me".to_string())),
243 ],
244 )
245 .unwrap();
246
247 let arr = root.get("identities").unwrap().as_array().unwrap();
248 assert_eq!(arr.len(), 1);
249
250 let elem = &arr[0];
251 assert_eq!(elem.get("identity").unwrap().as_str().unwrap(), "me");
252 assert_eq!(
253 elem.get("email").unwrap().as_str().unwrap(),
254 "me@example.com"
255 );
256 }
257
258 #[test]
259 fn test_add_keyed_with_seed() {
260 let mut root = Value::Object(Default::default());
261 let keyed = [KeyedList {
262 prefix: "identities",
263 slug_field: "identity",
264 }];
265 root.as_object_mut()
266 .unwrap()
267 .insert("identities".to_string(), Value::Array(vec![]));
268
269 let mut seed_obj = std::collections::BTreeMap::new();
270 seed_obj.insert("enabled".to_string(), Value::Bool(true));
271 seed_obj.insert("role".to_string(), Value::String("user".to_string()));
272 seed_obj.insert(
273 "email".to_string(),
274 Value::String("default@example.com".to_string()),
275 );
276 let seed = Value::Object(seed_obj);
277
278 add_keyed(
279 &mut root,
280 "identities",
281 "alice",
282 &keyed,
283 Some(&seed),
284 &[(
285 "email".to_string(),
286 Value::String("alice@example.com".to_string()),
287 )], )
289 .unwrap();
290
291 let elem = &root.get("identities").unwrap().as_array().unwrap()[0];
292 assert_eq!(elem.get("identity").unwrap().as_str().unwrap(), "alice");
293 assert_eq!(elem.get("role").unwrap().as_str().unwrap(), "user"); assert!(elem.get("enabled").unwrap().as_bool().unwrap()); assert_eq!(
296 elem.get("email").unwrap().as_str().unwrap(),
297 "alice@example.com"
298 ); }
300
301 #[test]
302 fn test_remove_keyed() {
303 let mut root = Value::Object(Default::default());
304 let keyed = [KeyedList {
305 prefix: "identities",
306 slug_field: "identity",
307 }];
308
309 let mut elem1 = Value::Object(Default::default());
310 elem1
311 .as_object_mut()
312 .unwrap()
313 .insert("identity".to_string(), Value::String("me".to_string()));
314
315 let mut elem2 = Value::Object(Default::default());
316 elem2
317 .as_object_mut()
318 .unwrap()
319 .insert("identity".to_string(), Value::String("other".to_string()));
320
321 root.as_object_mut()
322 .unwrap()
323 .insert("identities".to_string(), Value::Array(vec![elem1, elem2]));
324
325 remove_keyed(&mut root, "identities", "me", &keyed).unwrap();
326
327 let arr = root.get("identities").unwrap().as_array().unwrap();
328 assert_eq!(arr.len(), 1);
329 assert_eq!(arr[0].get("identity").unwrap().as_str().unwrap(), "other");
330 }
331
332 #[test]
333 fn test_add_and_remove_keyed_nested_dotted_prefix() {
334 let mut root = Value::Object(Default::default());
337 let keyed = [KeyedList {
338 prefix: "cfg.users",
339 slug_field: "uid",
340 }];
341
342 add_keyed(
343 &mut root,
344 "cfg.users",
345 "bob",
346 &keyed,
347 None,
348 &[("role".to_string(), Value::String("dev".to_string()))],
349 )
350 .unwrap();
351
352 let arr = root
353 .get("cfg")
354 .unwrap()
355 .get("users")
356 .unwrap()
357 .as_array()
358 .unwrap();
359 assert_eq!(arr.len(), 1);
360 assert_eq!(arr[0].get("uid").unwrap().as_str().unwrap(), "bob");
361 assert_eq!(arr[0].get("role").unwrap().as_str().unwrap(), "dev");
362
363 remove_keyed(&mut root, "cfg.users", "bob", &keyed).unwrap();
364 let arr = root
365 .get("cfg")
366 .unwrap()
367 .get("users")
368 .unwrap()
369 .as_array()
370 .unwrap();
371 assert!(arr.is_empty());
372 }
373}