Skip to main content

haystack_server/ops/
nav.rs

1//! The `nav` op — navigate a project for entity discovery.
2
3use actix_web::{HttpRequest, HttpResponse, web};
4
5use haystack_core::data::{HCol, HDict, HGrid};
6use haystack_core::kinds::Kind;
7
8use crate::content;
9use crate::error::HaystackError;
10use crate::state::AppState;
11
12/// POST /api/nav
13///
14/// Request may have a `navId` column:
15/// - No navId or empty: return top-level sites
16/// - navId is a site ref: return children (equips/spaces with siteRef)
17/// - navId is an equip ref: return children (points with equipRef)
18pub async fn handle(
19    req: HttpRequest,
20    body: String,
21    state: web::Data<AppState>,
22) -> Result<HttpResponse, HaystackError> {
23    let content_type = req
24        .headers()
25        .get("Content-Type")
26        .and_then(|v| v.to_str().ok())
27        .unwrap_or("");
28    let accept = req
29        .headers()
30        .get("Accept")
31        .and_then(|v| v.to_str().ok())
32        .unwrap_or("");
33
34    // Try to decode request grid; if body is empty, treat as no navId
35    let nav_id: Option<String> = if body.trim().is_empty() {
36        None
37    } else {
38        let request_grid = content::decode_request_grid(&body, content_type)
39            .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
40
41        request_grid.row(0).and_then(|row| match row.get("navId") {
42            Some(Kind::Str(s)) if !s.is_empty() => Some(s.clone()),
43            Some(Kind::Ref(r)) => Some(r.val.clone()),
44            _ => None,
45        })
46    };
47
48    let result_grid = match nav_id {
49        None => {
50            // Return top-level sites
51            let sites = state
52                .graph
53                .read_all("site", 0)
54                .map_err(|e| HaystackError::internal(format!("graph error: {e}")))?;
55
56            build_nav_grid(sites)
57        }
58        Some(ref parent_id) => {
59            // Check if the parent entity exists
60            let parent = state.graph.get(parent_id);
61            if parent.is_none() {
62                return Err(HaystackError::not_found(format!(
63                    "entity not found: {parent_id}"
64                )));
65            }
66
67            // Find children that reference this parent
68            let child_refs = state.graph.refs_to(parent_id, None);
69            let mut children = Vec::new();
70            for ref_val in child_refs {
71                if let Some(entity) = state.graph.get(&ref_val) {
72                    children.push(entity);
73                }
74            }
75            build_nav_grid(children)
76        }
77    };
78
79    let (encoded, ct) = content::encode_response_grid(&result_grid, accept)
80        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
81
82    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
83}
84
85/// Build a navigation grid from a list of entity dicts.
86fn build_nav_grid(entities: Vec<HDict>) -> HGrid {
87    if entities.is_empty() {
88        return HGrid::new();
89    }
90
91    let cols = vec![HCol::new("id"), HCol::new("dis"), HCol::new("navId")];
92    let rows: Vec<HDict> = entities
93        .into_iter()
94        .map(|entity| {
95            let mut row = HDict::new();
96            if let Some(id_ref) = entity.id() {
97                row.set("id", Kind::Ref(id_ref.clone()));
98                row.set("navId", Kind::Str(id_ref.val.clone()));
99            }
100            if let Some(dis) = entity.dis() {
101                row.set("dis", Kind::Str(dis.to_string()));
102            }
103            row
104        })
105        .collect();
106
107    HGrid::from_parts(HDict::new(), cols, rows)
108}