nestrs 0.3.8

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

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

#[dto]
struct StrictDto {
    #[IsString]
    value: String,
}

#[dto]
struct NestedStrictInnerDto {
    #[IsString]
    label: String,
}

#[dto]
struct NestedStrictDto {
    #[ValidateNested]
    inner: NestedStrictInnerDto,
    #[IsOptional]
    #[ValidateNested]
    optional_inner: Option<NestedStrictInnerDto>,
}

#[dto(allow_unknown_fields)]
struct LooseDto {
    #[IsString]
    value: String,
}

#[controller(prefix = "/dto", version = "v1")]
struct DtoController;

#[routes(state = AppState)]
impl DtoController {
    #[post("/strict")]
    async fn strict(ValidatedBody(dto): ValidatedBody<StrictDto>) -> String {
        dto.value
    }

    #[post("/nested")]
    async fn nested(ValidatedBody(dto): ValidatedBody<NestedStrictDto>) -> String {
        dto.inner.label
    }

    #[post("/loose")]
    async fn loose(ValidatedBody(dto): ValidatedBody<LooseDto>) -> String {
        dto.value
    }
}

#[module(controllers = [DtoController], providers = [AppState])]
struct AppModule;

#[tokio::test]
async fn dto_rejects_unknown_top_level_fields_by_default() {
    let router = NestFactory::create::<AppModule>().into_router();

    let response = router
        .oneshot(
            Request::builder()
                .uri("/v1/dto/strict")
                .method("POST")
                .header(header::CONTENT_TYPE, "application/json")
                .body(Body::from(r#"{"value":"ok","extra":"boom"}"#))
                .expect("request"),
        )
        .await
        .expect("serve");

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

#[tokio::test]
async fn dto_rejects_unknown_nested_fields() {
    let router = NestFactory::create::<AppModule>().into_router();

    let response = router
        .oneshot(
            Request::builder()
                .uri("/v1/dto/nested")
                .method("POST")
                .header(header::CONTENT_TYPE, "application/json")
                .body(Body::from(
                    r#"{"inner":{"label":"ok","extra":"boom"},"optional_inner":null}"#,
                ))
                .expect("request"),
        )
        .await
        .expect("serve");

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

#[tokio::test]
async fn dto_allows_optional_nested_fields_when_omitted() {
    let router = NestFactory::create::<AppModule>().into_router();

    let response = router
        .oneshot(
            Request::builder()
                .uri("/v1/dto/nested")
                .method("POST")
                .header(header::CONTENT_TYPE, "application/json")
                .body(Body::from(r#"{"inner":{"label":"ok"}}"#))
                .expect("request"),
        )
        .await
        .expect("serve");

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

#[tokio::test]
async fn dto_allow_unknown_fields_option_allows_extra_keys() {
    let router = NestFactory::create::<AppModule>().into_router();

    let response = router
        .oneshot(
            Request::builder()
                .uri("/v1/dto/loose")
                .method("POST")
                .header(header::CONTENT_TYPE, "application/json")
                .body(Body::from(r#"{"value":"ok","extra":"allowed"}"#))
                .expect("request"),
        )
        .await
        .expect("serve");

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