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 rustapi_core::health::HealthEndpointConfig;
use rustapi_core::{get, ProductionDefaultsConfig, RustApi};
use std::time::Duration;
use tokio::sync::oneshot;

#[tokio::test]
async fn test_production_defaults_enable_request_id_and_health_probes() {
    async fn hello() -> &'static str {
        "ok"
    }

    let app = RustApi::new()
        .production_defaults("users-api")
        .route("/hello", get(hello));

    assert_eq!(
        app.layers().len(),
        2,
        "request ID and tracing layers should be installed"
    );

    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    let port = addr.port();
    drop(listener);

    let (tx, rx) = oneshot::channel();
    let addr_str = format!("127.0.0.1:{}", port);

    let server_handle = tokio::spawn(async move {
        app.run_with_shutdown(&addr_str, async {
            rx.await.ok();
        })
        .await
    });

    tokio::time::sleep(Duration::from_millis(200)).await;
    let client = reqwest::Client::new();
    let base_url = format!("http://127.0.0.1:{}", port);

    let res = client
        .get(format!("{}/hello", base_url))
        .send()
        .await
        .expect("hello request failed");
    assert_eq!(res.status(), 200);
    assert!(res.headers().get("x-request-id").is_some());

    let res = client
        .get(format!("{}/health", base_url))
        .send()
        .await
        .expect("health request failed");
    assert_eq!(res.status(), 200);
    let body: serde_json::Value = res.json().await.unwrap();
    assert!(
        body.get("version").is_none(),
        "version should be omitted when no version is configured"
    );
    assert!(body.get("checks").is_some());

    tx.send(()).unwrap();
    let _ = tokio::time::timeout(Duration::from_secs(2), server_handle).await;
}

#[tokio::test]
async fn test_production_defaults_custom_config_applies_version_and_custom_paths() {
    let config = ProductionDefaultsConfig::new("billing-api")
        .version("1.2.3")
        .health_endpoint_config(
            HealthEndpointConfig::new()
                .health_path("/healthz")
                .readiness_path("/readyz")
                .liveness_path("/livez"),
        );

    let app = RustApi::new().production_defaults_with_config(config);

    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    let port = addr.port();
    drop(listener);

    let (tx, rx) = oneshot::channel();
    let addr_str = format!("127.0.0.1:{}", port);

    let server_handle = tokio::spawn(async move {
        app.run_with_shutdown(&addr_str, async {
            rx.await.ok();
        })
        .await
    });

    tokio::time::sleep(Duration::from_millis(200)).await;
    let client = reqwest::Client::new();
    let base_url = format!("http://127.0.0.1:{}", port);

    for path in ["/healthz", "/readyz", "/livez"] {
        let res = client
            .get(format!("{}{}", base_url, path))
            .send()
            .await
            .expect("probe request failed");
        assert_eq!(res.status(), 200);
    }

    let res = client
        .get(format!("{}/healthz", base_url))
        .send()
        .await
        .expect("healthz request failed");
    let body: serde_json::Value = res.json().await.unwrap();
    assert_eq!(
        body.get("version"),
        Some(&serde_json::Value::String("1.2.3".to_string()))
    );

    tx.send(()).unwrap();
    let _ = tokio::time::timeout(Duration::from_secs(2), server_handle).await;
}

#[test]
fn test_production_defaults_can_disable_optional_parts() {
    let app = RustApi::new().production_defaults_with_config(
        ProductionDefaultsConfig::new("minimal-api")
            .request_id(false)
            .tracing(false)
            .health_endpoints(false),
    );

    assert_eq!(app.layers().len(), 0);
}