Skip to main content

haystack_server/ops/
nav.rs

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