waygate 0.1.0

A modern HTTP client for Rust focusing on reliability, streaming, and extensibility.
Documentation
//! waygate - modern HTTP client for Rust.
//!
//! This crate is under active development. See `plan.md` for the roadmap and design goals.

/// Client implementation and user-facing API.
pub mod client {
    //! HTTP client entry point and high-level API.

    use http::{Method, Uri};

    use crate::{
        backend::{Backend, MockBackend},
        config::ClientConfig,
        error::Error,
        request::Request,
        response::Response,
    };

    /// Primary HTTP client type.
    #[derive(Clone, Debug)]
    pub struct Client<B = MockBackend> {
        backend: B,
        config: ClientConfig,
    }

    impl Client {
        /// Create a new client with the default backend and configuration.
        pub fn new() -> Self {
            Self::with_backend(MockBackend::default())
        }
    }

    impl<B> Client<B>
    where
        B: Backend + Clone,
    {
        /// Create a new client using the given backend.
        pub fn with_backend(backend: B) -> Self {
            Self {
                backend,
                config: ClientConfig::default(),
            }
        }

        /// Access the current configuration.
        pub fn config(&self) -> &ClientConfig {
            &self.config
        }

        /// Mutably access the configuration.
        pub fn config_mut(&mut self) -> &mut ClientConfig {
            &mut self.config
        }

        /// Send an HTTP request using this client.
        pub async fn send(&self, request: Request) -> Result<Response, Error> {
            self.backend.execute(request, &self.config).await
        }

        /// Convenience method for performing a GET request to the given URI.
        pub async fn get(&self, uri: Uri) -> Result<Response, Error> {
            let request = Request::new(Method::GET, uri);
            self.send(request).await
        }
    }
}

/// HTTP request types.
pub mod request {
    //! Types representing HTTP requests.

    use http::{HeaderMap, Method, Uri};

    /// Basic HTTP request type used by waygate.
    #[derive(Debug, Clone)]
    pub struct Request {
        /// HTTP method (GET, POST, etc.).
        pub method: Method,
        /// Request URI.
        pub uri: Uri,
        /// Request headers.
        pub headers: HeaderMap,
        /// Request body as bytes (streaming support will be added later).
        pub body: Vec<u8>,
    }

    impl Request {
        /// Create a new request with an empty body and no headers.
        pub fn new(method: Method, uri: Uri) -> Self {
            Self {
                method,
                uri,
                headers: HeaderMap::new(),
                body: Vec::new(),
            }
        }

        /// Create a new request with the given body.
        pub fn with_body(method: Method, uri: Uri, body: Vec<u8>) -> Self {
            Self {
                method,
                uri,
                headers: HeaderMap::new(),
                body,
            }
        }
    }
}

/// HTTP response types.
pub mod response {
    //! Types representing HTTP responses.

    use http::{HeaderMap, StatusCode};

    /// Basic HTTP response type used by waygate.
    #[derive(Debug, Clone)]
    pub struct Response {
        /// HTTP status code.
        pub status: StatusCode,
        /// Response headers.
        pub headers: HeaderMap,
        /// Response body as bytes (streaming support will be added later).
        pub body: Vec<u8>,
    }

    impl Response {
        /// Create a new response from the given parts.
        pub fn new(status: StatusCode, headers: HeaderMap, body: Vec<u8>) -> Self {
            Self { status, headers, body }
        }

        /// Returns true if the status code indicates success (2xx).
        pub fn is_success(&self) -> bool {
            self.status.is_success()
        }
    }
}

/// Backend abstraction used by the client.
pub mod backend {
    //! Traits and types for pluggable HTTP backends.

    use async_trait::async_trait;
    use http::{HeaderMap, StatusCode};
    use std::fmt;
    use hyper::{
        body::{to_bytes, Body as HyperBody},
        client::HttpConnector,
        Client as HyperClient,
    };

    use crate::{config::ClientConfig, error::Error, request::Request, response::Response};

    /// Trait implemented by HTTP backends used by `Client`.
    #[async_trait]
    pub trait Backend: Send + Sync {
        /// Execute the given request using the provided configuration.
        async fn execute(&self, request: Request, config: &ClientConfig) -> Result<Response, Error>;
    }

    /// A simple in-memory backend used for testing and initial bring-up.
    ///
    /// This backend does not perform real network I/O. It returns a fixed
    /// `501 Not Implemented` response.
    #[derive(Debug, Clone, Default)]
    pub struct MockBackend;

    #[async_trait]
    impl Backend for MockBackend {
        async fn execute(&self, _request: Request, _config: &ClientConfig) -> Result<Response, Error> {
            Ok(Response::new(
                StatusCode::NOT_IMPLEMENTED,
                HeaderMap::new(),
                Vec::new(),
            ))
        }
    }

    pub struct HyperBackend {
        client: HyperClient<HttpConnector, HyperBody>,
    }

    impl HyperBackend {
        pub fn new() -> Self {
            let client = HyperClient::new();
            Self { client }
        }
    }

    impl Clone for HyperBackend {
        fn clone(&self) -> Self {
            Self {
                client: self.client.clone(),
            }
        }
    }

    impl fmt::Debug for HyperBackend {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.debug_struct("HyperBackend").finish()
        }
    }

    #[async_trait]
    impl Backend for HyperBackend {
        async fn execute(&self, request: Request, _config: &ClientConfig) -> Result<Response, Error> {
            let mut req = http::Request::new(HyperBody::from(request.body));
            *req.method_mut() = request.method;
            *req.uri_mut() = request.uri;
            *req.headers_mut() = request.headers;

            let res = self
                .client
                .request(req)
                .await
                .map_err(|e| Error::Transport(e.to_string()))?;

            let status = res.status();
            let headers = res.headers().clone();
            let body_bytes = to_bytes(res.into_body())
                .await
                .map_err(|e| Error::Transport(e.to_string()))?
                .to_vec();

            Ok(Response::new(status, headers, body_bytes))
        }
    }
}

pub type MockClient = client::Client<backend::MockBackend>;
pub type HyperClient = client::Client<backend::HyperBackend>;

/// Error types for waygate.
pub mod error {
    //! Unified error type for the library.

    use thiserror::Error;

    /// Top-level error type used by waygate.
    #[derive(Debug, Error)]
    pub enum Error {
        /// The request was invalid (e.g., malformed URI).
        #[error("invalid request: {0}")]
        InvalidRequest(String),

        /// Underlying transport error.
        #[error("transport error: {0}")]
        Transport(String),

        /// Operation timed out.
        #[error("timeout")]
        Timeout,

        /// Misconfiguration detected.
        #[error("configuration error: {0}")]
        Config(String),

        /// Any other error not covered by more specific variants.
        #[error("unexpected error: {0}")]
        Other(String),
    }
}

/// Configuration types for the client.
pub mod config {
    //! Configuration structures for the client and requests.

    use std::time::Duration;

    /// Configuration for the HTTP client.
    #[derive(Debug, Clone)]
    pub struct ClientConfig {
        /// Optional connection timeout.
        pub connect_timeout: Option<Duration>,
        /// Optional overall request timeout.
        pub request_timeout: Option<Duration>,
        /// Maximum number of redirects to follow.
        pub max_redirects: u32,
    }

    impl Default for ClientConfig {
        fn default() -> Self {
            Self {
                connect_timeout: Some(Duration::from_secs(10)),
                request_timeout: Some(Duration::from_secs(30)),
                max_redirects: 5,
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::client::Client;
    use http::Uri;

    #[tokio::test]
    async fn client_get_uses_mock_backend() {
        let client = Client::new();
        let uri: Uri = "http://example.com".parse().expect("valid URI");
        let response = client.get(uri).await.expect("request should succeed");
        assert_eq!(response.status.as_u16(), 501);
    }
}