qrush 2.0.0

Lightweight Job Queue and Task Scheduler for Rust (Actix + Redis + Cron)
Documentation
// src/services/http_response.rs
//
// Framework-neutral HTTP response used by the dashboard service layer.
//
// The dashboard handlers (metrics, cron, templates) are written against this
// type so the exact same logic can be served by either Actix or Axum. Each
// framework adapter (`routes::metrics_route` for Actix, `routes::axum_route`
// for Axum) converts a `DashboardResponse` into its own response type.

/// A minimal, self-describing HTTP response: status, content type, body bytes
/// and any extra headers. Deliberately independent of any web framework.
pub struct DashboardResponse {
    pub status: u16,
    pub content_type: String,
    pub body: Vec<u8>,
    pub headers: Vec<(String, String)>,
}

impl DashboardResponse {
    pub fn new(status: u16, content_type: impl Into<String>, body: Vec<u8>) -> Self {
        Self {
            status,
            content_type: content_type.into(),
            body,
            headers: Vec::new(),
        }
    }

    /// Append an extra response header (builder style).
    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.push((name.into(), value.into()));
        self
    }

    /// `200 OK` HTML response.
    pub fn html(body: impl Into<String>) -> Self {
        Self::new(200, "text/html", body.into().into_bytes())
    }

    /// HTML response with an explicit status (e.g. `404`, `500`).
    pub fn html_status(status: u16, body: impl Into<String>) -> Self {
        Self::new(status, "text/html", body.into().into_bytes())
    }

    /// Plain-text response (used for terse error bodies like `"Redis error"`).
    pub fn text(status: u16, body: impl Into<String>) -> Self {
        Self::new(status, "text/plain; charset=utf-8", body.into().into_bytes())
    }

    /// `200 OK` JSON response.
    pub fn json(value: &serde_json::Value) -> Self {
        Self::json_status(200, value)
    }

    /// JSON response with an explicit status.
    pub fn json_status(status: u16, value: &serde_json::Value) -> Self {
        let body = serde_json::to_vec(value).unwrap_or_else(|_| b"{}".to_vec());
        Self::new(status, "application/json", body)
    }

    /// `200 OK` CSV download with a `Content-Disposition` attachment header.
    pub fn csv(body: Vec<u8>, filename: &str) -> Self {
        Self::new(200, "text/csv", body).with_header(
            "Content-Disposition",
            format!("attachment; filename={}", filename),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn html_defaults_to_200_text_html() {
        let r = DashboardResponse::html("<p>hi</p>");
        assert_eq!(r.status, 200);
        assert_eq!(r.content_type, "text/html");
        assert_eq!(r.body, b"<p>hi</p>");
        assert!(r.headers.is_empty());
    }

    #[test]
    fn html_status_uses_given_status() {
        let r = DashboardResponse::html_status(500, "boom");
        assert_eq!(r.status, 500);
        assert_eq!(r.content_type, "text/html");
    }

    #[test]
    fn text_is_plain() {
        let r = DashboardResponse::text(500, "Redis error");
        assert_eq!(r.status, 500);
        assert_eq!(r.content_type, "text/plain; charset=utf-8");
        assert_eq!(r.body, b"Redis error");
    }

    #[test]
    fn json_status_serializes_compactly() {
        let r = DashboardResponse::json_status(404, &serde_json::json!({"error": "job not found"}));
        assert_eq!(r.status, 404);
        assert_eq!(r.content_type, "application/json");
        assert_eq!(r.body, br#"{"error":"job not found"}"#);
    }

    #[test]
    fn csv_sets_attachment_disposition() {
        let r = DashboardResponse::csv(b"a,b\n1,2\n".to_vec(), "queue_default.csv");
        assert_eq!(r.status, 200);
        assert_eq!(r.content_type, "text/csv");
        let disposition = r
            .headers
            .iter()
            .find(|(k, _)| k == "Content-Disposition")
            .map(|(_, v)| v.as_str());
        assert_eq!(
            disposition,
            Some("attachment; filename=queue_default.csv")
        );
    }

    #[test]
    fn with_header_appends() {
        let r = DashboardResponse::html("x").with_header("X-Test", "1");
        assert!(r.headers.iter().any(|(k, v)| k == "X-Test" && v == "1"));
    }
}