grave 0.1.0

A lightweight, declarative Rust web framework built on Axum — define routes, state, and config with a single macro
Documentation
use grave::app;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUser {
    name: String,
}

#[derive(Serialize)]
struct User {
    name: String,
}

impl User {
    fn new(name: String) -> Self {
        Self { name }
    }
}

#[derive(Clone)]
struct AppState {
    message: String,
}

app! {
    Config => {
        port: 8081,
        host: "127.0.0.1",
        state: AppState {
            message: "Hello from State!".to_string()
        }
    },

    GET "/hello" => { "Hello world" },

    GET "/state" => |grave::State(state): grave::State<AppState>| {
        state.message.clone()
    },

    POST "/users" => |grave::Json(body): grave::Json<CreateUser>| {
        grave::Json(User::new(body.name))
    },

    GET "/hello/:name" => |Path(name): Path<String>| {
        format!("Hello, {}!", name)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use http::Request;
    use http::StatusCode;
    use http_body_util::BodyExt;
    use tower::ServiceExt;

    #[tokio::test]
    async fn test_get_hello_returns_hello_world() {
        let app = create_app();

        let response = app
            .oneshot(
                Request::builder()
                    .uri("/hello")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
        let body = response.into_body().collect().await.unwrap().to_bytes();
        assert_eq!(&body[..], b"Hello world");
    }

    #[tokio::test]
    async fn test_get_state_returns_state_message() {
        let app = create_app();

        let response = app
            .oneshot(
                Request::builder()
                    .uri("/state")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
        let body = response.into_body().collect().await.unwrap().to_bytes();
        assert_eq!(&body[..], b"Hello from State!");
    }

    #[tokio::test]
    async fn test_post_users_returns_json() {
        let app = create_app();

        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/users")
                    .header("Content-Type", "application/json")
                    .body(Body::from(r#"{"name":"grave"}"#))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
        let body = response.into_body().collect().await.unwrap().to_bytes();
        let user: serde_json::Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(user["name"], "grave");
    }

    #[tokio::test]
    async fn test_get_hello_name_returns_greeting() {
        let app = create_app();

        let response = app
            .oneshot(
                Request::builder()
                    .uri("/hello/world")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
        let body = response.into_body().collect().await.unwrap().to_bytes();
        assert_eq!(&body[..], b"Hello, world!");
    }

    #[tokio::test]
    async fn test_not_found_returns_404() {
        let app = create_app();

        let response = app
            .oneshot(
                Request::builder()
                    .uri("/nonexistent")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

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