vantus 0.2.0

Macro-first async Rust web platform with typed extraction, DI, and configuration binding.
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use vantus::{
    AppConfig, FrameworkError, HostBuilder, HostContext, Module, Path, Query, Request, Response,
    Service, ServiceCollection, TextBody, controller, module,
};
use serde::Serialize;

#[derive(Default)]
struct GreetingService;

impl GreetingService {
    fn greet(&self, value: &str) -> String {
        format!("hello {value}")
    }
}

#[derive(Clone, Default)]
struct MacroController;

#[controller]
impl MacroController {
    #[vantus::get("/users/{id}")]
    fn show(&self, id: Path<u32>, mode: Query<String>, config: AppConfig) -> Response {
        Response::text(format!(
            "{}:{}:{}",
            id.into_inner(),
            mode.into_inner(),
            config.service_name
        ))
    }

    #[vantus::post("/echo")]
    fn echo(
        &self,
        service: Service<GreetingService>,
        body: TextBody,
    ) -> Result<Response, FrameworkError> {
        #[derive(Serialize)]
        struct Payload {
            echoed: String,
        }

        Response::json_serialized(&Payload {
            echoed: service.as_ref().greet(body.as_str()),
        })
        .map_err(|error| FrameworkError::Internal(error.to_string()))
    }
}

#[derive(Clone)]
struct MacroModule {
    started: Arc<AtomicBool>,
    stopped: Arc<AtomicBool>,
}

#[allow(dead_code)]
#[module]
impl MacroModule {
    fn configure_services(&self, services: &mut ServiceCollection) -> Result<(), FrameworkError> {
        services.add_singleton(GreetingService);
        Ok(())
    }

    fn configure_routes(
        &self,
        routes: &mut dyn vantus::__private::RouteRegistrar,
    ) -> Result<(), FrameworkError> {
        <MacroController as Module>::configure_routes(&MacroController, routes)
    }

    async fn on_start(&self, _host: &HostContext) -> Result<(), FrameworkError> {
        self.started.store(true, Ordering::SeqCst);
        Ok(())
    }

    async fn on_stop(&self, _host: &HostContext) -> Result<(), FrameworkError> {
        self.stopped.store(true, Ordering::SeqCst);
        Ok(())
    }
}

#[tokio::test]
async fn controller_macro_registers_routes_and_extracts_values() {
    let mut builder = HostBuilder::new();
    builder.module(MacroModule {
        started: Arc::new(AtomicBool::new(false)),
        stopped: Arc::new(AtomicBool::new(false)),
    });
    let host = builder.build().unwrap();

    let response = host
        .handle(Request::from_bytes(b"GET /users/42?mode=verbose HTTP/1.1\r\n\r\n").unwrap())
        .await;
    assert_eq!(
        String::from_utf8(response.body).unwrap(),
        "42:verbose:mini-backend"
    );
}

#[tokio::test]
async fn module_macro_forwards_async_lifecycle_hooks() {
    let started = Arc::new(AtomicBool::new(false));
    let stopped = Arc::new(AtomicBool::new(false));
    let mut builder = HostBuilder::new();
    builder.module(MacroModule {
        started: Arc::clone(&started),
        stopped: Arc::clone(&stopped),
    });

    let host = builder.build().unwrap();
    let server = host.serve().await.unwrap();
    assert!(started.load(Ordering::SeqCst));

    server.shutdown();
    server.wait().await.unwrap();
    assert!(stopped.load(Ordering::SeqCst));
}

#[test]
fn macro_invalid_parameter_patterns_fail_with_compile_errors() {
    let t = trybuild::TestCases::new();
    t.compile_fail("tests/ui/*.rs");
}