base-sensible-wrapper 0.1.0

A simple api wrapper with sane defaults
Documentation
use super::interface::ApiInterface;
use anyhow::{Context, Result};
use reqwest::{
    Method,
    header::{HeaderName, HeaderValue},
};
use reqwest_middleware::ClientWithMiddleware;
use serde::{Serialize, de::DeserializeOwned};
use std::collections::HashMap;
use std::fmt::Display;

/// The wrapper around an api definition that handles building and making requests correctly.
/// Internally stores a copy of the current reqwest client, the global config, and the
/// api interface to use. Can be safely sent across threads thanks to reqwest's internal
/// architecture. Note that interfaces impl Send + Sync by default as well.
pub struct ApiClient<T: ApiInterface> {
    pub(super) interface: T,
    pub(super) http_client: ClientWithMiddleware,
}
unsafe impl<T: ApiInterface> Send for ApiClient<T> {}
unsafe impl<T: ApiInterface> Sync for ApiClient<T> {}

impl<T: ApiInterface> ApiClient<T> {
    /// This member builds and executes a new request based on params passed in. It will call
    /// the underlying interface for the base api url and authentication methods, and collect
    /// all relevant info before executing. Return type is determined by the callee.
    pub async fn request<
        R: DeserializeOwned,
        E: Display,
        HK: Into<HeaderName>,
        HV: Into<HeaderValue>,
        P: Serialize,
        B: Serialize,
    >(
        &self,
        endpoint: E,
        method: Method,
        headers: HashMap<HK, HV>,
        params: Option<P>,
        body: Option<B>,
    ) -> Result<R> {
        let request = self.interface.build_request(
            self.http_client.clone(),
            endpoint,
            method,
            headers,
            params,
            body,
        )?;
        trace!("Sending request for json response: {request:?}");

        self.http_client
            .execute(request)
            .await
            .context("(JSON) Cannot send request")?
            .json()
            .await
            .context("(JSON) Cannot decode request")
    }

    /// A debug version of `Self::request` that returns the body as text instead of deserializing
    /// it. Useful if there is an issue with the call
    pub async fn string_request<
        E: Display,
        HK: Into<HeaderName>,
        HV: Into<HeaderValue>,
        P: Serialize,
        B: Serialize,
    >(
        &self,
        endpoint: E,
        method: Method,
        headers: HashMap<HK, HV>,
        params: Option<P>,
        body: Option<B>,
    ) -> Result<String> {
        let request = self.interface.build_request(
            self.http_client.clone(),
            endpoint,
            method,
            headers,
            params,
            body,
        )?;
        trace!("Sending request for text response: {request:?}");

        let response = self
            .http_client
            .execute(request)
            .await
            .context("(TEXT) Cannot send request")?;

        trace!("Response for text request: {response:?}");

        response
            .text()
            .await
            .context("(TEXT) Cannot decode request")
    }

    /// A simple get method. Auto-sets specific params.
    pub async fn simple_get<R: DeserializeOwned, E: Display>(&self, endpoint: E) -> Result<R> {
        self.request(
            endpoint,
            Method::GET,
            HashMap::<HeaderName, HeaderValue>::new(),
            None::<()>,
            None::<()>,
        )
        .await
    }
    /// A simple get method that allows also sending query params
    pub async fn get_params<R: DeserializeOwned, E: Display, P: Serialize>(
        &self,
        endpoint: E,
        params: P,
    ) -> Result<R> {
        self.request(
            endpoint,
            Method::GET,
            HashMap::<HeaderName, HeaderValue>::new(),
            Some(params),
            None::<()>,
        )
        .await
    }

    /// A simple post method that requires body
    pub async fn simple_post<R: DeserializeOwned, E: Display, B: Serialize>(
        &self,
        endpoint: E,
        body: B,
    ) -> Result<R> {
        self.request(
            endpoint,
            Method::POST,
            HashMap::<HeaderName, HeaderValue>::new(),
            None::<()>,
            Some(body),
        )
        .await
    }
    /// A simple post method that requires body and allows optional query params
    pub async fn post_params<R: DeserializeOwned, E: Display, P: Serialize, B: Serialize>(
        &self,
        endpoint: E,
        params: P,
        body: B,
    ) -> Result<R> {
        self.request(
            endpoint,
            Method::POST,
            HashMap::<HeaderName, HeaderValue>::new(),
            Some(params),
            Some(body),
        )
        .await
    }
}