Skip to main content

haystack_server/ops/
libs.rs

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