nestrs 0.3.4

NestJS-like API framework for Rust on top of Axum and Tower.
Documentation
use axum::body::Body;
use axum::http::{Request, StatusCode};
use nestrs::prelude::*;
use std::panic::{catch_unwind, AssertUnwindSafe};
use tower::util::ServiceExt;

#[derive(Default)]
#[injectable]
struct AState;

#[controller(prefix = "/a", version = "v1")]
struct AController;

impl AController {
    #[get("/")]
    async fn ok() -> &'static str {
        "a"
    }
}

impl_routes!(AController, state AState => [
    GET "/" with () => AController::ok,
]);

#[derive(Default)]
#[injectable]
struct BState;

#[controller(prefix = "/b", version = "v1")]
struct BController;

impl BController {
    #[get("/")]
    async fn ok() -> &'static str {
        "b"
    }
}

impl_routes!(BController, state BState => [
    GET "/" with () => BController::ok,
]);

#[module(
    imports = [BModule],
    controllers = [AController],
    providers = [AState],
)]
struct AModule;

#[module(
    imports = [AModule],
    controllers = [BController],
    providers = [BState],
)]
struct BModule;

#[test]
fn cyclic_module_imports_panic_with_clear_message() {
    let err = catch_unwind(AssertUnwindSafe(|| {
        let _ = <AModule as Module>::build();
    }))
    .expect_err("should panic on cyclic module imports");

    let msg = err
        .downcast_ref::<String>()
        .map(|s| s.as_str())
        .or_else(|| err.downcast_ref::<&str>().copied())
        .unwrap_or("<non-string panic>");

    assert!(
        msg.contains("Circular module dependency detected"),
        "unexpected panic message: {msg}"
    );
}

#[module(
    imports = [BForwardModule],
    controllers = [AController],
    providers = [AState],
)]
struct AForwardModule;

#[module(
    imports = [forward_ref::<AForwardModule>()],
    controllers = [BController],
    providers = [BState],
)]
struct BForwardModule;

#[tokio::test]
async fn forward_ref_breaks_back_edge_and_builds_router() {
    let (_, router) = <AForwardModule as Module>::build();

    let a = router
        .clone()
        .oneshot(
            Request::builder()
                .uri("/v1/a")
                .method("GET")
                .body(Body::empty())
                .expect("request should be valid"),
        )
        .await
        .expect("router should serve request");
    assert_eq!(a.status(), StatusCode::OK);

    let b = router
        .oneshot(
            Request::builder()
                .uri("/v1/b")
                .method("GET")
                .body(Body::empty())
                .expect("request should be valid"),
        )
        .await
        .expect("router should serve request");
    assert_eq!(b.status(), StatusCode::OK);
}