aep-core 0.1.1

Transport-independent protocol primitives for the Agent Enrollment Protocol.
Documentation
use async_trait::async_trait;
use http::{HeaderMap, Method, StatusCode};
use std::{error::Error as StdError, fmt};

use thiserror::Error;
use url::Url;

#[derive(Clone)]
pub struct HttpRequest {
    pub method: Method,
    pub url: Url,
    pub headers: HeaderMap,
    pub body: Vec<u8>,
}

impl fmt::Debug for HttpRequest {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("HttpRequest")
            .field("method", &self.method)
            .field("url", &"[REDACTED]")
            .field("headers", &"[REDACTED]")
            .field("body", &"[REDACTED]")
            .finish()
    }
}

#[derive(Clone)]
pub struct HttpResponse {
    pub status: StatusCode,
    pub final_url: Url,
    pub headers: HeaderMap,
    pub body: Vec<u8>,
}

impl fmt::Debug for HttpResponse {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("HttpResponse")
            .field("status", &self.status)
            .field("final_url", &"[REDACTED]")
            .field("headers", &"[REDACTED]")
            .field("body", &"[REDACTED]")
            .finish()
    }
}

#[derive(Debug, Error)]
#[error("{message}")]
pub struct TransportError {
    message: String,
    #[source]
    source: Option<Box<dyn StdError + Send + Sync>>,
}

impl TransportError {
    pub fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            source: None,
        }
    }

    pub fn with_source(
        message: impl Into<String>,
        source: impl StdError + Send + Sync + 'static,
    ) -> Self {
        Self {
            message: message.into(),
            source: Some(Box::new(source)),
        }
    }
}

#[async_trait]
pub trait HttpTransport: Send + Sync {
    async fn send(&self, request: HttpRequest) -> Result<HttpResponse, TransportError>;
}

#[cfg(test)]
mod tests {
    use std::fmt;

    use super::*;

    #[derive(Debug)]
    struct ExampleError;

    impl fmt::Display for ExampleError {
        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
            formatter.write_str("cause")
        }
    }

    impl StdError for ExampleError {}

    #[test]
    fn preserves_transport_error_context() {
        assert_eq!(
            TransportError::new("request failed").to_string(),
            "request failed"
        );
        let error = TransportError::with_source("request failed", ExampleError);
        assert_eq!(error.to_string(), "request failed");
        assert_eq!(
            StdError::source(&error).map(ToString::to_string),
            Some("cause".to_owned())
        );
    }

    #[test]
    fn redacts_http_messages_from_debug_output() {
        let request = HttpRequest {
            method: Method::POST,
            url: Url::parse("https://service.example/path?token=secret").expect("URL"),
            headers: HeaderMap::from_iter([(
                http::header::AUTHORIZATION,
                "Bearer secret".parse().expect("header"),
            )]),
            body: b"secret body".to_vec(),
        };
        let response = HttpResponse {
            status: StatusCode::OK,
            final_url: request.url.clone(),
            headers: request.headers.clone(),
            body: request.body.clone(),
        };
        for output in [format!("{request:?}"), format!("{response:?}")] {
            assert!(!output.contains("secret"));
            assert!(output.contains("[REDACTED]"));
        }
    }
}