1use actix_web::{HttpRequest, HttpResponse, web};
30use std::collections::{HashMap, HashSet};
31
32use haystack_core::data::{HCol, HDict, HGrid};
33use haystack_core::kinds::{HRef, Kind, Number};
34
35use crate::content;
36use crate::error::HaystackError;
37use crate::state::AppState;
38
39pub async fn handle_flow(
57 req: HttpRequest,
58 body: String,
59 state: web::Data<AppState>,
60) -> Result<HttpResponse, HaystackError> {
61 let content_type = req
62 .headers()
63 .get("Content-Type")
64 .and_then(|v| v.to_str().ok())
65 .unwrap_or("");
66 let accept = req
67 .headers()
68 .get("Accept")
69 .and_then(|v| v.to_str().ok())
70 .unwrap_or("");
71
72 let (filter, root, depth) = if body.trim().is_empty() {
74 (None, None, 10usize)
75 } else {
76 let rg = content::decode_request_grid(&body, content_type)
77 .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
78 let row = rg.row(0);
79 let filter = row.and_then(|r| match r.get("filter") {
80 Some(Kind::Str(s)) if !s.is_empty() => Some(s.clone()),
81 _ => None,
82 });
83 let root = row.and_then(|r| match r.get("root") {
84 Some(Kind::Ref(r)) => Some(r.val.clone()),
85 _ => None,
86 });
87 let depth = row
88 .and_then(|r| match r.get("depth") {
89 Some(Kind::Number(n)) => Some(n.val as usize),
90 _ => None,
91 })
92 .unwrap_or(10);
93 (filter, root, depth)
94 };
95
96 let entities: Vec<HDict> = match (&root, &filter) {
98 (Some(root_id), _) => state
99 .graph
100 .subtree(root_id, depth)
101 .into_iter()
102 .map(|(e, _)| e)
103 .collect(),
104 (None, Some(f)) => {
105 let f = if f == "*" {
106 return build_flow_all(&state, accept);
107 } else {
108 f
109 };
110 state
111 .graph
112 .read_all(f, 0)
113 .map_err(|e| HaystackError::bad_request(format!("filter error: {e}")))?
114 }
115 (None, None) => state.graph.all_entities(),
116 };
117
118 let entity_ids: HashSet<String> = entities
120 .iter()
121 .filter_map(|e| e.id().map(|r| r.val.clone()))
122 .collect();
123
124 let all_edges = state.graph.all_edges();
125 let edges: Vec<(String, String, String)> = all_edges
126 .into_iter()
127 .filter(|(src, _, tgt)| entity_ids.contains(src) && entity_ids.contains(tgt))
128 .collect();
129
130 build_flow_response(&entities, &edges, accept)
131}
132
133fn build_flow_all(state: &AppState, accept: &str) -> Result<HttpResponse, HaystackError> {
135 let entities = state.graph.all_entities();
136 let edges = state.graph.all_edges();
137 build_flow_response(&entities, &edges, accept)
138}
139
140fn build_flow_response(
142 entities: &[HDict],
143 edges: &[(String, String, String)],
144 accept: &str,
145) -> Result<HttpResponse, HaystackError> {
146 if entities.is_empty() {
147 let (encoded, ct) = content::encode_response_grid(&HGrid::new(), accept)
148 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
149 return Ok(HttpResponse::Ok().content_type(ct).body(encoded));
150 }
151
152 let mut type_depths: HashMap<String, usize> = HashMap::new();
154 for entity in entities {
155 let id = entity.id().map(|r| r.val.clone()).unwrap_or_default();
156 let depth = entity_depth(entity);
157 type_depths.insert(id, depth);
158 }
159
160 let mut depth_counts: HashMap<usize, usize> = HashMap::new();
162
163 let mut node_rows: Vec<HDict> = Vec::with_capacity(entities.len());
165 let mut all_tags: HashSet<String> = HashSet::new();
166
167 for entity in entities {
168 let id = entity.id().map(|r| r.val.clone()).unwrap_or_default();
169 let depth = type_depths.get(&id).copied().unwrap_or(0);
170 let x_idx = depth_counts.entry(depth).or_insert(0);
171 let pos_x = (*x_idx as f64) * 280.0;
172 let pos_y = (depth as f64) * 200.0;
173 *x_idx += 1;
174
175 let mut row = entity.clone();
176 row.set("nodeId", Kind::Str(id.clone()));
177 row.set("nodeType", Kind::Str(entity_type_name(entity)));
178 row.set("posX", Kind::Number(Number::unitless(pos_x)));
179 row.set("posY", Kind::Number(Number::unitless(pos_y)));
180
181 if let Some(parent) = find_parent_ref(entity) {
183 row.set("parentId", Kind::Str(parent));
184 }
185
186 for name in row.tag_names() {
187 all_tags.insert(name.to_string());
188 }
189 node_rows.push(row);
190 }
191
192 let edge_rows: Vec<HDict> = edges
194 .iter()
195 .map(|(src, tag, tgt)| {
196 let mut row = HDict::new();
197 row.set("edgeId", Kind::Str(format!("{src}:{tag}:{tgt}")));
198 row.set("source", Kind::Str(src.clone()));
199 row.set("target", Kind::Str(tgt.clone()));
200 row.set("label", Kind::Str(tag.clone()));
201 row
202 })
203 .collect();
204
205 let edge_cols = vec![
206 HCol::new("edgeId"),
207 HCol::new("source"),
208 HCol::new("target"),
209 HCol::new("label"),
210 ];
211 let edges_grid = HGrid::from_parts(HDict::new(), edge_cols, edge_rows);
212
213 let mut sorted_tags: Vec<String> = all_tags.into_iter().collect();
215 sorted_tags.sort();
216 let cols: Vec<HCol> = sorted_tags.iter().map(|n| HCol::new(n.as_str())).collect();
217
218 let mut meta = HDict::new();
220 let edges_zinc = haystack_core::codecs::codec_for("text/zinc")
221 .map(|c| c.encode_grid(&edges_grid).unwrap_or_default())
222 .unwrap_or_default();
223 meta.set("edges", Kind::Str(edges_zinc));
224
225 let nodes_grid = HGrid::from_parts(meta, cols, node_rows);
226
227 let (encoded, ct) = content::encode_response_grid(&nodes_grid, accept)
228 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
229
230 Ok(HttpResponse::Ok().content_type(ct).body(encoded))
231}
232
233pub async fn handle_edges(
244 req: HttpRequest,
245 body: String,
246 state: web::Data<AppState>,
247) -> Result<HttpResponse, HaystackError> {
248 let content_type = req
249 .headers()
250 .get("Content-Type")
251 .and_then(|v| v.to_str().ok())
252 .unwrap_or("");
253 let accept = req
254 .headers()
255 .get("Accept")
256 .and_then(|v| v.to_str().ok())
257 .unwrap_or("");
258
259 let (filter, ref_type) = if body.trim().is_empty() {
260 (None, None)
261 } else {
262 let rg = content::decode_request_grid(&body, content_type)
263 .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
264 let row = rg.row(0);
265 let filter = row.and_then(|r| match r.get("filter") {
266 Some(Kind::Str(s)) if !s.is_empty() => Some(s.clone()),
267 _ => None,
268 });
269 let ref_type = row.and_then(|r| match r.get("refType") {
270 Some(Kind::Str(s)) if !s.is_empty() => Some(s.clone()),
271 _ => None,
272 });
273 (filter, ref_type)
274 };
275
276 let all_edges = state.graph.all_edges();
278
279 let entity_ids: Option<HashSet<String>> = filter.map(|f| {
281 if f == "*" {
282 return state
283 .graph
284 .all_entities()
285 .into_iter()
286 .filter_map(|e| e.id().map(|r| r.val.clone()))
287 .collect();
288 }
289 state
290 .graph
291 .read_all(&f, 0)
292 .unwrap_or_default()
293 .into_iter()
294 .filter_map(|e| e.id().map(|r| r.val.clone()))
295 .collect()
296 });
297
298 let edges: Vec<(String, String, String)> = all_edges
299 .into_iter()
300 .filter(|(src, tag, _)| {
301 if let Some(ref ids) = entity_ids
302 && !ids.contains(src)
303 {
304 return false;
305 }
306 if let Some(ref rt) = ref_type
307 && tag != rt
308 {
309 return false;
310 }
311 true
312 })
313 .collect();
314
315 if edges.is_empty() {
316 let (encoded, ct) = content::encode_response_grid(&HGrid::new(), accept)
317 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
318 return Ok(HttpResponse::Ok().content_type(ct).body(encoded));
319 }
320
321 let cols = vec![
322 HCol::new("id"),
323 HCol::new("source"),
324 HCol::new("target"),
325 HCol::new("refTag"),
326 ];
327 let rows: Vec<HDict> = edges
328 .iter()
329 .map(|(src, tag, tgt)| {
330 let mut row = HDict::new();
331 row.set("id", Kind::Str(format!("{src}:{tag}:{tgt}")));
332 row.set("source", Kind::Ref(HRef::from_val(src)));
333 row.set("target", Kind::Ref(HRef::from_val(tgt)));
334 row.set("refTag", Kind::Str(tag.clone()));
335 row
336 })
337 .collect();
338
339 let grid = HGrid::from_parts(HDict::new(), cols, rows);
340 let (encoded, ct) = content::encode_response_grid(&grid, accept)
341 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
342
343 Ok(HttpResponse::Ok().content_type(ct).body(encoded))
344}
345
346pub async fn handle_tree(
361 req: HttpRequest,
362 body: String,
363 state: web::Data<AppState>,
364) -> Result<HttpResponse, HaystackError> {
365 let content_type = req
366 .headers()
367 .get("Content-Type")
368 .and_then(|v| v.to_str().ok())
369 .unwrap_or("");
370 let accept = req
371 .headers()
372 .get("Accept")
373 .and_then(|v| v.to_str().ok())
374 .unwrap_or("");
375
376 let rg = content::decode_request_grid(&body, content_type)
377 .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
378
379 let row = rg
380 .row(0)
381 .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
382
383 let root = match row.get("root") {
384 Some(Kind::Ref(r)) => r.val.clone(),
385 Some(Kind::Str(s)) => s.clone(),
386 _ => return Err(HaystackError::bad_request("'root' Ref is required")),
387 };
388
389 let max_depth = match row.get("maxDepth") {
390 Some(Kind::Number(n)) => n.val as usize,
391 _ => 10,
392 };
393
394 if !state.graph.contains(&root) {
396 return Err(HaystackError::not_found(format!(
397 "root entity not found: {root}"
398 )));
399 }
400
401 let subtree = state.graph.subtree(&root, max_depth);
402
403 if subtree.is_empty() {
404 let (encoded, ct) = content::encode_response_grid(&HGrid::new(), accept)
405 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
406 return Ok(HttpResponse::Ok().content_type(ct).body(encoded));
407 }
408
409 let parent_map = build_parent_map(&subtree, &state);
411
412 let mut all_tags: HashSet<String> = HashSet::new();
413 let mut rows: Vec<HDict> = Vec::with_capacity(subtree.len());
414
415 for (entity, depth) in &subtree {
416 let mut row = entity.clone();
417 row.set("depth", Kind::Number(Number::unitless(*depth as f64)));
418
419 let id = entity.id().map(|r| r.val.clone()).unwrap_or_default();
420 if let Some(parent) = parent_map.get(&id) {
421 row.set("parentId", Kind::Ref(HRef::from_val(parent)));
422 }
423 row.set("navId", Kind::Str(id));
424
425 for name in row.tag_names() {
426 all_tags.insert(name.to_string());
427 }
428 rows.push(row);
429 }
430
431 let mut sorted_tags: Vec<String> = all_tags.into_iter().collect();
432 sorted_tags.sort();
433 let cols: Vec<HCol> = sorted_tags.iter().map(|n| HCol::new(n.as_str())).collect();
434 let grid = HGrid::from_parts(HDict::new(), cols, rows);
435
436 let (encoded, ct) = content::encode_response_grid(&grid, accept)
437 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
438
439 Ok(HttpResponse::Ok().content_type(ct).body(encoded))
440}
441
442pub async fn handle_neighbors(
458 req: HttpRequest,
459 body: String,
460 state: web::Data<AppState>,
461) -> Result<HttpResponse, HaystackError> {
462 let content_type = req
463 .headers()
464 .get("Content-Type")
465 .and_then(|v| v.to_str().ok())
466 .unwrap_or("");
467 let accept = req
468 .headers()
469 .get("Accept")
470 .and_then(|v| v.to_str().ok())
471 .unwrap_or("");
472
473 let rg = content::decode_request_grid(&body, content_type)
474 .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
475
476 let row = rg
477 .row(0)
478 .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
479
480 let id = match row.get("id") {
481 Some(Kind::Ref(r)) => r.val.clone(),
482 Some(Kind::Str(s)) => s.clone(),
483 _ => return Err(HaystackError::bad_request("'id' Ref is required")),
484 };
485
486 let hops = match row.get("hops") {
487 Some(Kind::Number(n)) => n.val as usize,
488 _ => 1,
489 };
490
491 let ref_types_str: Option<String> = match row.get("refTypes") {
492 Some(Kind::Str(s)) if !s.is_empty() => Some(s.clone()),
493 _ => None,
494 };
495
496 if !state.graph.contains(&id) {
497 return Err(HaystackError::not_found(format!("entity not found: {id}")));
498 }
499
500 let ref_types_vec: Option<Vec<String>> =
501 ref_types_str.map(|s| s.split(',').map(|t| t.trim().to_string()).collect());
502 let ref_types_refs: Option<Vec<&str>> = ref_types_vec
503 .as_ref()
504 .map(|v| v.iter().map(|s| s.as_str()).collect());
505
506 let (entities, edges) = state.graph.neighbors(&id, hops, ref_types_refs.as_deref());
507
508 build_flow_response(&entities, &edges, accept)
509}
510
511pub async fn handle_path(
526 req: HttpRequest,
527 body: String,
528 state: web::Data<AppState>,
529) -> Result<HttpResponse, HaystackError> {
530 let content_type = req
531 .headers()
532 .get("Content-Type")
533 .and_then(|v| v.to_str().ok())
534 .unwrap_or("");
535 let accept = req
536 .headers()
537 .get("Accept")
538 .and_then(|v| v.to_str().ok())
539 .unwrap_or("");
540
541 let rg = content::decode_request_grid(&body, content_type)
542 .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
543
544 let row = rg
545 .row(0)
546 .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
547
548 let from = match row.get("from") {
549 Some(Kind::Ref(r)) => r.val.clone(),
550 Some(Kind::Str(s)) => s.clone(),
551 _ => return Err(HaystackError::bad_request("'from' Ref is required")),
552 };
553
554 let to = match row.get("to") {
555 Some(Kind::Ref(r)) => r.val.clone(),
556 Some(Kind::Str(s)) => s.clone(),
557 _ => return Err(HaystackError::bad_request("'to' Ref is required")),
558 };
559
560 let path = state.graph.shortest_path(&from, &to);
561
562 if path.is_empty() {
563 let (encoded, ct) = content::encode_response_grid(&HGrid::new(), accept)
564 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
565 return Ok(HttpResponse::Ok().content_type(ct).body(encoded));
566 }
567
568 let mut all_tags: HashSet<String> = HashSet::new();
569 let mut rows: Vec<HDict> = Vec::with_capacity(path.len());
570
571 for (idx, ref_val) in path.iter().enumerate() {
572 let mut row = state.graph.get(ref_val).unwrap_or_else(|| {
573 let mut stub = HDict::new();
574 stub.set("id", Kind::Ref(HRef::from_val(ref_val)));
575 stub
576 });
577 row.set("pathIndex", Kind::Number(Number::unitless(idx as f64)));
578 for name in row.tag_names() {
579 all_tags.insert(name.to_string());
580 }
581 rows.push(row);
582 }
583
584 let mut sorted_tags: Vec<String> = all_tags.into_iter().collect();
585 sorted_tags.sort();
586 let cols: Vec<HCol> = sorted_tags.iter().map(|n| HCol::new(n.as_str())).collect();
587 let grid = HGrid::from_parts(HDict::new(), cols, rows);
588
589 let (encoded, ct) = content::encode_response_grid(&grid, accept)
590 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
591
592 Ok(HttpResponse::Ok().content_type(ct).body(encoded))
593}
594
595pub async fn handle_stats(state: web::Data<AppState>) -> Result<HttpResponse, HaystackError> {
607 let entities = state.graph.all_entities();
608 let edges = state.graph.all_edges();
609
610 let mut type_counts: HashMap<String, usize> = HashMap::new();
612 for entity in &entities {
613 let etype = entity_type_name(entity);
614 *type_counts.entry(etype).or_insert(0) += 1;
615 }
616
617 let mut ref_counts: HashMap<String, usize> = HashMap::new();
619 for (_, tag, _) in &edges {
620 *ref_counts.entry(tag.clone()).or_insert(0) += 1;
621 }
622
623 let component_count = count_components(&entities, &edges);
625
626 let cols = vec![HCol::new("metric"), HCol::new("value"), HCol::new("detail")];
627
628 let mut rows: Vec<HDict> = Vec::new();
629
630 let mut row = HDict::new();
632 row.set("metric", Kind::Str("totalEntities".into()));
633 row.set(
634 "value",
635 Kind::Number(Number::unitless(entities.len() as f64)),
636 );
637 rows.push(row);
638
639 let mut row = HDict::new();
641 row.set("metric", Kind::Str("totalEdges".into()));
642 row.set("value", Kind::Number(Number::unitless(edges.len() as f64)));
643 rows.push(row);
644
645 let mut row = HDict::new();
647 row.set("metric", Kind::Str("connectedComponents".into()));
648 row.set(
649 "value",
650 Kind::Number(Number::unitless(component_count as f64)),
651 );
652 rows.push(row);
653
654 let mut type_entries: Vec<_> = type_counts.into_iter().collect();
656 type_entries.sort_by(|a, b| b.1.cmp(&a.1));
657 for (etype, count) in type_entries {
658 let mut row = HDict::new();
659 row.set("metric", Kind::Str("entityType".into()));
660 row.set("value", Kind::Number(Number::unitless(count as f64)));
661 row.set("detail", Kind::Str(etype));
662 rows.push(row);
663 }
664
665 let mut ref_entries: Vec<_> = ref_counts.into_iter().collect();
667 ref_entries.sort_by(|a, b| b.1.cmp(&a.1));
668 for (rtype, count) in ref_entries {
669 let mut row = HDict::new();
670 row.set("metric", Kind::Str("refType".into()));
671 row.set("value", Kind::Number(Number::unitless(count as f64)));
672 row.set("detail", Kind::Str(rtype));
673 rows.push(row);
674 }
675
676 let grid = HGrid::from_parts(HDict::new(), cols, rows);
677
678 let accept = "text/zinc";
680 let (encoded, ct) = content::encode_response_grid(&grid, accept)
681 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
682
683 Ok(HttpResponse::Ok().content_type(ct).body(encoded))
684}
685
686fn entity_type_name(entity: &HDict) -> String {
690 for tag in &[
692 "site", "space", "floor", "wing", "equip", "point", "device", "conn", "weather",
693 ] {
694 if entity.has(tag) {
695 return (*tag).to_string();
696 }
697 }
698 "entity".to_string()
699}
700
701fn entity_depth(entity: &HDict) -> usize {
703 if entity.has("site") {
704 0
705 } else if entity.has("space") || entity.has("floor") || entity.has("wing") {
706 1
707 } else if entity.has("equip") {
708 2
709 } else if entity.has("point") {
710 3
711 } else {
712 1 }
714}
715
716fn find_parent_ref(entity: &HDict) -> Option<String> {
718 for tag in &["equipRef", "spaceRef", "siteRef"] {
720 if let Some(Kind::Ref(r)) = entity.get(tag) {
721 return Some(r.val.clone());
722 }
723 }
724 None
725}
726
727fn build_parent_map(subtree: &[(HDict, usize)], state: &AppState) -> HashMap<String, String> {
729 let mut parent_map = HashMap::new();
730 let subtree_ids: HashSet<String> = subtree
731 .iter()
732 .filter_map(|(e, _)| e.id().map(|r| r.val.clone()))
733 .collect();
734
735 for (entity, _) in subtree {
736 let id = match entity.id() {
737 Some(r) => r.val.clone(),
738 None => continue,
739 };
740 if let Some(parent) = find_parent_ref(entity)
741 && subtree_ids.contains(&parent)
742 {
743 parent_map.insert(id, parent);
744 }
745 }
746 let _ = state; parent_map
748}
749
750fn count_components(entities: &[HDict], edges: &[(String, String, String)]) -> usize {
752 if entities.is_empty() {
753 return 0;
754 }
755
756 let mut id_to_idx: HashMap<String, usize> = HashMap::new();
757 for (i, entity) in entities.iter().enumerate() {
758 if let Some(r) = entity.id() {
759 id_to_idx.insert(r.val.clone(), i);
760 }
761 }
762
763 let n = entities.len();
764 let mut parent: Vec<usize> = (0..n).collect();
765 let mut rank: Vec<usize> = vec![0; n];
766
767 fn find(parent: &mut [usize], x: usize) -> usize {
768 if parent[x] != x {
769 parent[x] = find(parent, parent[x]);
770 }
771 parent[x]
772 }
773
774 fn union(parent: &mut [usize], rank: &mut [usize], x: usize, y: usize) {
775 let rx = find(parent, x);
776 let ry = find(parent, y);
777 if rx != ry {
778 if rank[rx] < rank[ry] {
779 parent[rx] = ry;
780 } else if rank[rx] > rank[ry] {
781 parent[ry] = rx;
782 } else {
783 parent[ry] = rx;
784 rank[rx] += 1;
785 }
786 }
787 }
788
789 for (src, _, tgt) in edges {
790 if let (Some(&si), Some(&ti)) = (id_to_idx.get(src), id_to_idx.get(tgt)) {
791 union(&mut parent, &mut rank, si, ti);
792 }
793 }
794
795 let mut roots: HashSet<usize> = HashSet::new();
796 for i in 0..n {
797 roots.insert(find(&mut parent, i));
798 }
799 roots.len()
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805
806 fn make_site(id: &str) -> HDict {
807 let mut d = HDict::new();
808 d.set("id", Kind::Ref(HRef::from_val(id)));
809 d.set("site", Kind::Marker);
810 d.set("dis", Kind::Str(format!("Site {id}")));
811 d
812 }
813
814 fn make_equip(id: &str, site_ref: &str) -> HDict {
815 let mut d = HDict::new();
816 d.set("id", Kind::Ref(HRef::from_val(id)));
817 d.set("equip", Kind::Marker);
818 d.set("siteRef", Kind::Ref(HRef::from_val(site_ref)));
819 d.set("dis", Kind::Str(format!("Equip {id}")));
820 d
821 }
822
823 fn make_point(id: &str, equip_ref: &str, site_ref: &str) -> HDict {
824 let mut d = HDict::new();
825 d.set("id", Kind::Ref(HRef::from_val(id)));
826 d.set("point", Kind::Marker);
827 d.set("equipRef", Kind::Ref(HRef::from_val(equip_ref)));
828 d.set("siteRef", Kind::Ref(HRef::from_val(site_ref)));
829 d.set("dis", Kind::Str(format!("Point {id}")));
830 d
831 }
832
833 #[test]
834 fn entity_type_detection() {
835 assert_eq!(entity_type_name(&make_site("s1")), "site");
836 assert_eq!(entity_type_name(&make_equip("e1", "s1")), "equip");
837 assert_eq!(entity_type_name(&make_point("p1", "e1", "s1")), "point");
838
839 let empty = HDict::new();
840 assert_eq!(entity_type_name(&empty), "entity");
841 }
842
843 #[test]
844 fn entity_depth_classification() {
845 assert_eq!(entity_depth(&make_site("s1")), 0);
846 assert_eq!(entity_depth(&make_equip("e1", "s1")), 2);
847 assert_eq!(entity_depth(&make_point("p1", "e1", "s1")), 3);
848 }
849
850 #[test]
851 fn parent_ref_detection() {
852 let equip = make_equip("e1", "s1");
853 assert_eq!(find_parent_ref(&equip), Some("s1".to_string()));
854
855 let point = make_point("p1", "e1", "s1");
856 assert_eq!(find_parent_ref(&point), Some("e1".to_string()));
858
859 let site = make_site("s1");
860 assert_eq!(find_parent_ref(&site), None);
861 }
862
863 #[test]
864 fn connected_components_single() {
865 let entities = vec![make_site("s1"), make_equip("e1", "s1")];
866 let edges = vec![("e1".into(), "siteRef".into(), "s1".into())];
867 assert_eq!(count_components(&entities, &edges), 1);
868 }
869
870 #[test]
871 fn connected_components_disjoint() {
872 let entities = vec![make_site("s1"), make_site("s2")];
873 let edges: Vec<(String, String, String)> = vec![];
874 assert_eq!(count_components(&entities, &edges), 2);
875 }
876
877 #[test]
878 fn connected_components_empty() {
879 let entities: Vec<HDict> = vec![];
880 let edges: Vec<(String, String, String)> = vec![];
881 assert_eq!(count_components(&entities, &edges), 0);
882 }
883}