rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use http::StatusCode;

use crate::http::{HttpResponse, request::HttpRequest};

#[derive(Debug, Clone)]
pub struct TechnicalErrorResponse {
    pub status: StatusCode,
    pub exception_type: String,
    pub exception_value: String,
    pub request_path: String,
    pub request_method: String,
    pub traceback: Vec<String>,
    pub settings: Vec<(String, String)>,
}

impl TechnicalErrorResponse {
    #[must_use]
    pub fn new(status: StatusCode, error: &str) -> Self {
        let (exception_type, exception_value) = split_error(error);

        Self {
            status,
            exception_type,
            exception_value,
            request_path: String::new(),
            request_method: String::new(),
            traceback: Vec::new(),
            settings: Vec::new(),
        }
    }

    #[must_use]
    pub fn render(&self) -> HttpResponse {
        let title = format!(
            "{} {}",
            self.status.as_u16(),
            self.status.canonical_reason().unwrap_or("Error")
        );
        let traceback_items = if self.traceback.is_empty() {
            "<li>No traceback available.</li>".to_string()
        } else {
            self.traceback
                .iter()
                .map(|line| format!("<li>{}</li>", escape_html(line)))
                .collect::<Vec<_>>()
                .join("")
        };
        let settings_rows = if self.settings.is_empty() {
            "<tr><th>Details</th><td>None</td></tr>".to_string()
        } else {
            self.settings
                .iter()
                .map(|(key, value)| {
                    format!(
                        "<tr><th>{}</th><td><pre>{}</pre></td></tr>",
                        escape_html(key),
                        escape_html(value)
                    )
                })
                .collect::<Vec<_>>()
                .join("")
        };

        let body = format!(
            concat!(
                "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">",
                "<title>{title}</title>",
                "<style>",
                "body{{margin:0;font-family:Arial,sans-serif;background:#f6f6cf;color:#111;}}",
                ".page{{max-width:1100px;margin:0 auto;padding:24px;}}",
                "h1{{margin:0 0 8px;font-size:32px;}}",
                ".subtitle{{font-size:18px;margin-bottom:20px;}}",
                ".panel{{background:#fff;border:1px solid #d6d0a5;margin-bottom:16px;}}",
                ".panel h2{{margin:0;padding:10px 14px;background:#efe8a1;font-size:18px;border-bottom:1px solid #d6d0a5;}}",
                ".panel .content{{padding:14px;}}",
                "table{{width:100%;border-collapse:collapse;}}",
                "th,td{{padding:10px;vertical-align:top;border-top:1px solid #ece5b6;text-align:left;}}",
                "th{{width:220px;background:#faf7d9;}}",
                "ul{{margin:0;padding-left:24px;}}pre{{margin:0;white-space:pre-wrap;word-break:break-word;}}",
                "code{{background:#faf7d9;padding:2px 4px;}}",
                "</style></head><body><div class=\"page\">",
                "<h1>{title}</h1><div class=\"subtitle\">{exception_type}: {exception_value}</div>",
                "<section class=\"panel\"><h2>Request information</h2><div class=\"content\"><table>",
                "<tr><th>Method</th><td><code>{request_method}</code></td></tr>",
                "<tr><th>Path</th><td><code>{request_path}</code></td></tr>",
                "</table></div></section>",
                "<section class=\"panel\"><h2>Traceback</h2><div class=\"content\"><ul>{traceback_items}</ul></div></section>",
                "<section class=\"panel\"><h2>Environment</h2><div class=\"content\"><table>{settings_rows}</table></div></section>",
                "</div></body></html>"
            ),
            title = escape_html(&title),
            exception_type = escape_html(&self.exception_type),
            exception_value = escape_html(&self.exception_value),
            request_method = escape_html(&self.request_method),
            request_path = escape_html(&self.request_path),
            traceback_items = traceback_items,
            settings_rows = settings_rows,
        );

        HttpResponse::with_status(self.status, body)
    }
}

/// Generate a debug 404 response listing URL patterns.
#[must_use]
pub fn technical_404_response(request: &HttpRequest, patterns: &[String]) -> HttpResponse {
    let mut error =
        TechnicalErrorResponse::new(StatusCode::NOT_FOUND, "Resolver404: Page not found");
    error.request_method = request.method.to_string();
    error.request_path = request.get_full_path();
    error.exception_value = format!(
        "No route matched {}. {} pattern(s) were tried.",
        request.path,
        patterns.len()
    );
    error.traceback = patterns
        .iter()
        .map(|pattern| format!("Tried pattern: {pattern}"))
        .collect();
    error.settings = vec![("Headers".to_string(), headers_summary(request))];

    error.render()
}

/// Generate a debug 500 response with traceback.
#[must_use]
pub fn technical_500_response(request: &HttpRequest, error: &str) -> HttpResponse {
    let mut technical = TechnicalErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, error);
    technical.request_method = request.method.to_string();
    technical.request_path = request.get_full_path();
    technical.traceback = error.lines().map(str::to_owned).collect();
    technical.settings = vec![("Headers".to_string(), headers_summary(request))];

    technical.render()
}

#[must_use]
fn split_error(error: &str) -> (String, String) {
    if let Some((kind, value)) = error.split_once(':') {
        (kind.trim().to_string(), value.trim().to_string())
    } else {
        ("Error".to_string(), error.to_string())
    }
}

#[must_use]
fn headers_summary(request: &HttpRequest) -> String {
    if request.headers.is_empty() {
        return "No headers".to_string();
    }

    request
        .headers
        .iter()
        .map(|(name, value)| {
            format!(
                "{}: {}",
                name.as_str(),
                String::from_utf8_lossy(value.as_bytes())
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}

#[must_use]
fn escape_html(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('\"', "&quot;")
        .replace('\'', "&#x27;")
}

#[cfg(test)]
mod tests {
    use bytes::Bytes;
    use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, header::HOST};

    use super::{technical_404_response, technical_500_response};
    use crate::http::request::HttpRequest;

    fn build_request() -> HttpRequest {
        let mut headers = HeaderMap::new();
        headers.insert(HOST, HeaderValue::from_static("example.com"));
        headers.insert("x-request-id", HeaderValue::from_static("req-123"));

        HttpRequest::from_axum(
            Method::GET,
            Uri::from_static("/missing/?page=2"),
            headers,
            Bytes::new(),
        )
    }

    #[test]
    fn test_technical_404_includes_patterns() {
        let response = technical_404_response(
            &build_request(),
            &["admin/".to_string(), "articles/<slug>/".to_string()],
        );
        let body = std::str::from_utf8(&response.content).expect("utf8 body");

        assert_eq!(response.status_code, StatusCode::NOT_FOUND);
        assert!(body.contains("admin/"));
        assert!(body.contains("articles/&lt;slug&gt;/"));
        assert!(body.contains("/missing/?page=2"));
    }

    #[test]
    fn test_technical_500_includes_error() {
        let response =
            technical_500_response(&build_request(), "ValueError: broken\nframe one\nframe two");
        let body = std::str::from_utf8(&response.content).expect("utf8 body");

        assert_eq!(response.status_code, StatusCode::INTERNAL_SERVER_ERROR);
        assert!(body.contains("ValueError"));
        assert!(body.contains("broken"));
        assert!(body.contains("frame one"));
        assert!(body.contains("x-request-id: req-123"));
    }
}