Skip to main content

haystack_server/
error.rs

1//! Haystack error grids and Actix Web error integration.
2
3use actix_web::http::StatusCode;
4use actix_web::{HttpResponse, ResponseError};
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
61impl ResponseError for HaystackError {
62    fn status_code(&self) -> StatusCode {
63        self.status
64    }
65
66    fn error_response(&self) -> HttpResponse {
67        let grid = error_grid(&self.message);
68        // Encode as zinc by default for error responses
69        match content::encode_response_grid(&grid, "text/zinc") {
70            Ok((body, content_type)) => HttpResponse::build(self.status)
71                .content_type(content_type)
72                .body(body),
73            Err(_) => HttpResponse::build(self.status)
74                .content_type("text/plain")
75                .body(format!("Error: {}", self.message)),
76        }
77    }
78}
79
80/// Helper to create an HGrid with a single empty row and named columns.
81///
82/// Used by op handlers that return simple tabular data.
83pub fn grid_from_cols_rows(col_names: &[&str], rows: Vec<HDict>) -> HGrid {
84    let cols: Vec<HCol> = col_names.iter().map(|n| HCol::new(*n)).collect();
85    HGrid::from_parts(HDict::new(), cols, rows)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn error_grid_has_err_marker() {
94        let grid = error_grid("something went wrong");
95        assert!(grid.is_err());
96        assert_eq!(
97            grid.meta.get("dis"),
98            Some(&Kind::Str("something went wrong".to_string()))
99        );
100        assert!(grid.is_empty());
101    }
102
103    #[test]
104    fn haystack_error_display() {
105        let err = HaystackError::bad_request("invalid filter");
106        assert_eq!(err.to_string(), "invalid filter");
107        assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
108    }
109
110    #[test]
111    fn haystack_error_response_is_grid() {
112        let err = HaystackError::internal("test error");
113        let resp = err.error_response();
114        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
115    }
116
117    #[test]
118    fn grid_from_cols_rows_builds_correctly() {
119        let mut row = HDict::new();
120        row.set("name", Kind::Str("about".into()));
121        row.set("summary", Kind::Str("Summary of about op".into()));
122
123        let grid = grid_from_cols_rows(&["name", "summary"], vec![row]);
124        assert_eq!(grid.len(), 1);
125        assert_eq!(grid.cols.len(), 2);
126    }
127}