Skip to main content

haystack_server/
error.rs

1//! Haystack error grids and Axum error integration.
2
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use std::fmt;
6
7use haystack_core::data::{HCol, HDict, HGrid};
8use haystack_core::kinds::Kind;
9
10use crate::content;
11
12/// Build a Haystack error grid with the given message.
13///
14/// An error grid has `err` marker and `dis` string in its metadata,
15/// with no columns and no rows.
16pub fn error_grid(message: &str) -> HGrid {
17    let mut meta = HDict::new();
18    meta.set("err", Kind::Marker);
19    meta.set("dis", Kind::Str(message.to_string()));
20    HGrid::from_parts(meta, vec![], vec![])
21}
22
23/// Haystack-specific error type that renders as an error grid in responses.
24#[derive(Debug)]
25pub struct HaystackError {
26    pub message: String,
27    pub status: StatusCode,
28}
29
30impl HaystackError {
31    pub fn new(message: impl Into<String>, status: StatusCode) -> Self {
32        Self {
33            message: message.into(),
34            status,
35        }
36    }
37
38    pub fn bad_request(message: impl Into<String>) -> Self {
39        Self::new(message, StatusCode::BAD_REQUEST)
40    }
41
42    pub fn not_found(message: impl Into<String>) -> Self {
43        Self::new(message, StatusCode::NOT_FOUND)
44    }
45
46    pub fn internal(message: impl Into<String>) -> Self {
47        Self::new(message, StatusCode::INTERNAL_SERVER_ERROR)
48    }
49
50    pub fn forbidden(message: impl Into<String>) -> Self {
51        Self::new(message, StatusCode::FORBIDDEN)
52    }
53}
54
55impl fmt::Display for HaystackError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(f, "{}", self.message)
58    }
59}
60
61// Note: Error responses are always encoded as Zinc (the default Haystack format)
62// regardless of the client's Accept header, because IntoResponse does not have
63// access to the original request. All Haystack clients must support Zinc decoding.
64impl IntoResponse for HaystackError {
65    fn into_response(self) -> Response {
66        let grid = error_grid(&self.message);
67        match content::encode_response_grid(&grid, "text/zinc") {
68            Ok((body, content_type)) => (
69                self.status,
70                [(axum::http::header::CONTENT_TYPE, content_type)],
71                body,
72            )
73                .into_response(),
74            Err(_) => (
75                self.status,
76                [(axum::http::header::CONTENT_TYPE, "text/plain")],
77                format!("Error: {}", self.message),
78            )
79                .into_response(),
80        }
81    }
82}
83
84/// Helper to create an HGrid with a single empty row and named columns.
85///
86/// Used by op handlers that return simple tabular data.
87pub fn grid_from_cols_rows(col_names: &[&str], rows: Vec<HDict>) -> HGrid {
88    let cols: Vec<HCol> = col_names.iter().map(|n| HCol::new(*n)).collect();
89    HGrid::from_parts(HDict::new(), cols, rows)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn error_grid_has_err_marker() {
98        let grid = error_grid("something went wrong");
99        assert!(grid.is_err());
100        assert_eq!(
101            grid.meta.get("dis"),
102            Some(&Kind::Str("something went wrong".to_string()))
103        );
104        assert!(grid.is_empty());
105    }
106
107    #[test]
108    fn haystack_error_display() {
109        let err = HaystackError::bad_request("invalid filter");
110        assert_eq!(err.to_string(), "invalid filter");
111        assert_eq!(err.status, StatusCode::BAD_REQUEST);
112    }
113
114    #[test]
115    fn grid_from_cols_rows_builds_correctly() {
116        let mut row = HDict::new();
117        row.set("name", Kind::Str("about".into()));
118        row.set("summary", Kind::Str("Summary of about op".into()));
119
120        let grid = grid_from_cols_rows(&["name", "summary"], vec![row]);
121        assert_eq!(grid.len(), 1);
122        assert_eq!(grid.cols.len(), 2);
123    }
124}