1use std::collections::BTreeMap;
31
32use serde_json::Value;
33
34use crate::merge::sdl::{IdentityRule, MergeStrategyType, SdlDocument};
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum PathSegment {
39 Key(String),
41 Identity(String),
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct CanonicalPath {
48 segments: Vec<PathSegment>,
49}
50
51impl CanonicalPath {
52 pub fn parse(path: &str) -> Result<Self, PathError> {
59 if path.is_empty() {
60 return Err(PathError::EmptyPath);
61 }
62
63 let mut segments = Vec::new();
64 let mut remaining = path;
65
66 while !remaining.is_empty() {
67 if remaining.starts_with('[') {
69 let close = remaining
70 .find(']')
71 .ok_or_else(|| PathError::InvalidSyntax {
72 path: path.to_string(),
73 detail: "unclosed bracket".to_string(),
74 })?;
75 let identity = &remaining[1..close];
76 if identity.is_empty() {
77 return Err(PathError::InvalidSyntax {
78 path: path.to_string(),
79 detail: "empty identity in brackets".to_string(),
80 });
81 }
82 segments.push(PathSegment::Identity(identity.to_string()));
83 remaining = &remaining[close + 1..];
84 if !remaining.is_empty() {
86 if remaining.starts_with('.') {
87 remaining = &remaining[1..]; } else {
89 return Err(PathError::InvalidSyntax {
90 path: path.to_string(),
91 detail: format!(
92 "expected '.' or end after ']', got '{}'",
93 &remaining[..1]
94 ),
95 });
96 }
97 }
98 } else {
99 let end = remaining.find(['.', '[']).unwrap_or(remaining.len());
101 let key = &remaining[..end];
102 if key.is_empty() {
103 return Err(PathError::InvalidSyntax {
104 path: path.to_string(),
105 detail: "empty key segment".to_string(),
106 });
107 }
108 segments.push(PathSegment::Key(key.to_string()));
109 remaining = &remaining[end..];
110 if remaining.starts_with('.') {
111 remaining = &remaining[1..]; }
113 }
114 }
115
116 Ok(CanonicalPath { segments })
117 }
118
119 pub fn segments(&self) -> &[PathSegment] {
121 &self.segments
122 }
123
124 pub fn parent(&self) -> Option<CanonicalPath> {
126 if self.segments.len() <= 1 {
127 return None;
128 }
129 Some(CanonicalPath {
130 segments: self.segments[..self.segments.len() - 1].to_vec(),
131 })
132 }
133
134 pub fn last_segment(&self) -> Option<&PathSegment> {
136 self.segments.last()
137 }
138}
139
140impl std::fmt::Display for CanonicalPath {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 for (i, seg) in self.segments.iter().enumerate() {
143 if i > 0 {
144 match seg {
147 PathSegment::Identity(_) => {}
148 PathSegment::Key(_) => write!(f, ".")?,
149 }
150 }
151 match seg {
152 PathSegment::Key(k) => write!(f, "{k}")?,
153 PathSegment::Identity(id) => write!(f, "[{id}]")?,
154 }
155 }
156 Ok(())
157 }
158}
159
160pub fn build_path_map(value: &Value, schema: &SdlDocument) -> BTreeMap<String, Value> {
168 let mut map = BTreeMap::new();
169 build_path_map_inner(value, schema, "", &mut map);
170 map
171}
172
173fn build_path_map_inner(
174 value: &Value,
175 schema: &SdlDocument,
176 prefix: &str,
177 map: &mut BTreeMap<String, Value>,
178) {
179 match value {
180 Value::Object(obj) => {
181 for (key, val) in obj {
182 let path = if prefix.is_empty() {
183 key.clone()
184 } else {
185 format!("{prefix}.{key}")
186 };
187 build_path_map_inner(val, schema, &path, map);
188 }
189 }
190 Value::Array(arr) => {
191 let identity_rule = schema.property_def(prefix).and_then(|def| {
193 if matches!(
194 def.merge.strategy_type,
195 MergeStrategyType::OrderedUnique
196 | MergeStrategyType::SetUnion
197 | MergeStrategyType::EdgeList
198 ) {
199 def.merge.identity.clone()
200 } else {
201 None
202 }
203 });
204
205 match identity_rule {
206 Some(IdentityRule::PrimitiveValue) => {
207 map.insert(prefix.to_string(), Value::Array(arr.clone()));
209 for val in arr {
211 let identity = value_to_identity_string(val);
212 let path = format!("{prefix}[{identity}]");
213 map.insert(path, val.clone());
214 }
215 }
216 Some(IdentityRule::Key { key: identity_key }) => {
217 map.insert(prefix.to_string(), Value::Array(arr.clone()));
219 for val in arr {
221 if let Some(identity) = val.get(&identity_key).and_then(|v| v.as_str()) {
222 let path = format!("{prefix}[{identity}]");
223 map.insert(path.clone(), val.clone());
224 build_path_map_inner(val, schema, &path, map);
225 }
226 }
227 }
228 None => {
229 for (idx, val) in arr.iter().enumerate() {
231 let path = format!("{prefix}[{idx}]");
232 map.insert(path.clone(), val.clone());
233 build_path_map_inner(val, schema, &path, map);
234 }
235 }
236 }
237 }
238 _ => {
239 map.insert(prefix.to_string(), value.clone());
241 }
242 }
243}
244
245fn value_to_identity_string(val: &Value) -> String {
247 match val {
248 Value::String(s) => s.clone(),
249 Value::Number(n) => n.to_string(),
250 Value::Bool(b) => b.to_string(),
251 Value::Null => "null".to_string(),
252 _ => {
253 serde_json::to_string(val).unwrap_or_else(|_| "unknown".to_string())
255 }
256 }
257}
258
259pub fn resolve_path<'a>(root: &'a Value, path: &CanonicalPath) -> Option<&'a Value> {
261 let mut current = root;
262
263 for segment in path.segments() {
264 current = match segment {
265 PathSegment::Key(key) => match current {
266 Value::Object(map) => map.get(key)?,
267 _ => return None,
268 },
269 PathSegment::Identity(id) => match current {
270 Value::Array(arr) => {
271 if idx_from_str(id) < arr.len() {
278 let candidate = &arr[idx_from_str(id)];
280 if matches!(
282 candidate,
283 Value::String(_) | Value::Number(_) | Value::Bool(_)
284 ) {
285 return Some(candidate);
286 }
287 }
288 arr.iter().find(|item| item_has_identity(item, id))?
290 }
291 _ => return None,
292 },
293 };
294 }
295
296 Some(current)
297}
298
299fn item_has_identity(item: &Value, identity: &str) -> bool {
301 match item {
302 Value::String(s) => s == identity,
303 Value::Number(n) => n.to_string() == identity,
304 Value::Bool(b) => b.to_string() == identity,
305 Value::Object(map) => {
306 map.values().any(|v| match v {
308 Value::String(s) => s == identity,
309 Value::Number(n) => n.to_string() == identity,
310 _ => false,
311 })
312 }
313 _ => false,
314 }
315}
316
317fn idx_from_str(s: &str) -> usize {
318 s.parse().unwrap_or(usize::MAX)
319}
320
321pub fn path_union(maps: &[&BTreeMap<String, Value>]) -> Vec<String> {
325 let mut seen: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
326 for map in maps {
327 for key in map.keys() {
328 seen.insert(key.as_str());
329 }
330 }
331 seen.into_iter().map(String::from).collect()
332}
333
334#[derive(Debug, thiserror::Error)]
337pub enum PathError {
338 #[error("path cannot be empty")]
339 EmptyPath,
340
341 #[error("invalid path syntax '{path}': {detail}")]
342 InvalidSyntax { path: String, detail: String },
343}
344
345#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::merge::sdl::SdlDocument;
351 use serde_json::json;
352
353 fn test_sdl() -> SdlDocument {
354 SdlDocument::from_yaml(
355 r#"
356schema:
357 version: "1.0"
358 required: []
359 properties:
360 characters:
361 type: array
362 merge:
363 type: ordered_unique
364 identity:
365 mode: key
366 key: id
367 tags:
368 type: array
369 merge:
370 type: ordered_unique
371 identity:
372 mode: primitive_value
373"#,
374 )
375 .unwrap()
376 }
377
378 #[test]
379 fn test_parse_simple_path() {
380 let path = CanonicalPath::parse("name").unwrap();
381 assert_eq!(path.segments(), &[PathSegment::Key("name".to_string())]);
382 }
383
384 #[test]
385 fn test_parse_nested_path() {
386 let path = CanonicalPath::parse("properties.homeworld").unwrap();
387 assert_eq!(
388 path.segments(),
389 &[
390 PathSegment::Key("properties".to_string()),
391 PathSegment::Key("homeworld".to_string()),
392 ]
393 );
394 }
395
396 #[test]
397 fn test_parse_identity_path() {
398 let path = CanonicalPath::parse("characters[obiwan]").unwrap();
399 assert_eq!(
400 path.segments(),
401 &[
402 PathSegment::Key("characters".to_string()),
403 PathSegment::Identity("obiwan".to_string()),
404 ]
405 );
406 }
407
408 #[test]
409 fn test_parse_identity_with_subpath() {
410 let path = CanonicalPath::parse("characters[obiwan].name").unwrap();
411 assert_eq!(
412 path.segments(),
413 &[
414 PathSegment::Key("characters".to_string()),
415 PathSegment::Identity("obiwan".to_string()),
416 PathSegment::Key("name".to_string()),
417 ]
418 );
419 }
420
421 #[test]
422 fn test_path_to_string() {
423 let path = CanonicalPath::parse("characters[obiwan].name").unwrap();
424 assert_eq!(path.to_string(), "characters[obiwan].name");
425 }
426
427 #[test]
428 fn test_build_path_map_with_identity_keys() {
429 let value = json!({
430 "name": "Luke",
431 "characters": [
432 {"id": "obiwan", "name": "Obi-Wan"},
433 {"id": "anakin", "name": "Anakin"}
434 ],
435 "tags": ["jedi", "force"]
436 });
437
438 let schema = test_sdl();
439 let map = build_path_map(&value, &schema);
440
441 assert_eq!(map.get("name"), Some(&json!("Luke")));
443
444 assert_eq!(map.get("characters[obiwan].name"), Some(&json!("Obi-Wan")));
446 assert_eq!(map.get("characters[anakin].name"), Some(&json!("Anakin")));
447
448 assert!(map.contains_key("tags[jedi]"));
450 assert!(map.contains_key("tags[force]"));
451
452 assert!(map.contains_key("characters[obiwan]"));
454 assert!(map.contains_key("characters[anakin]"));
455 }
456
457 #[test]
458 fn test_resolve_simple_path() {
459 let value = json!({"name": "Luke", "homeworld": "Tatooine"});
460 let path = CanonicalPath::parse("name").unwrap();
461 assert_eq!(resolve_path(&value, &path), Some(&json!("Luke")));
462 }
463
464 #[test]
465 fn test_resolve_nested_path() {
466 let value = json!({"properties": {"homeworld": "Tatooine"}});
467 let path = CanonicalPath::parse("properties.homeworld").unwrap();
468 assert_eq!(resolve_path(&value, &path), Some(&json!("Tatooine")));
469 }
470
471 #[test]
472 fn test_resolve_identity_path() {
473 let value = json!({
474 "characters": [
475 {"id": "obiwan", "name": "Obi-Wan"},
476 ]
477 });
478 let path = CanonicalPath::parse("characters[obiwan].name").unwrap();
479 assert_eq!(resolve_path(&value, &path), Some(&json!("Obi-Wan")));
480 }
481
482 #[test]
483 fn test_resolve_nonexistent_path() {
484 let value = json!({"name": "Luke"});
485 let path = CanonicalPath::parse("nonexistent").unwrap();
486 assert_eq!(resolve_path(&value, &path), None);
487 }
488
489 #[test]
490 fn test_parent_path() {
491 let path = CanonicalPath::parse("characters[obiwan].name").unwrap();
492 let parent = path.parent().unwrap();
493 assert_eq!(parent.to_string(), "characters[obiwan]");
494
495 let grandparent = parent.parent().unwrap();
496 assert_eq!(grandparent.to_string(), "characters");
497 }
498
499 #[test]
500 fn test_parent_of_root_is_none() {
501 let path = CanonicalPath::parse("name").unwrap();
502 assert!(path.parent().is_none());
503 }
504
505 #[test]
506 fn test_path_union() {
507 let map_a: BTreeMap<String, Value> =
508 [("a".to_string(), json!(1)), ("b".to_string(), json!(2))]
509 .into_iter()
510 .collect();
511 let map_b: BTreeMap<String, Value> =
512 [("b".to_string(), json!(20)), ("c".to_string(), json!(3))]
513 .into_iter()
514 .collect();
515
516 let union = path_union(&[&map_a, &map_b]);
517 assert_eq!(union, vec!["a", "b", "c"]);
518 }
519
520 #[test]
521 fn test_empty_path_error() {
522 assert!(CanonicalPath::parse("").is_err());
523 }
524}