openidauthzen 0.1.0-alpha.1

OpenID AuthZEN Authorization API 1.0 — Policy Decision and Enforcement Points for Rust
Documentation
//! HTTP abstraction layer for [`crate::client::AuthZenClient`].
//!
//! Consumers that need custom transport (e.g. mTLS, retries, middleware)
//! can implement [`HttpClient`] directly. When the `reqwest` feature is
//! enabled, [`ReqwestClient`] provides a ready-made implementation.

use crate::error::Error;

/// Raw HTTP response returned by an [`HttpClient`] implementation.
#[derive(Debug, Clone)]
pub struct HttpResponse {
    /// HTTP status code (e.g. `200`, `400`).
    pub status: u16,
    /// Response headers as name-value pairs (lowercased names).
    pub headers: Vec<(String, String)>,
    /// Response body as raw bytes.
    pub body: Vec<u8>,
}

/// HTTP method for a request.
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum Method {
    /// HTTP GET.
    Get,
    /// HTTP POST.
    Post,
}

/// Trait abstracting HTTP transport for the AuthZEN client.
///
/// Implement this to plug in your own HTTP stack. The
/// [`crate::client::AuthZenClient`] is generic over `C: HttpClient`,
/// so any implementation works — reqwest, hyper, a mock for testing, etc.
#[async_trait::async_trait]
pub trait HttpClient: Send + Sync {
    /// Send an HTTP request and return the raw response.
    ///
    /// Implementations MUST:
    /// - Apply all entries from `headers` as request headers.
    /// - Send `body` as the request payload when `Some`.
    ///
    /// Implementations SHOULD NOT interpret status codes — the caller
    /// ([`crate::client::AuthZenClient`]) handles error status mapping.
    async fn request(
        &self,
        method: Method,
        url: &str,
        headers: &[(&str, &str)],
        body: Option<Vec<u8>>,
    ) -> Result<HttpResponse, Error>;
}

/// [`HttpClient`] implementation backed by [`reqwest`].
///
/// Available when the `reqwest` feature is enabled. Automatically sets
/// `Content-Type: application/json` when a body is present.
#[cfg(feature = "reqwest")]
pub struct ReqwestClient {
    inner: reqwest::Client,
}

#[cfg(feature = "reqwest")]
impl ReqwestClient {
    /// Create a new client with the given request timeout.
    pub fn new(timeout: std::time::Duration) -> Result<Self, Error> {
        let inner =
            reqwest::Client::builder().timeout(timeout).build().map_err(Error::HttpClient)?;
        Ok(Self { inner })
    }
}

#[cfg(feature = "reqwest")]
#[async_trait::async_trait]
impl HttpClient for ReqwestClient {
    async fn request(
        &self,
        method: Method,
        url: &str,
        headers: &[(&str, &str)],
        body: Option<Vec<u8>>,
    ) -> Result<HttpResponse, Error> {
        let mut builder = match method {
            Method::Get => self.inner.get(url),
            Method::Post => self.inner.post(url),
        };

        for (key, value) in headers {
            builder = builder.header(*key, *value);
        }

        if body.is_some() {
            builder = builder.header("content-type", "application/json");
        }

        if let Some(body) = body {
            builder = builder.body(body);
        }

        let resp = builder.send().await.map_err(|e| Error::Http(Box::new(e)))?;

        let status = resp.status().as_u16();
        let headers = resp
            .headers()
            .iter()
            .map(|(k, v)| (k.as_str().to_owned(), v.to_str().unwrap_or("").to_owned()))
            .collect();
        let body = resp.bytes().await.map_err(|e| Error::Http(Box::new(e)))?.to_vec();

        Ok(HttpResponse { status, headers, body })
    }
}