Skip to main content

haystack_server/ops/
libs.rs

1//! Library and spec management endpoints.
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/specs — list specs, optionally filtered by library.
15pub async fn handle_specs(
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    let ns = state.namespace.read();
30
31    let lib_filter: Option<String> = if body.trim().is_empty() {
32        None
33    } else {
34        let grid = content::decode_request_grid(&body, content_type)
35            .map_err(|e| HaystackError::bad_request(format!("decode error: {e}")))?;
36        grid.row(0).and_then(|row| match row.get("lib") {
37            Some(Kind::Str(s)) if !s.is_empty() => Some(s.clone()),
38            _ => None,
39        })
40    };
41
42    let specs = ns.specs(lib_filter.as_deref());
43    let cols = vec![
44        HCol::new("qname"),
45        HCol::new("name"),
46        HCol::new("lib"),
47        HCol::new("base"),
48        HCol::new("doc"),
49        HCol::new("abstract"),
50    ];
51
52    let mut rows: Vec<HDict> = specs
53        .iter()
54        .map(|spec| {
55            let mut row = HDict::new();
56            row.set("qname", Kind::Str(spec.qname.clone()));
57            row.set("name", Kind::Str(spec.name.clone()));
58            row.set("lib", Kind::Str(spec.lib.clone()));
59            if let Some(ref base) = spec.base {
60                row.set("base", Kind::Str(base.clone()));
61            }
62            row.set("doc", Kind::Str(spec.doc.clone()));
63            if spec.is_abstract {
64                row.set("abstract", Kind::Marker);
65            }
66            row
67        })
68        .collect();
69
70    rows.sort_by(|a, b| {
71        let a_name = match a.get("qname") {
72            Some(Kind::Str(s)) => s.as_str(),
73            _ => "",
74        };
75        let b_name = match b.get("qname") {
76            Some(Kind::Str(s)) => s.as_str(),
77            _ => "",
78        };
79        a_name.cmp(b_name)
80    });
81
82    let grid = HGrid::from_parts(HDict::new(), cols, rows);
83    let (encoded, ct) = content::encode_response_grid(&grid, accept)
84        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
85    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
86}
87
88/// POST /api/spec — get a single spec by qualified name.
89pub async fn handle_spec(
90    State(state): State<SharedState>,
91    headers: HeaderMap,
92    body: String,
93) -> Result<Response, HaystackError> {
94    let content_type = headers
95        .get("Content-Type")
96        .and_then(|v| v.to_str().ok())
97        .unwrap_or("");
98    let accept = headers
99        .get("Accept")
100        .and_then(|v| v.to_str().ok())
101        .unwrap_or("");
102
103    let grid = content::decode_request_grid(&body, content_type)
104        .map_err(|e| HaystackError::bad_request(format!("decode error: {e}")))?;
105    let row = grid
106        .row(0)
107        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
108    let qname = match row.get("qname") {
109        Some(Kind::Str(s)) => s.clone(),
110        _ => return Err(HaystackError::bad_request("qname column required")),
111    };
112
113    let ns = state.namespace.read();
114    let spec = ns
115        .get_spec(&qname)
116        .ok_or_else(|| HaystackError::bad_request(format!("spec '{}' not found", qname)))?;
117
118    let cols = vec![
119        HCol::new("qname"),
120        HCol::new("name"),
121        HCol::new("lib"),
122        HCol::new("base"),
123        HCol::new("doc"),
124        HCol::new("abstract"),
125        HCol::new("slots"),
126    ];
127
128    let mut result = HDict::new();
129    result.set("qname", Kind::Str(spec.qname.clone()));
130    result.set("name", Kind::Str(spec.name.clone()));
131    result.set("lib", Kind::Str(spec.lib.clone()));
132    if let Some(ref base) = spec.base {
133        result.set("base", Kind::Str(base.clone()));
134    }
135    result.set("doc", Kind::Str(spec.doc.clone()));
136    if spec.is_abstract {
137        result.set("abstract", Kind::Marker);
138    }
139    let slot_names: Vec<String> = spec.slots.iter().map(|s| s.name.clone()).collect();
140    result.set("slots", Kind::Str(slot_names.join(",")));
141
142    let grid = HGrid::from_parts(HDict::new(), cols, vec![result]);
143    let (encoded, ct) = content::encode_response_grid(&grid, accept)
144        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
145    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
146}
147
148/// POST /api/loadLib — load a library from Xeto source text.
149pub async fn handle_load_lib(
150    State(state): State<SharedState>,
151    headers: HeaderMap,
152    body: String,
153) -> Result<Response, HaystackError> {
154    let content_type = headers
155        .get("Content-Type")
156        .and_then(|v| v.to_str().ok())
157        .unwrap_or("");
158    let accept = headers
159        .get("Accept")
160        .and_then(|v| v.to_str().ok())
161        .unwrap_or("");
162
163    let grid = content::decode_request_grid(&body, content_type)
164        .map_err(|e| HaystackError::bad_request(format!("decode error: {e}")))?;
165    let row = grid
166        .row(0)
167        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
168    let name = match row.get("name") {
169        Some(Kind::Str(s)) => s.clone(),
170        _ => return Err(HaystackError::bad_request("name column required")),
171    };
172    let source = match row.get("source") {
173        Some(Kind::Str(s)) => s.clone(),
174        _ => return Err(HaystackError::bad_request("source column required")),
175    };
176
177    let mut ns = state.namespace.write();
178    let qnames = ns
179        .load_xeto_str(&source, &name)
180        .map_err(|e| HaystackError::bad_request(format!("load error: {e}")))?;
181
182    let cols = vec![HCol::new("loaded"), HCol::new("specs")];
183    let mut result = HDict::new();
184    result.set("loaded", Kind::Str(name));
185    result.set("specs", Kind::Str(qnames.join(",")));
186    let grid = HGrid::from_parts(HDict::new(), cols, vec![result]);
187    let (encoded, ct) = content::encode_response_grid(&grid, accept)
188        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
189    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
190}
191
192/// POST /api/unloadLib — unload a library by name.
193pub async fn handle_unload_lib(
194    State(state): State<SharedState>,
195    headers: HeaderMap,
196    body: String,
197) -> Result<Response, HaystackError> {
198    let content_type = headers
199        .get("Content-Type")
200        .and_then(|v| v.to_str().ok())
201        .unwrap_or("");
202    let accept = headers
203        .get("Accept")
204        .and_then(|v| v.to_str().ok())
205        .unwrap_or("");
206
207    let grid = content::decode_request_grid(&body, content_type)
208        .map_err(|e| HaystackError::bad_request(format!("decode error: {e}")))?;
209    let row = grid
210        .row(0)
211        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
212    let name = match row.get("name") {
213        Some(Kind::Str(s)) => s.clone(),
214        _ => return Err(HaystackError::bad_request("name column required")),
215    };
216
217    let mut ns = state.namespace.write();
218    ns.unload_lib(&name).map_err(HaystackError::bad_request)?;
219
220    let cols = vec![HCol::new("unloaded")];
221    let mut result = HDict::new();
222    result.set("unloaded", Kind::Str(name));
223    let grid = HGrid::from_parts(HDict::new(), cols, vec![result]);
224    let (encoded, ct) = content::encode_response_grid(&grid, accept)
225        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
226    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
227}
228
229/// POST /api/exportLib — export a library to Xeto source text.
230pub async fn handle_export_lib(
231    State(state): State<SharedState>,
232    headers: HeaderMap,
233    body: String,
234) -> Result<Response, HaystackError> {
235    let content_type = headers
236        .get("Content-Type")
237        .and_then(|v| v.to_str().ok())
238        .unwrap_or("");
239    let accept = headers
240        .get("Accept")
241        .and_then(|v| v.to_str().ok())
242        .unwrap_or("");
243
244    let grid = content::decode_request_grid(&body, content_type)
245        .map_err(|e| HaystackError::bad_request(format!("decode error: {e}")))?;
246    let row = grid
247        .row(0)
248        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
249    let name = match row.get("name") {
250        Some(Kind::Str(s)) => s.clone(),
251        _ => return Err(HaystackError::bad_request("name column required")),
252    };
253
254    let ns = state.namespace.read();
255    let xeto_text = ns
256        .export_lib_xeto(&name)
257        .map_err(HaystackError::bad_request)?;
258
259    let cols = vec![HCol::new("name"), HCol::new("source")];
260    let mut result = HDict::new();
261    result.set("name", Kind::Str(name));
262    result.set("source", Kind::Str(xeto_text));
263    let grid = HGrid::from_parts(HDict::new(), cols, vec![result]);
264    let (encoded, ct) = content::encode_response_grid(&grid, accept)
265        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
266    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
267}
268
269/// POST /api/validate — validate entities against the ontology.
270pub async fn handle_validate(
271    State(state): State<SharedState>,
272    headers: HeaderMap,
273    body: String,
274) -> Result<Response, HaystackError> {
275    let content_type = headers
276        .get("Content-Type")
277        .and_then(|v| v.to_str().ok())
278        .unwrap_or("");
279    let accept = headers
280        .get("Accept")
281        .and_then(|v| v.to_str().ok())
282        .unwrap_or("");
283
284    let grid = content::decode_request_grid(&body, content_type)
285        .map_err(|e| HaystackError::bad_request(format!("decode error: {e}")))?;
286
287    let ns = state.namespace.read();
288
289    let cols = vec![
290        HCol::new("entity"),
291        HCol::new("issueType"),
292        HCol::new("detail"),
293    ];
294    let mut rows: Vec<HDict> = Vec::new();
295
296    for entity in &grid.rows {
297        let issues = ns.validate_entity(entity);
298        for issue in issues {
299            let mut row = HDict::new();
300            if let Some(ref e) = issue.entity {
301                row.set("entity", Kind::Str(e.clone()));
302            }
303            row.set("issueType", Kind::Str(issue.issue_type));
304            row.set("detail", Kind::Str(issue.detail));
305            rows.push(row);
306        }
307    }
308
309    let grid = HGrid::from_parts(HDict::new(), cols, rows);
310    let (encoded, ct) = content::encode_response_grid(&grid, accept)
311        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
312    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
313}