rustapi-core 0.1.450

The core engine of the RustAPI framework. Provides the hyper-based HTTP server, router, extraction logic, and foundational traits.
Documentation
use bytes::Bytes;
use http::{Extensions, Method, StatusCode};
use rustapi_core::interceptor::{RequestInterceptor, ResponseInterceptor};
use rustapi_core::middleware::{BoxedNext, MiddlewareLayer};
use rustapi_core::{
    get, BodyVariant, IntoResponse, PathParams, Request, Response, RouteMatch, RustApi,
};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Instant;

const DEFAULT_WARMUP_ITERS: usize = 2_000;
const DEFAULT_SAMPLE_ITERS: usize = 20_000;

#[derive(Clone)]
struct NoopRequestInterceptor;

impl RequestInterceptor for NoopRequestInterceptor {
    fn intercept(&self, request: Request) -> Request {
        request
    }

    fn clone_box(&self) -> Box<dyn RequestInterceptor> {
        Box::new(self.clone())
    }
}

#[derive(Clone)]
struct NoopResponseInterceptor;

impl ResponseInterceptor for NoopResponseInterceptor {
    fn intercept(&self, response: Response) -> Response {
        response
    }

    fn clone_box(&self) -> Box<dyn ResponseInterceptor> {
        Box::new(self.clone())
    }
}

#[derive(Clone)]
struct NoopMiddleware;

impl MiddlewareLayer for NoopMiddleware {
    fn call(
        &self,
        req: Request,
        next: BoxedNext,
    ) -> Pin<Box<dyn Future<Output = Response> + Send + 'static>> {
        Box::pin(async move { next(req).await })
    }

    fn clone_box(&self) -> Box<dyn MiddlewareLayer> {
        Box::new(self.clone())
    }
}

#[derive(Clone)]
struct Scenario {
    name: &'static str,
    path_kind: &'static str,
    features: &'static str,
    router: Arc<rustapi_core::Router>,
    layers: Arc<rustapi_core::middleware::LayerStack>,
    interceptors: Arc<rustapi_core::InterceptorChain>,
}

#[derive(Debug, Clone)]
struct ScenarioResult {
    name: &'static str,
    path_kind: &'static str,
    features: &'static str,
    throughput_req_s: f64,
    p50_us: f64,
    p95_us: f64,
    p99_us: f64,
    mean_us: f64,
}

fn scenario(
    name: &'static str,
    path_kind: &'static str,
    features: &'static str,
    app: RustApi,
) -> Scenario {
    let layers = app.layers().clone();
    let interceptors = app.interceptors().clone();
    let router = app.into_router();

    Scenario {
        name,
        path_kind,
        features,
        router: Arc::new(router),
        layers: Arc::new(layers),
        interceptors: Arc::new(interceptors),
    }
}

fn build_request(state: Arc<Extensions>) -> Request {
    let req = http::Request::builder()
        .method(Method::GET)
        .uri("/hello")
        .body(())
        .expect("request build should succeed");
    let (parts, _) = req.into_parts();

    Request::new(
        parts,
        BodyVariant::Buffered(Bytes::new()),
        state,
        PathParams::new(),
    )
}

async fn route_request_direct(
    router: &rustapi_core::Router,
    request: Request,
    path: &str,
    method: &Method,
) -> Response {
    match router.match_route(path, method) {
        RouteMatch::Found { handler, .. } => handler(request).await,
        RouteMatch::NotFound => rustapi_core::ApiError::not_found("Not found").into_response(),
        RouteMatch::MethodNotAllowed { allowed } => {
            let allowed_str: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect();
            let mut response = rustapi_core::ApiError::new(
                StatusCode::METHOD_NOT_ALLOWED,
                "method_not_allowed",
                "Method not allowed",
            )
            .into_response();
            response.headers_mut().insert(
                http::header::ALLOW,
                allowed_str
                    .join(", ")
                    .parse()
                    .expect("allow header should parse"),
            );
            response
        }
    }
}

async fn execute_scenario_request(scenario: &Scenario) -> Response {
    let method = Method::GET;
    let path = "/hello";
    let request = build_request(scenario.router.state_ref());

    if scenario.layers.is_empty() && scenario.interceptors.is_empty() {
        route_request_direct(&scenario.router, request, path, &method).await
    } else if scenario.layers.is_empty() {
        let request = scenario.interceptors.intercept_request(request);
        let response = route_request_direct(&scenario.router, request, path, &method).await;
        scenario.interceptors.intercept_response(response)
    } else {
        let request = scenario.interceptors.intercept_request(request);
        let router = scenario.router.clone();
        let path = path.to_string();
        let method = method.clone();

        let final_handler: BoxedNext = Arc::new(move |req: Request| {
            let router = router.clone();
            let path = path.clone();
            let method = method.clone();
            Box::pin(async move { route_request_direct(&router, req, &path, &method).await })
                as Pin<Box<dyn Future<Output = Response> + Send + 'static>>
        });

        let response = scenario.layers.execute(request, final_handler).await;
        scenario.interceptors.intercept_response(response)
    }
}

async fn measure_scenario(
    scenario: &Scenario,
    warmup_iters: usize,
    sample_iters: usize,
) -> ScenarioResult {
    for _ in 0..warmup_iters {
        let response = execute_scenario_request(scenario).await;
        std::hint::black_box(response.status());
    }

    let mut latencies_ns = Vec::with_capacity(sample_iters);
    let wall_clock_start = Instant::now();

    for _ in 0..sample_iters {
        let request_start = Instant::now();
        let response = execute_scenario_request(scenario).await;
        let elapsed = request_start.elapsed();

        assert_eq!(
            response.status(),
            StatusCode::OK,
            "benchmark scenario must stay healthy"
        );

        latencies_ns.push(elapsed.as_nanos() as u64);
        std::hint::black_box(response.status());
    }

    let wall_clock_elapsed = wall_clock_start.elapsed();
    latencies_ns.sort_unstable();

    let total_ns: u128 = latencies_ns.iter().map(|&v| v as u128).sum();
    let mean_ns = total_ns as f64 / sample_iters as f64;

    ScenarioResult {
        name: scenario.name,
        path_kind: scenario.path_kind,
        features: scenario.features,
        throughput_req_s: sample_iters as f64 / wall_clock_elapsed.as_secs_f64(),
        p50_us: percentile_us(&latencies_ns, 50.0),
        p95_us: percentile_us(&latencies_ns, 95.0),
        p99_us: percentile_us(&latencies_ns, 99.0),
        mean_us: mean_ns / 1_000.0,
    }
}

fn percentile_us(sorted_latencies_ns: &[u64], percentile: f64) -> f64 {
    if sorted_latencies_ns.is_empty() {
        return 0.0;
    }

    let max_index = sorted_latencies_ns.len() - 1;
    let rank = ((percentile / 100.0) * max_index as f64).round() as usize;
    sorted_latencies_ns[rank.min(max_index)] as f64 / 1_000.0
}

fn parse_env_usize(name: &str, default: usize) -> usize {
    std::env::var(name)
        .ok()
        .and_then(|value| value.parse::<usize>().ok())
        .filter(|value| *value > 0)
        .unwrap_or(default)
}

fn print_results(results: &[ScenarioResult]) {
    println!("# RustAPI Performance Snapshot");
    println!();
    println!("Synthetic in-process request pipeline benchmark.");
    println!();
    println!(
        "| Scenario | Execution path | Features | Req/s | Mean (µs) | p50 (µs) | p95 (µs) | p99 (µs) |"
    );
    println!("|---|---|---|---:|---:|---:|---:|---:|");

    for result in results {
        println!(
            "| {} | {} | {} | {:.0} | {:.2} | {:.2} | {:.2} | {:.2} |",
            result.name,
            result.path_kind,
            result.features,
            result.throughput_req_s,
            result.mean_us,
            result.p50_us,
            result.p95_us,
            result.p99_us,
        );
    }

    println!();
    if let Some(baseline) = results.iter().find(|result| result.name == "baseline") {
        println!("## Relative overhead vs baseline");
        println!();
        println!("| Scenario | Req/s delta | p99 delta |",);
        println!("|---|---:|---:|");
        for result in results {
            let req_delta = ((result.throughput_req_s / baseline.throughput_req_s) - 1.0) * 100.0;
            let p99_delta = ((result.p99_us / baseline.p99_us) - 1.0) * 100.0;
            println!(
                "| {} | {:+.2}% | {:+.2}% |",
                result.name, req_delta, p99_delta,
            );
        }
    }
}

async fn hello() -> &'static str {
    "ok"
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let warmup_iters = parse_env_usize("RUSTAPI_PERF_WARMUP", DEFAULT_WARMUP_ITERS);
    let sample_iters = parse_env_usize("RUSTAPI_PERF_ITERS", DEFAULT_SAMPLE_ITERS);

    let scenarios = vec![
        scenario(
            "baseline",
            "ultra fast",
            "no middleware, no interceptors",
            RustApi::new().route("/hello", get(hello)),
        ),
        scenario(
            "request_interceptor",
            "fast",
            "1 request interceptor",
            RustApi::new()
                .request_interceptor(NoopRequestInterceptor)
                .route("/hello", get(hello)),
        ),
        scenario(
            "request_response_interceptors",
            "fast",
            "1 request + 1 response interceptor",
            RustApi::new()
                .request_interceptor(NoopRequestInterceptor)
                .response_interceptor(NoopResponseInterceptor)
                .route("/hello", get(hello)),
        ),
        scenario(
            "middleware_only",
            "full",
            "1 middleware layer",
            RustApi::new()
                .layer(NoopMiddleware)
                .route("/hello", get(hello)),
        ),
        scenario(
            "full_stack_minimal",
            "full",
            "1 middleware + 1 request + 1 response interceptor",
            RustApi::new()
                .layer(NoopMiddleware)
                .request_interceptor(NoopRequestInterceptor)
                .response_interceptor(NoopResponseInterceptor)
                .route("/hello", get(hello)),
        ),
        scenario(
            "request_id_layer",
            "full",
            "RequestIdLayer",
            RustApi::new()
                .layer(rustapi_core::RequestIdLayer::new())
                .route("/hello", get(hello)),
        ),
    ];

    println!("Warmup iterations: {}", warmup_iters);
    println!("Measured iterations: {}", sample_iters);
    println!();

    let mut results = Vec::with_capacity(scenarios.len());
    for scenario in &scenarios {
        println!("Running scenario: {}", scenario.name);
        results.push(measure_scenario(scenario, warmup_iters, sample_iters).await);
    }

    println!();
    print_results(&results);

    Ok(())
}