rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use std::collections::HashMap;

use crate::{http::HttpResponse, template::engine::TemplateEngine};
use http::{StatusCode, header};
use minijinja::Value;

/// Render a template with context and return an HttpResponse.
#[must_use]
pub fn render(
    engine: &TemplateEngine,
    template_name: &str,
    context: HashMap<String, String>,
    status: Option<StatusCode>,
    content_type: Option<&str>,
) -> HttpResponse {
    match engine.render(template_name, Value::from_serialize(&context)) {
        Ok(rendered) => {
            let mut response =
                HttpResponse::with_status(status.unwrap_or(StatusCode::OK), rendered);
            response.set_header(
                header::CONTENT_TYPE,
                content_type.unwrap_or("text/html; charset=utf-8"),
            );
            response
        }
        Err(_) => {
            HttpResponse::with_status(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")
        }
    }
}

/// Create a redirect response.
#[must_use]
pub fn redirect(url: &str, permanent: bool) -> HttpResponse {
    let status = if permanent {
        StatusCode::MOVED_PERMANENTLY
    } else {
        StatusCode::FOUND
    };
    let mut response = HttpResponse::with_status(status, "");
    response.set_header(header::LOCATION, url);
    response
}

/// Get object or return 404.
#[must_use]
pub fn get_object_or_404<T>(
    objects: &[T],
    predicate: impl Fn(&T) -> bool,
) -> Result<&T, HttpResponse> {
    objects
        .iter()
        .find(|object| predicate(object))
        .ok_or_else(|| HttpResponse::with_status(StatusCode::NOT_FOUND, "Not Found"))
}

/// Get list or return 404.
#[must_use]
pub fn get_list_or_404<T>(
    objects: &[T],
    predicate: impl Fn(&T) -> bool,
) -> Result<Vec<&T>, HttpResponse> {
    let matches: Vec<&T> = objects.iter().filter(|object| predicate(object)).collect();
    if matches.is_empty() {
        Err(HttpResponse::with_status(
            StatusCode::NOT_FOUND,
            "Not Found",
        ))
    } else {
        Ok(matches)
    }
}

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

    fn response_body(response: &HttpResponse) -> &str {
        std::str::from_utf8(&response.content).expect("utf-8 response body")
    }

    fn response_header(response: &HttpResponse, name: header::HeaderName) -> Option<&str> {
        response.headers.get(name)?.to_str().ok()
    }

    #[test]
    fn test_render_basic_template() {
        let mut engine = TemplateEngine::new();
        engine.add_template("hello.html", "Hello {{ name }}!");

        let response = render(
            &engine,
            "hello.html",
            HashMap::from([(String::from("name"), String::from("World"))]),
            None,
            None,
        );

        assert_eq!(response.status_code, StatusCode::OK);
        assert_eq!(response_body(&response), "Hello World!");
        assert_eq!(
            response_header(&response, header::CONTENT_TYPE),
            Some("text/html; charset=utf-8")
        );
    }

    #[test]
    fn test_render_with_custom_status() {
        let mut engine = TemplateEngine::new();
        engine.add_template("created.html", "Created");

        let response = render(
            &engine,
            "created.html",
            HashMap::new(),
            Some(StatusCode::CREATED),
            None,
        );

        assert_eq!(response.status_code, StatusCode::CREATED);
        assert_eq!(response_body(&response), "Created");
    }

    #[test]
    fn test_render_missing_template_returns_500() {
        let engine = TemplateEngine::new();

        let response = render(&engine, "missing.html", HashMap::new(), None, None);

        assert_eq!(response.status_code, StatusCode::INTERNAL_SERVER_ERROR);
        assert_eq!(response_body(&response), "Internal Server Error");
    }

    #[test]
    fn test_render_empty_context() {
        let mut engine = TemplateEngine::new();
        engine.add_template("empty.html", "No context needed");

        let response = render(&engine, "empty.html", HashMap::new(), None, None);

        assert_eq!(response.status_code, StatusCode::OK);
        assert_eq!(response_body(&response), "No context needed");
    }

    #[test]
    fn test_render_with_custom_content_type() {
        let mut engine = TemplateEngine::new();
        engine.add_template("page.html", "Page");

        let response = render(
            &engine,
            "page.html",
            HashMap::new(),
            None,
            Some("application/xhtml+xml"),
        );

        assert_eq!(
            response_header(&response, header::CONTENT_TYPE),
            Some("application/xhtml+xml")
        );
        assert_eq!(response.content_type, "application/xhtml+xml");
    }

    #[test]
    fn test_render_custom_status_and_content_type() {
        let mut engine = TemplateEngine::new();
        engine.add_template("page.html", "Page");

        let response = render(
            &engine,
            "page.html",
            HashMap::new(),
            Some(StatusCode::ACCEPTED),
            Some("text/plain; charset=utf-8"),
        );

        assert_eq!(response.status_code, StatusCode::ACCEPTED);
        assert_eq!(
            response_header(&response, header::CONTENT_TYPE),
            Some("text/plain; charset=utf-8")
        );
    }

    #[test]
    fn test_redirect_temporary() {
        let response = redirect("/login", false);

        assert_eq!(response.status_code, StatusCode::FOUND);
        assert_eq!(response_header(&response, header::LOCATION), Some("/login"));
        assert!(response.content.is_empty());
    }

    #[test]
    fn test_redirect_permanent() {
        let response = redirect("/moved", true);

        assert_eq!(response.status_code, StatusCode::MOVED_PERMANENTLY);
        assert_eq!(response_header(&response, header::LOCATION), Some("/moved"));
    }

    #[test]
    fn test_redirect_preserves_exact_location() {
        let response = redirect("https://example.com/path?q=rust#frag", false);

        assert_eq!(
            response_header(&response, header::LOCATION),
            Some("https://example.com/path?q=rust#frag")
        );
    }

    #[test]
    fn test_get_object_or_404_found() {
        let values = [1, 2, 3];

        let object = get_object_or_404(&values, |value| *value == 2).expect("object found");

        assert_eq!(*object, 2);
    }

    #[test]
    fn test_get_object_or_404_returns_first_match() {
        let values = [2, 4, 6, 8];

        let object = get_object_or_404(&values, |value| *value % 2 == 0).expect("object found");

        assert_eq!(*object, 2);
    }

    #[test]
    fn test_get_object_or_404_not_found() {
        let values = [1, 2, 3];

        let response = get_object_or_404(&values, |value| *value == 4).expect_err("404 response");

        assert_eq!(response.status_code, StatusCode::NOT_FOUND);
        assert_eq!(response_body(&response), "Not Found");
    }

    #[test]
    fn test_get_object_or_404_empty_slice() {
        let values: [i32; 0] = [];

        let response = get_object_or_404(&values, |_| true).expect_err("404 response");

        assert_eq!(response.status_code, StatusCode::NOT_FOUND);
    }

    #[test]
    fn test_get_list_or_404_found() {
        let values = [1, 2, 3, 4];

        let objects = get_list_or_404(&values, |value| *value % 2 == 0).expect("objects found");

        assert_eq!(objects, vec![&2, &4]);
    }

    #[test]
    fn test_get_list_or_404_partial_matches() {
        let values = ["alpha", "beta", "gamma"];

        let objects = get_list_or_404(&values, |value| value.contains('a')).expect("objects found");

        assert_eq!(objects, vec![&"alpha", &"beta", &"gamma"]);
    }

    #[test]
    fn test_get_list_or_404_empty() {
        let values = [1, 2, 3];

        let response = get_list_or_404(&values, |value| *value > 10).expect_err("404 response");

        assert_eq!(response.status_code, StatusCode::NOT_FOUND);
        assert_eq!(response_body(&response), "Not Found");
    }

    #[test]
    fn test_get_list_or_404_preserves_order() {
        let values = [5, 1, 3, 2, 4];

        let objects = get_list_or_404(&values, |value| *value > 2).expect("objects found");

        assert_eq!(objects, vec![&5, &3, &4]);
    }
}