parcelwing 0.1.0

Official Rust SDK for the Parcel Wing API.
Documentation
use std::collections::HashMap;

use serde::de::DeserializeOwned;
use serde::Serialize;

use crate::error::{ApiError, Error, ErrorPayload};

#[derive(Clone, Debug)]
pub(crate) struct HttpClient {
    api_key: String,
    base_url: String,
    client: reqwest::Client,
    default_headers: HashMap<String, String>,
}

impl HttpClient {
    pub(crate) fn new(
        api_key: String,
        base_url: String,
        client: reqwest::Client,
        default_headers: HashMap<String, String>,
    ) -> Self {
        Self {
            api_key,
            base_url,
            client,
            default_headers,
        }
    }

    pub(crate) async fn get<T>(
        &self,
        path: &str,
        query: Option<Vec<(&str, String)>>,
    ) -> Result<T, Error>
    where
        T: DeserializeOwned,
    {
        let request = self.client.get(self.url(path));
        let request = self.apply_headers(request);

        let request = if let Some(query) = query {
            request.query(&query)
        } else {
            request
        };

        self.send(request).await
    }

    pub(crate) async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
    where
        T: DeserializeOwned,
        B: Serialize + ?Sized,
    {
        let request = self
            .apply_headers(self.client.post(self.url(path)))
            .json(body);
        self.send(request).await
    }

    pub(crate) async fn patch<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
    where
        T: DeserializeOwned,
        B: Serialize + ?Sized,
    {
        let request = self
            .apply_headers(self.client.patch(self.url(path)))
            .json(body);
        self.send(request).await
    }

    pub(crate) async fn delete<T>(&self, path: &str) -> Result<T, Error>
    where
        T: DeserializeOwned,
    {
        let request = self.apply_headers(self.client.delete(self.url(path)));
        self.send(request).await
    }

    async fn send<T>(&self, request: reqwest::RequestBuilder) -> Result<T, Error>
    where
        T: DeserializeOwned,
    {
        let response = request.send().await?;
        let status = response.status();
        let text = response.text().await?;

        if !status.is_success() {
            if let Ok(mut payload) = serde_json::from_str::<ErrorPayload>(&text) {
                payload.error.status = status.as_u16();
                return Err(Error::Api(Box::new(payload.error)));
            }

            if let Ok(mut api_error) = serde_json::from_str::<ApiError>(&text) {
                api_error.status = status.as_u16();
                return Err(Error::Api(Box::new(api_error)));
            }

            return Err(Error::Http {
                status: status.as_u16(),
                body: text,
            });
        }

        serde_json::from_str(&text).map_err(Error::Decode)
    }

    fn apply_headers(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
        let mut request = request
            .header(reqwest::header::ACCEPT, "application/json")
            .header(
                reqwest::header::AUTHORIZATION,
                format!("Bearer {}", self.api_key),
            )
            .header(
                "X-ParcelWing-SDK",
                format!("rust/{}", env!("CARGO_PKG_VERSION")),
            );

        for (key, value) in &self.default_headers {
            request = request.header(key, value);
        }

        request
    }

    fn url(&self, path: &str) -> String {
        format!("{}{}", self.base_url, path)
    }
}