spikard-http 0.15.3

High-performance HTTP server for Spikard with tower-http middleware stack
Documentation
#![cfg(feature = "di")]

use axum::body::{Body, to_bytes};
use axum::http::{Request, StatusCode};
use spikard_core::di::{Dependency, DependencyContainer, DependencyError, ResolvedDependencies};
use spikard_http::di_handler::DependencyInjectingHandler;
use spikard_http::handler_trait::{Handler, HandlerResult, RequestData};
use std::any::Any;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

struct OkHandler;

impl Handler for OkHandler {
    fn call(
        &self,
        _request: Request<Body>,
        _request_data: RequestData,
    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
        Box::pin(async move { Ok(axum::http::Response::new(Body::empty())) })
    }
}

fn minimal_request_data() -> RequestData {
    RequestData {
        path_params: Arc::new(HashMap::new()),
        query_params: Arc::new(serde_json::json!({})),
        validated_params: None,
        raw_query_params: Arc::new(HashMap::new()),
        body: Arc::new(serde_json::Value::Null),
        raw_body: None,
        headers: Arc::new(HashMap::new()),
        cookies: Arc::new(HashMap::new()),
        method: "GET".to_string(),
        path: "/".to_string(),
        #[cfg(feature = "di")]
        dependencies: None,
    }
}

async fn read_json_body(resp: axum::http::Response<Body>) -> serde_json::Value {
    let bytes = to_bytes(resp.into_body(), usize::MAX).await.expect("read body");
    serde_json::from_slice(&bytes).expect("parse json")
}

#[tokio::test]
async fn missing_dependency_returns_structured_500() {
    let container = Arc::new(DependencyContainer::new());
    let handler = DependencyInjectingHandler::new(Arc::new(OkHandler), container, vec!["missing".to_string()]);

    let req = Request::builder().uri("/").body(Body::empty()).unwrap();
    let resp = handler.call(req, minimal_request_data()).await.expect("ok response");

    assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
    let json = read_json_body(resp).await;
    assert_eq!(json["title"], "Dependency Resolution Failed");
    assert_eq!(json["errors"][0]["type"], "missing_dependency");
    assert_eq!(json["errors"][0]["dependency_key"], "missing");
}

#[tokio::test]
async fn circular_dependency_returns_structured_500() {
    struct DependsOn {
        dep_key: String,
        deps: Vec<String>,
    }

    impl Dependency for DependsOn {
        fn resolve(
            &self,
            _request: &axum::http::Request<()>,
            _request_data: &spikard_core::RequestData,
            _resolved: &ResolvedDependencies,
        ) -> Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, DependencyError>> + Send + '_>> {
            Box::pin(async move { Ok(Arc::new(()) as Arc<dyn Any + Send + Sync>) })
        }

        fn key(&self) -> &str {
            &self.dep_key
        }

        fn depends_on(&self) -> Vec<String> {
            self.deps.clone()
        }
    }

    let mut container = DependencyContainer::new();
    container
        .register(
            "a".to_string(),
            Arc::new(DependsOn {
                dep_key: "a".to_string(),
                deps: vec!["b".to_string()],
            }),
        )
        .unwrap();
    container
        .register(
            "b".to_string(),
            Arc::new(DependsOn {
                dep_key: "b".to_string(),
                deps: vec!["a".to_string()],
            }),
        )
        .unwrap();

    let handler = DependencyInjectingHandler::new(Arc::new(OkHandler), Arc::new(container), vec!["a".to_string()]);
    let req = Request::builder().uri("/").body(Body::empty()).unwrap();
    let resp = handler.call(req, minimal_request_data()).await.expect("ok response");

    assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
    let json = read_json_body(resp).await;
    assert_eq!(json["errors"][0]["type"], "circular_dependency");
    assert!(json["errors"][0]["cycle"].is_array());
}

#[tokio::test]
async fn resolution_failed_returns_structured_503() {
    struct FailingDependency;

    impl Dependency for FailingDependency {
        fn resolve(
            &self,
            _request: &axum::http::Request<()>,
            _request_data: &spikard_core::RequestData,
            _resolved: &ResolvedDependencies,
        ) -> Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, DependencyError>> + Send + '_>> {
            Box::pin(async move {
                Err(DependencyError::ResolutionFailed {
                    message: "boom".to_string(),
                })
            })
        }

        fn key(&self) -> &'static str {
            "failing"
        }

        fn depends_on(&self) -> Vec<String> {
            Vec::new()
        }
    }

    let mut container = DependencyContainer::new();
    container
        .register("failing".to_string(), Arc::new(FailingDependency))
        .unwrap();

    let handler =
        DependencyInjectingHandler::new(Arc::new(OkHandler), Arc::new(container), vec!["failing".to_string()]);

    let req = Request::builder().uri("/").body(Body::empty()).unwrap();
    let resp = handler.call(req, minimal_request_data()).await.expect("ok response");

    assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
    let json = read_json_body(resp).await;
    assert_eq!(json["title"], "Service Unavailable");
    assert_eq!(json["errors"][0]["type"], "resolution_failed");
    assert_eq!(json["errors"][0]["msg"], "boom");
}