trillium-api 0.3.0

an api handler for trillium.rs
Documentation
use serde::{Deserialize, Serialize};
use trillium::{Conn, Handler, Headers, KnownHeaderName, Status};
use trillium_api::*;
use trillium_testing::{TestServer, harness, test};

#[derive(Serialize, Deserialize, Debug)]
struct Struct {
    string: String,
    numbers: Option<Vec<usize>>,
}

#[derive(Serialize, Deserialize, Debug)]
struct ApiResponse {
    s: Struct,
}

fn app_with_body() -> impl Handler {
    api(|__: &mut Conn, Body(mut s): Body<Struct>| async move {
        if let Some(numbers) = &mut s.numbers {
            numbers.push(100);
        }
        Body(ApiResponse { s })
    })
}

#[test(harness)]
async fn json_request_json_response() {
    let app = TestServer::new(app_with_body()).await;

    app.post("/")
        .with_request_header("content-type", "application/json")
        .with_body(r#"{"string": "string", "numbers": [ 1, 2, 3]}"#)
        .await
        .assert_ok()
        .assert_body(r#"{"s":{"string":"string","numbers":[1,2,3,100]}}"#);
}

#[test(harness)]
async fn form_urlencoded_json_response() {
    let app = TestServer::new(app_with_body()).await;

    app.post("/")
        .with_request_header("content-type", "application/x-www-form-urlencoded")
        .with_body(r#"string=string"#)
        .await
        .assert_ok()
        .assert_body(r#"{"s":{"string":"string","numbers":null}}"#);
}

#[cfg(feature = "sonic-rs")]
#[test(harness)]
async fn malformed_json_request() {
    let app = TestServer::new(app_with_body()).await;

    let response = app
        .post("/")
        .with_request_header("content-type", "application/json")
        .with_body(r#"this is not valid json"#)
        .await;

    response.assert_status(422);
    let response_body = response.body();
    let expected = sonic_rs::json!({"error": {"path": ".", "message": "Invalid literal (`true`, `false`, or a `null`) while parsing at line 1 column 4\n\n\tthis is not\n\t...^.......\n","type":"parse_error"}});
    assert_eq!(
        sonic_rs::from_str::<sonic_rs::Value>(response_body).unwrap(),
        expected
    );
}

fn app_without_body() -> impl Handler {
    api(|_: &mut Conn, _: ()| async { Json(json!({"health": "ok" })) })
}

#[test(harness)]
async fn get_json_response() {
    let app = TestServer::new(app_without_body()).await;

    app.get("/")
        .await
        .assert_ok()
        .assert_body(r#"{"health":"ok"}"#)
        .assert_header(KnownHeaderName::ContentType, "application/json");
}

#[test(harness)]
async fn get_custom_content_type() {
    let handler = (
        Headers::from_iter([(KnownHeaderName::ContentType, "application/custom+json")]),
        Json(json!({"health": "ok"})),
    );
    let app = TestServer::new(handler).await;

    app.get("/")
        .await
        .assert_ok()
        .assert_body(r#"{"health":"ok"}"#)
        .assert_header(KnownHeaderName::ContentType, "application/custom+json");
}

fn app_with_json() -> impl Handler {
    api(|_: &mut Conn, Json(value): Json<Value>| async { Json(value) })
}

#[test(harness)]
async fn json_try_from_conn_checks_content_type() {
    let app = TestServer::new(app_with_json()).await;

    app.post("/")
        .with_request_header("content-type", "application/x-www-form-urlencoded")
        .with_body(r#"string=string"#)
        .await
        .assert_status(trillium::Status::UnsupportedMediaType);

    app.post("/")
        .with_request_header("content-type", "application/json")
        .with_body(r#"{"string": 1}"#)
        .await
        .assert_ok();
}

async fn error_handler(conn: &mut Conn, error: Error) {
    conn.set_body(format!("my error format: {error:?}"));
    conn.set_status(&error);
}

fn app_with_error_handler() -> impl Handler {
    (
        api(|_: &mut Conn, Json(value): Json<Value>| async { Json(value) }),
        BeforeSend(api(error_handler)),
    )
}

#[test(harness)]
async fn error_handler_works() {
    let _ = env_logger::builder().is_test(true).try_init();
    let app = TestServer::new(app_with_error_handler()).await;

    app.post("/")
        .with_request_header("content-type", "application/x-www-form-urlencoded")
        .with_body(r#"string=string"#)
        .await
        .assert_status(Status::UnsupportedMediaType)
        .assert_body(
            "my error format: UnsupportedMimeType { mime_type: \
             \"application/x-www-form-urlencoded\" }",
        );
}