Skip to main content

haystack_server/ops/
nav.rs

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