nestforge 1.9.0

NestJS-inspired modular backend framework for Rust
Documentation
use nestforge::{
    controller, routes, Container, ControllerDefinition, HttpException, ModuleDefinition, Pipe,
    PipedBody, PipedParam, PipedQuery, RequestContext,
};
use serde::Deserialize;
use tower::ServiceExt;

struct TrimmedSlugPipe;

impl Pipe<String> for TrimmedSlugPipe {
    type Output = String;

    fn transform(value: String, _ctx: &RequestContext) -> Result<Self::Output, HttpException> {
        let slug = value.trim().to_lowercase();
        if slug.is_empty() {
            return Err(HttpException::bad_request("slug is required"));
        }
        Ok(slug)
    }
}

#[derive(Deserialize)]
struct SearchQuery {
    q: String,
}

struct SearchPipe;

impl Pipe<SearchQuery> for SearchPipe {
    type Output = String;

    fn transform(value: SearchQuery, _ctx: &RequestContext) -> Result<Self::Output, HttpException> {
        let query = value.q.trim().to_string();
        if query.len() < 2 {
            return Err(HttpException::bad_request("query must be at least 2 chars"));
        }
        Ok(query)
    }
}

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

struct CreatePipe;

impl Pipe<CreatePayload> for CreatePipe {
    type Output = String;

    fn transform(
        value: CreatePayload,
        _ctx: &RequestContext,
    ) -> Result<Self::Output, HttpException> {
        let name = value.name.trim().to_string();
        if name.is_empty() {
            return Err(HttpException::bad_request("name is required"));
        }
        Ok(name)
    }
}

#[controller("/pipes")]
struct PipesController;

#[routes]
impl PipesController {
    #[nestforge::get("/slug/{value}")]
    async fn slug(value: PipedParam<String, TrimmedSlugPipe>) -> Result<String, HttpException> {
        Ok(value.into_inner())
    }

    #[nestforge::get("/search")]
    async fn search(query: PipedQuery<SearchQuery, SearchPipe>) -> Result<String, HttpException> {
        Ok(query.into_inner())
    }

    #[nestforge::post("/body")]
    async fn body(name: PipedBody<CreatePayload, CreatePipe>) -> Result<String, HttpException> {
        Ok(name.into_inner())
    }
}

struct PipesModule;

impl ModuleDefinition for PipesModule {
    fn register(_container: &Container) -> anyhow::Result<()> {
        Ok(())
    }

    fn controllers() -> Vec<axum::Router<Container>> {
        vec![PipesController::router()]
    }
}

#[tokio::test]
async fn piped_param_transforms_route_input() {
    let app = nestforge::NestForgeFactory::<PipesModule>::create()
        .expect("factory should build")
        .into_router();

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .uri("/pipes/slug/  Hello-World  ")
                .body(axum::body::Body::empty())
                .expect("request should build"),
        )
        .await
        .expect("request should succeed");

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

#[tokio::test]
async fn piped_query_can_reject_invalid_query_values() {
    let app = nestforge::NestForgeFactory::<PipesModule>::create()
        .expect("factory should build")
        .into_router();

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .uri("/pipes/search?q=a")
                .body(axum::body::Body::empty())
                .expect("request should build"),
        )
        .await
        .expect("request should succeed");

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

#[tokio::test]
async fn piped_body_transforms_json_payloads() {
    let app = nestforge::NestForgeFactory::<PipesModule>::create()
        .expect("factory should build")
        .into_router();

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .method("POST")
                .uri("/pipes/body")
                .header(axum::http::header::CONTENT_TYPE, "application/json")
                .body(axum::body::Body::from(
                    serde_json::json!({ "name": "  Vernon  " }).to_string(),
                ))
                .expect("request should build"),
        )
        .await
        .expect("request should succeed");

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