1use std::collections::HashMap;
7use std::sync::Arc;
8
9use crate::{is_node_single_cardinality, StaticNode, StaticNodegroup};
10
11#[derive(Debug, Clone)]
17pub enum PathError {
18 EmptyPath,
20 AliasNotFound {
22 segment: String,
23 parent_alias: Option<String>,
24 },
25 UnderscoreOnNonCollector { node_alias: String },
27 StarOnSingleCardinality { node_alias: String },
29 NoNodegroup { node_alias: String },
31 ModelNotInitialized(String),
33 TilesNotInitialized,
35}
36
37impl std::fmt::Display for PathError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 PathError::EmptyPath => write!(f, "Path is empty"),
41 PathError::AliasNotFound {
42 segment,
43 parent_alias,
44 } => {
45 if let Some(parent) = parent_alias {
46 write!(
47 f,
48 "No child with alias '{}' found under '{}'",
49 segment, parent
50 )
51 } else {
52 write!(f, "No child with alias '{}' found under root node", segment)
53 }
54 }
55 PathError::UnderscoreOnNonCollector { node_alias } => {
56 write!(
57 f,
58 "'_' used on node '{}' which is not a collector (no inner/outer split)",
59 node_alias
60 )
61 }
62 PathError::StarOnSingleCardinality { node_alias } => {
63 write!(
64 f,
65 "'*' used on node '{}' which is single-cardinality",
66 node_alias
67 )
68 }
69 PathError::NoNodegroup { node_alias } => {
70 write!(f, "Node '{}' has no nodegroup_id", node_alias)
71 }
72 PathError::ModelNotInitialized(msg) => {
73 write!(f, "Model not initialized: {}", msg)
74 }
75 PathError::TilesNotInitialized => write!(f, "Tiles not initialized"),
76 }
77 }
78}
79
80impl std::error::Error for PathError {}
81
82impl From<String> for PathError {
83 fn from(s: String) -> Self {
84 PathError::ModelNotInitialized(s)
85 }
86}
87
88#[derive(Debug, Clone)]
98pub struct PathResolutionInfo {
99 pub target_node: Arc<StaticNode>,
101 pub nodegroup_id: String,
103 pub parent_nodegroup_id: Option<String>,
105 pub child_node_ids: Vec<String>,
107 pub is_single: bool,
109}
110
111pub fn resolve_path_segments(
132 path: &str,
133 root_node: &Arc<StaticNode>,
134 nodes: &HashMap<String, Arc<StaticNode>>,
135 edges: &HashMap<String, Vec<String>>,
136 nodegroups: Option<&HashMap<String, Arc<StaticNodegroup>>>,
137) -> Result<PathResolutionInfo, PathError> {
138 let segments: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
140
141 if segments.is_empty() {
142 return Err(PathError::EmptyPath);
143 }
144
145 let mut current_node = Arc::clone(root_node);
146 let mut parent_nodegroup_id: Option<String> = None;
147
148 for segment in &segments {
149 if *segment == "_" {
152 if !current_node.is_collector {
153 return Err(PathError::UnderscoreOnNonCollector {
154 node_alias: current_node.alias.clone().unwrap_or_default(),
155 });
156 }
157 continue;
158 }
159 if *segment == "*" {
161 if is_node_single_cardinality(¤t_node, nodegroups) {
162 return Err(PathError::StarOnSingleCardinality {
163 node_alias: current_node.alias.clone().unwrap_or_default(),
164 });
165 }
166 continue;
167 }
168
169 let child_ids = edges.get(¤t_node.nodeid).cloned().unwrap_or_default();
171
172 let matched = child_ids.iter().find_map(|child_id| {
174 nodes.get(child_id).and_then(|child_node| {
175 if child_node.alias.as_deref() == Some(segment) {
176 Some(Arc::clone(child_node))
177 } else {
178 None
179 }
180 })
181 });
182
183 parent_nodegroup_id = current_node.nodegroup_id.clone();
184 current_node = matched.ok_or_else(|| PathError::AliasNotFound {
185 segment: segment.to_string(),
186 parent_alias: current_node.alias.clone(),
187 })?;
188 }
189
190 let nodegroup_id = current_node
192 .nodegroup_id
193 .clone()
194 .ok_or_else(|| PathError::NoNodegroup {
195 node_alias: current_node.alias.clone().unwrap_or_default(),
196 })?;
197
198 let child_node_ids = edges.get(¤t_node.nodeid).cloned().unwrap_or_default();
200
201 let is_single = is_node_single_cardinality(¤t_node, nodegroups);
203
204 Ok(PathResolutionInfo {
205 target_node: current_node,
206 nodegroup_id,
207 parent_nodegroup_id,
208 child_node_ids,
209 is_single,
210 })
211}
212
213#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn make_node(
223 nodeid: &str,
224 alias: &str,
225 nodegroup_id: Option<&str>,
226 is_collector: bool,
227 istopnode: bool,
228 ) -> Arc<StaticNode> {
229 Arc::new(StaticNode {
230 nodeid: nodeid.to_string(),
231 name: alias.to_string(),
232 alias: Some(alias.to_string()),
233 datatype: "string".to_string(),
234 is_collector,
235 nodegroup_id: nodegroup_id.map(|s| s.to_string()),
236 graph_id: "test-graph".to_string(),
237 isrequired: false,
238 exportable: true,
239 sortorder: None,
240 config: HashMap::new(),
241 parentproperty: None,
242 ontologyclass: None,
243 description: None,
244 fieldname: None,
245 hascustomalias: false,
246 issearchable: false,
247 istopnode,
248 sourcebranchpublication_id: None,
249 source_identifier_id: None,
250 is_immutable: None,
251 })
252 }
253
254 fn setup_graph() -> (
262 Arc<StaticNode>,
263 HashMap<String, Arc<StaticNode>>,
264 HashMap<String, Vec<String>>,
265 HashMap<String, Arc<StaticNodegroup>>,
266 ) {
267 let root = make_node("root-id", "root", Some("root-id"), false, true);
268 let building = make_node("building-id", "building", Some("ng-building"), false, false);
269 let name = make_node("name-id", "name", Some("ng-building"), false, false);
270 let address = make_node("address-id", "address", Some("ng-address"), true, false);
271 let city = make_node("city-id", "city", Some("ng-address"), false, false);
272 let status = make_node("status-id", "status", Some("ng-status"), false, false);
273
274 let mut nodes = HashMap::new();
275 for n in [&root, &building, &name, &address, &city, &status] {
276 nodes.insert(n.nodeid.clone(), Arc::clone(n));
277 }
278
279 let mut edges: HashMap<String, Vec<String>> = HashMap::new();
280 edges.insert(
281 "root-id".into(),
282 vec!["building-id".into(), "status-id".into()],
283 );
284 edges.insert(
285 "building-id".into(),
286 vec!["name-id".into(), "address-id".into()],
287 );
288 edges.insert("address-id".into(), vec!["city-id".into()]);
289
290 let mut nodegroups = HashMap::new();
291 let make_ng = |id: &str, cardinality: Option<&str>| {
292 Arc::new(StaticNodegroup {
293 nodegroupid: id.to_string(),
294 cardinality: cardinality.map(|s| s.to_string()),
295 legacygroupid: None,
296 parentnodegroup_id: None,
297 grouping_node_id: None,
298 })
299 };
300 nodegroups.insert("ng-building".into(), make_ng("ng-building", Some("1")));
301 nodegroups.insert("ng-address".into(), make_ng("ng-address", Some("n")));
302 nodegroups.insert("ng-status".into(), make_ng("ng-status", Some("1")));
303
304 (root, nodes, edges, nodegroups)
305 }
306
307 #[test]
308 fn test_single_segment_path() {
309 let (root, nodes, edges, nodegroups) = setup_graph();
310 let result =
311 resolve_path_segments("building", &root, &nodes, &edges, Some(&nodegroups)).unwrap();
312
313 assert_eq!(result.target_node.alias.as_deref(), Some("building"));
314 assert_eq!(result.nodegroup_id, "ng-building");
315 assert_eq!(result.child_node_ids.len(), 2);
317 }
318
319 #[test]
320 fn test_multi_segment_path() {
321 let (root, nodes, edges, nodegroups) = setup_graph();
322 let result =
323 resolve_path_segments("building.name", &root, &nodes, &edges, Some(&nodegroups))
324 .unwrap();
325
326 assert_eq!(result.target_node.alias.as_deref(), Some("name"));
327 assert_eq!(result.nodegroup_id, "ng-building");
328 assert!(result.child_node_ids.is_empty());
329 assert!(result.is_single); }
331
332 #[test]
333 fn test_cross_nodegroup_path() {
334 let (root, nodes, edges, nodegroups) = setup_graph();
335 let result = resolve_path_segments(
336 "building.address.city",
337 &root,
338 &nodes,
339 &edges,
340 Some(&nodegroups),
341 )
342 .unwrap();
343
344 assert_eq!(result.target_node.alias.as_deref(), Some("city"));
345 assert_eq!(result.nodegroup_id, "ng-address");
346 }
347
348 #[test]
349 fn test_leading_dot_stripped() {
350 let (root, nodes, edges, nodegroups) = setup_graph();
351 let result =
352 resolve_path_segments(".building.name", &root, &nodes, &edges, Some(&nodegroups))
353 .unwrap();
354
355 assert_eq!(result.target_node.alias.as_deref(), Some("name"));
356 }
357
358 #[test]
359 fn test_collector_is_not_single() {
360 let (root, nodes, edges, nodegroups) = setup_graph();
361 let result =
362 resolve_path_segments("building.address", &root, &nodes, &edges, Some(&nodegroups))
363 .unwrap();
364
365 assert_eq!(result.target_node.alias.as_deref(), Some("address"));
366 assert!(!result.is_single);
368 }
369
370 #[test]
371 fn test_empty_path_error() {
372 let (root, nodes, edges, nodegroups) = setup_graph();
373 let err = resolve_path_segments("", &root, &nodes, &edges, Some(&nodegroups)).unwrap_err();
374 assert!(matches!(err, PathError::EmptyPath));
375 }
376
377 #[test]
378 fn test_only_dots_error() {
379 let (root, nodes, edges, nodegroups) = setup_graph();
380 let err =
381 resolve_path_segments("...", &root, &nodes, &edges, Some(&nodegroups)).unwrap_err();
382 assert!(matches!(err, PathError::EmptyPath));
383 }
384
385 #[test]
386 fn test_alias_not_found() {
387 let (root, nodes, edges, nodegroups) = setup_graph();
388 let err = resolve_path_segments(
389 "building.nonexistent",
390 &root,
391 &nodes,
392 &edges,
393 Some(&nodegroups),
394 )
395 .unwrap_err();
396 match err {
397 PathError::AliasNotFound {
398 segment,
399 parent_alias,
400 } => {
401 assert_eq!(segment, "nonexistent");
402 assert_eq!(parent_alias.as_deref(), Some("building"));
403 }
404 _ => panic!("Expected AliasNotFound, got {:?}", err),
405 }
406 }
407
408 #[test]
409 fn test_first_segment_not_found() {
410 let (root, nodes, edges, nodegroups) = setup_graph();
411 let err =
412 resolve_path_segments("unknown", &root, &nodes, &edges, Some(&nodegroups)).unwrap_err();
413 match err {
414 PathError::AliasNotFound {
415 segment,
416 parent_alias,
417 } => {
418 assert_eq!(segment, "unknown");
419 assert_eq!(parent_alias.as_deref(), Some("root"));
420 }
421 _ => panic!("Expected AliasNotFound, got {:?}", err),
422 }
423 }
424
425 #[test]
426 fn test_path_beyond_leaf_fails() {
427 let (root, nodes, edges, nodegroups) = setup_graph();
428 let err = resolve_path_segments(
430 "building.name.extra",
431 &root,
432 &nodes,
433 &edges,
434 Some(&nodegroups),
435 )
436 .unwrap_err();
437 assert!(matches!(err, PathError::AliasNotFound { .. }));
438 }
439
440 #[test]
441 fn test_no_nodegroup_error() {
442 let root = make_node("root-id", "root", Some("root-id"), false, true);
444 let child = Arc::new(StaticNode {
445 nodeid: "child-id".to_string(),
446 name: "child".to_string(),
447 alias: Some("child".to_string()),
448 datatype: "string".to_string(),
449 is_collector: false,
450 nodegroup_id: None, graph_id: "test-graph".to_string(),
452 isrequired: false,
453 exportable: true,
454 sortorder: None,
455 config: HashMap::new(),
456 parentproperty: None,
457 ontologyclass: None,
458 description: None,
459 fieldname: None,
460 hascustomalias: false,
461 issearchable: false,
462 istopnode: false,
463 sourcebranchpublication_id: None,
464 source_identifier_id: None,
465 is_immutable: None,
466 });
467
468 let mut nodes = HashMap::new();
469 nodes.insert("root-id".into(), Arc::clone(&root));
470 nodes.insert("child-id".into(), Arc::clone(&child));
471
472 let mut edges = HashMap::new();
473 edges.insert("root-id".into(), vec!["child-id".into()]);
474
475 let err = resolve_path_segments("child", &root, &nodes, &edges, None).unwrap_err();
476 assert!(matches!(err, PathError::NoNodegroup { .. }));
477 }
478
479 #[test]
484 fn test_underscore_skipped_on_collector() {
485 let (root, nodes, edges, nodegroups) = setup_graph();
486 let result = resolve_path_segments(
488 "building.address._.city",
489 &root,
490 &nodes,
491 &edges,
492 Some(&nodegroups),
493 )
494 .unwrap();
495 assert_eq!(result.target_node.alias.as_deref(), Some("city"));
496 assert_eq!(result.nodegroup_id, "ng-address");
497 }
498
499 #[test]
500 fn test_underscore_errors_on_non_collector() {
501 let (root, nodes, edges, nodegroups) = setup_graph();
502 let err =
504 resolve_path_segments("building._.name", &root, &nodes, &edges, Some(&nodegroups))
505 .unwrap_err();
506 assert!(matches!(err, PathError::UnderscoreOnNonCollector { .. }));
507 }
508
509 #[test]
510 fn test_star_skipped_on_multi_cardinality() {
511 let (root, nodes, edges, nodegroups) = setup_graph();
512 let result = resolve_path_segments(
514 "building.address.*.city",
515 &root,
516 &nodes,
517 &edges,
518 Some(&nodegroups),
519 )
520 .unwrap();
521 assert_eq!(result.target_node.alias.as_deref(), Some("city"));
522 }
523
524 #[test]
525 fn test_star_errors_on_single_cardinality() {
526 let (root, nodes, edges, nodegroups) = setup_graph();
527 let err =
529 resolve_path_segments("building.*.name", &root, &nodes, &edges, Some(&nodegroups))
530 .unwrap_err();
531 assert!(matches!(err, PathError::StarOnSingleCardinality { .. }));
532 }
533
534 #[test]
535 fn test_star_and_underscore_combined() {
536 let (root, nodes, edges, nodegroups) = setup_graph();
537 let result = resolve_path_segments(
539 "building.address.*._.city",
540 &root,
541 &nodes,
542 &edges,
543 Some(&nodegroups),
544 )
545 .unwrap();
546 assert_eq!(result.target_node.alias.as_deref(), Some("city"));
547 }
548}