Skip to main content

haystack_server/ops/
defs.rs

1//! The `defs` and `libs` ops — query the definition namespace.
2//!
3//! # defs (`POST /api/defs`)
4//!
5//! Returns definitions from the ontology namespace, optionally filtered
6//! by a substring match on the def symbol.
7//!
8//! ## Request Grid Columns
9//!
10//! | Column   | Kind | Description                                    |
11//! |----------|------|------------------------------------------------|
12//! | `filter` | Str  | *(optional)* Substring to match against def symbols |
13//!
14//! An empty body returns all definitions.
15//!
16//! ## Response Grid Columns
17//!
18//! | Column | Kind   | Description              |
19//! |--------|--------|--------------------------|
20//! | `def`  | Symbol | Definition symbol        |
21//! | `lib`  | Symbol | Owning library name      |
22//! | `doc`  | Str    | Documentation string     |
23//!
24//! Rows are sorted by `def` symbol.
25//!
26//! # libs (`POST /api/libs`)
27//!
28//! Returns all loaded ontology libraries.
29//!
30//! ## Response Grid Columns
31//!
32//! | Column    | Kind | Description       |
33//! |-----------|------|-------------------|
34//! | `name`    | Str  | Library name      |
35//! | `version` | Str  | Library version   |
36//!
37//! Rows are sorted by `name`.
38//!
39//! # Errors
40//!
41//! - **400 Bad Request** — request grid decode failure.
42//! - **500 Internal Server Error** — encoding error.
43
44use actix_web::{HttpRequest, HttpResponse, web};
45
46use haystack_core::data::{HCol, HDict, HGrid};
47use haystack_core::kinds::Kind;
48
49use crate::content;
50use crate::error::HaystackError;
51use crate::state::AppState;
52
53/// POST /api/defs
54///
55/// Request may have a `filter` column with a filter string.
56/// Returns matching def records as a grid.
57pub async fn handle(
58    req: HttpRequest,
59    body: String,
60    state: web::Data<AppState>,
61) -> Result<HttpResponse, HaystackError> {
62    let content_type = req
63        .headers()
64        .get("Content-Type")
65        .and_then(|v| v.to_str().ok())
66        .unwrap_or("");
67    let accept = req
68        .headers()
69        .get("Accept")
70        .and_then(|v| v.to_str().ok())
71        .unwrap_or("");
72
73    let ns = state.namespace.read();
74
75    // Parse optional filter from request
76    let filter: Option<String> = if body.trim().is_empty() {
77        None
78    } else {
79        let request_grid = content::decode_request_grid(&body, content_type)
80            .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
81
82        request_grid.row(0).and_then(|row| match row.get("filter") {
83            Some(Kind::Str(s)) if !s.is_empty() => Some(s.clone()),
84            _ => None,
85        })
86    };
87
88    // Build def grid
89    let cols = vec![HCol::new("def"), HCol::new("lib"), HCol::new("doc")];
90
91    let defs = ns.defs();
92    let mut rows: Vec<HDict> = Vec::new();
93
94    for (symbol, def) in defs {
95        // If a filter is provided, only include defs whose symbol contains the filter
96        if let Some(ref f) = filter
97            && !symbol.contains(f.as_str())
98        {
99            continue;
100        }
101
102        let mut row = HDict::new();
103        row.set(
104            "def",
105            Kind::Symbol(haystack_core::kinds::Symbol::new(symbol)),
106        );
107        row.set(
108            "lib",
109            Kind::Symbol(haystack_core::kinds::Symbol::new(&def.lib)),
110        );
111        row.set("doc", Kind::Str(def.doc.clone()));
112        rows.push(row);
113    }
114
115    // Sort by symbol for deterministic output
116    rows.sort_by(|a, b| {
117        let a_name = match a.get("def") {
118            Some(Kind::Symbol(s)) => s.val(),
119            _ => "",
120        };
121        let b_name = match b.get("def") {
122            Some(Kind::Symbol(s)) => s.val(),
123            _ => "",
124        };
125        a_name.cmp(b_name)
126    });
127
128    let grid = HGrid::from_parts(HDict::new(), cols, rows);
129    let (encoded, ct) = content::encode_response_grid(&grid, accept)
130        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
131
132    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
133}
134
135/// POST /api/libs — returns a grid of library names.
136pub async fn handle_libs(
137    req: HttpRequest,
138    state: web::Data<AppState>,
139) -> Result<HttpResponse, HaystackError> {
140    let accept = req
141        .headers()
142        .get("Accept")
143        .and_then(|v| v.to_str().ok())
144        .unwrap_or("");
145
146    let ns = state.namespace.read();
147
148    let cols = vec![HCol::new("name"), HCol::new("version")];
149    let libs = ns.libs();
150    let mut rows: Vec<HDict> = libs
151        .values()
152        .map(|lib| {
153            let mut row = HDict::new();
154            row.set("name", Kind::Str(lib.name.clone()));
155            row.set("version", Kind::Str(lib.version.clone()));
156            row
157        })
158        .collect();
159
160    // Sort by name for deterministic output
161    rows.sort_by(|a, b| {
162        let a_name = match a.get("name") {
163            Some(Kind::Str(s)) => s.as_str(),
164            _ => "",
165        };
166        let b_name = match b.get("name") {
167            Some(Kind::Str(s)) => s.as_str(),
168            _ => "",
169        };
170        a_name.cmp(b_name)
171    });
172
173    let grid = HGrid::from_parts(HDict::new(), cols, rows);
174    let (encoded, ct) = content::encode_response_grid(&grid, accept)
175        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
176
177    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
178}