indieweb 0.10.0

A collection of utilities for working with the IndieWeb.
Documentation
pub use ::http::{Request, Response};
use http::header::CONTENT_TYPE;

use crate::Error;

/// Represents the value for HTTP headers to show a form-encoded response.
pub static CONTENT_TYPE_FORM_URLENCODED: &str = "application/x-www-form-urlencoded";

/// Represents the value for HTTP headers to show a JSON-encoded response.
pub static CONTENT_TYPE_JSON: &str = "application/json";

#[derive(Clone)]
#[derive(Default)]
pub enum Body {
    Bytes(Vec<u8>),
    #[default]
    Empty,
}

impl std::fmt::Debug for Body {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Bytes(bytes) => f.debug_tuple("Bytes").field(&bytes.len()).finish(),
            Self::Empty => write!(f, "no bytes"),
        }
    }
}


impl From<Vec<u8>> for Body {
    fn from(value: Vec<u8>) -> Self {
        Self::Bytes(value)
    }
}

impl From<String> for Body {
    fn from(value: String) -> Self {
        value.into_bytes().into()
    }
}

impl Body {
    pub fn as_bytes(&self) -> &[u8] {
        match self {
            Self::Empty => &[],
            Self::Bytes(b) => b,
        }
    }

    pub fn text(&self) -> Result<String, Error> {
        String::from_utf8(self.as_bytes().to_vec()).map_err(Error::FromUTF8)
    }
}

pub(crate) fn from_json_value<V: serde::de::DeserializeOwned>(
    response: Response<Body>,
) -> Result<V, Error> {
    let ct_header = String::from_utf8(
        response
            .headers()
            .get(CONTENT_TYPE)
            .map(|hv| hv.as_bytes())
            .unwrap_or_default()
            .to_vec(),
    )?;

    if ct_header.starts_with(CONTENT_TYPE_JSON) {
        Ok(serde_json::from_slice(response.into_body().as_bytes())?)
    } else {
        Err(Error::ResponseNotJson(ct_header))
    }
}

#[async_trait::async_trait]
pub trait Client: Send + Sync {
    async fn send_request(&self, request: Request<Body>) -> Result<Response<Body>, Error>;
}

#[cfg(feature = "reqwest")]
pub mod reqwest {
    use std::time::Duration;

    pub(super) fn into_local_request(
        request: ::http::Request<super::Body>,
    ) -> Result<reqwest::Request, crate::Error> {
        let method = reqwest::Method::from_bytes(request.method().as_str().as_bytes())
            .map_err(crate::Error::ReqwestMethod)?;
        let url = request.uri().to_string().parse()?;
        let mut req = reqwest::Request::new(method, url);

        for (name, value) in request.headers() {
            req.headers_mut().insert(name, value.clone());
        }

        let _ = req
            .body_mut()
            .insert(request.into_body().as_bytes().to_vec().into());

        Ok(req)
    }

    pub(super) async fn into_local_response(
        response: reqwest::Response,
    ) -> Result<::http::Response<super::Body>, crate::Error> {
        let mut resp = ::http::Response::builder().status(response.status().as_u16());

        for (name, value) in response.headers() {
            resp = resp.header(name, value);
        }

        let body = response
            .bytes()
            .await
            .map_err(crate::Error::from)?
            .to_vec()
            .into();
        resp.body(body).map_err(crate::Error::Http)
    }

    pub struct Client(::reqwest::Client);

    impl std::default::Default for Client {
        fn default() -> Self {
            Self(
                ::reqwest::Client::builder()
                    .timeout(Duration::from_secs(5))
                    .build()
                    .expect("failed to build a http client"),
            )
        }
    }

    impl From<::reqwest::Client> for Client {
        fn from(client: ::reqwest::Client) -> Self {
            Self(client)
        }
    }

    #[async_trait::async_trait]
    impl super::Client for Client {
        #[tracing::instrument(skip(self))]
        async fn send_request(
            &self,
            request: ::http::Request<super::Body>,
        ) -> Result<::http::Response<super::Body>, crate::Error> {
            let local_request = into_local_request(request)?;
            let resp = self.0.execute(local_request).await;

            match resp {
                Ok(resp) => into_local_response(resp).await,
                Err(err) => Err(Box::new(err).into()),
            }
        }
    }

    // Re-export for convenience at crate root
    pub use reqwest as reqwest_client;
}

#[cfg(feature = "blocking")]
pub mod blocking {
    use std::time::Duration;

    type ReqwestMethod = ::reqwest::Method;
    type ReqwestRequest = ::reqwest::blocking::Request;
    type ReqwestResponse = ::reqwest::blocking::Response;
    type ReqwestClient = ::reqwest::blocking::Client;

    fn into_local_request(
        request: ::http::Request<crate::http::Body>,
    ) -> Result<ReqwestRequest, crate::Error> {
        let method = ReqwestMethod::from_bytes(request.method().as_str().as_bytes())
            .map_err(crate::Error::ReqwestMethod)?;
        let url = request.uri().to_string().parse()?;
        let mut req = ReqwestRequest::new(method, url);

        for (name, value) in request.headers() {
            req.headers_mut().insert(name, value.clone());
        }

        let _ = req
            .body_mut()
            .insert(request.into_body().as_bytes().to_vec().into());

        Ok(req)
    }

    fn into_local_response(
        response: ReqwestResponse,
    ) -> Result<::http::Response<crate::http::Body>, crate::Error> {
        let mut resp = ::http::Response::builder().status(response.status().as_u16());

        for (name, value) in response.headers() {
            resp = resp.header(name, value);
        }

        let body = response
            .bytes()
            .map_err(crate::Error::from)?
            .to_vec()
            .into();
        resp.body(body).map_err(crate::Error::Http)
    }

    pub struct Client(ReqwestClient);

    impl std::default::Default for Client {
        fn default() -> Self {
            Self(
                ReqwestClient::builder()
                    .timeout(Duration::from_secs(5))
                    .build()
                    .expect("failed to build a blocking http client"),
            )
        }
    }

    impl From<ReqwestClient> for Client {
        fn from(client: ReqwestClient) -> Self {
            Self(client)
        }
    }

    #[async_trait::async_trait]
    impl crate::http::Client for Client {
        #[tracing::instrument(skip(self))]
        async fn send_request(
            &self,
            request: ::http::Request<crate::http::Body>,
        ) -> Result<::http::Response<crate::http::Body>, crate::Error> {
            let local_request = into_local_request(request)?;
            let resp = self.0.execute(local_request);

            match resp {
                Ok(resp) => into_local_response(resp),
                Err(err) => Err(Box::new(err).into()),
            }
        }
    }
}

#[cfg(feature = "reqwest_middleware")]
pub mod reqwest_middleware {
    use futures::TryFutureExt;

    #[cfg(feature = "reqwest")]
    use super::reqwest::into_local_request;

    #[cfg(feature = "reqwest")]
    use super::reqwest::into_local_response;

    #[cfg(feature = "reqwest")]
    pub struct MiddlewareClient(::reqwest_middleware::ClientWithMiddleware);

    #[cfg(feature = "reqwest")]
    impl From<::reqwest_middleware::ClientWithMiddleware> for MiddlewareClient {
        fn from(client: ::reqwest_middleware::ClientWithMiddleware) -> Self {
            Self(client)
        }
    }

    #[cfg(feature = "reqwest")]
    #[async_trait::async_trait]
    impl super::Client for MiddlewareClient {
        async fn send_request(
            &self,
            request: ::http::Request<super::Body>,
        ) -> Result<::http::Response<super::Body>, crate::Error> {
            let local_request = into_local_request(request)?;
            self.0
                .execute(local_request)
                .map_err(crate::Error::ReqwestMiddleware)
                .and_then(into_local_response)
                .await
        }
    }
}

/// Middleware client for reqwest with middleware support.
/// Available when `reqwest_middleware` feature is enabled.
#[cfg(feature = "reqwest_middleware")]
pub use reqwest_middleware::MiddlewareClient;