apisdk 0.0.4-beta.3

A highlevel API client framework for Rust.
Documentation
use std::sync::Arc;

use crate::{
    ApiError, ApiResult, ApiRouter, ApiSignature, Client, ClientBuilder, Initialiser, IntoUrl,
    LogConfig, LogMiddleware, Method, Middleware, OriginalEndpoint, RequestBuilder,
    RequestTraceIdMiddleware, RewriteHost, RewriteHostMiddleware, SignatureMiddleware, Url,
};

/// This struct is used to build an instance of ApiCore
pub struct ApiBuilder {
    /// Reqwest ClientBuilder
    client: ClientBuilder,
    /// Base url for target api
    base_url: Url,
    /// The holder of ApiRouter
    router: Option<Arc<dyn ApiRouter>>,
    /// The holder of ApiSignature
    signature: Option<Arc<dyn ApiSignature>>,
    /// The holder of LogConfig
    logger: Option<Arc<LogConfig>>,
    /// The initialisers for Reqwest
    initialisers: Vec<Arc<dyn Initialiser>>,
    /// The middlewares for Reqwest
    middlewares: Vec<Arc<dyn Middleware>>,
}

impl ApiBuilder {
    /// Create an instance of ApiBuilder
    /// - base_url: base url for target api
    pub fn new(base_url: impl IntoUrl + std::fmt::Debug) -> ApiResult<Self> {
        Ok(Self {
            client: ClientBuilder::default(),
            base_url: base_url.into_url().map_err(ApiError::InvalidUrl)?,
            router: None,
            signature: None,
            logger: None,
            initialisers: vec![],
            middlewares: vec![],
        })
    }

    /// Set the ClientBuilder to create Client instance of Reqwest
    /// - client: Reqwest ClientBuilder
    pub fn with_client(self, client: ClientBuilder) -> Self {
        Self { client, ..self }
    }

    /// Set the ApiRouter
    /// - router: ApiRouter
    pub fn with_router<T>(self, router: T) -> Self
    where
        T: ApiRouter,
    {
        Self {
            router: Some(Arc::new(router)),
            ..self
        }
    }

    /// Set the ApiSignature
    /// - signature: ApiSignature
    pub fn with_signature<T>(self, signature: T) -> Self
    where
        T: ApiSignature,
    {
        Self {
            signature: Some(Arc::new(signature)),
            ..self
        }
    }

    /// Set the LogConfig
    /// - logger: LogConfig
    pub fn with_logger<T>(self, logger: T) -> Self
    where
        T: Into<LogConfig>,
    {
        Self {
            logger: Some(Arc::new(logger.into())),
            ..self
        }
    }

    /// Add initialiser
    /// - initialiser: Reqwest Initialiser
    pub fn with_initialiser<T>(self, initialiser: T) -> Self
    where
        T: Initialiser,
    {
        let mut s = self;
        s.initialisers.push(Arc::new(initialiser));
        s
    }

    /// Add middleware
    /// - middleware: Reqwest Middleware
    pub fn with_middleware<T>(self, middleware: T) -> Self
    where
        T: Middleware,
    {
        let mut s = self;
        s.middlewares.push(Arc::new(middleware));
        s
    }

    /// Build an instance of ApiCore
    pub fn build(self) -> ApiCore {
        let mut client = reqwest_middleware::ClientBuilder::new(self.client.build().unwrap());

        // Apply middleware in correct order
        client = client.with(RequestTraceIdMiddleware);
        client = client.with(RewriteHostMiddleware);
        for middleware in self.middlewares {
            client = client.with_arc(middleware);
        }
        if self.signature.is_some() {
            client = client.with(SignatureMiddleware);
        }
        if self.logger.is_some() {
            client = client.with(LogMiddleware);
        }

        // Apply initialisers
        if let Some(logger) = self.logger {
            client = client.with_arc_init(logger);
        };
        for initialiser in self.initialisers {
            client = client.with_arc_init(initialiser);
        }

        ApiCore {
            client: client.build(),
            base_url: self.base_url,
            router: self.router,
            signature: self.signature,
        }
    }
}

/// This struct is used to create HTTP request
pub struct ApiCore {
    /// Reqwest Client
    client: Client,
    /// Base url for target api
    base_url: Url,
    /// The holder of ApiRouter
    router: Option<Arc<dyn ApiRouter>>,
    /// The holder of ApiSignature
    signature: Option<Arc<dyn ApiSignature>>,
}

impl std::fmt::Debug for ApiCore {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut d = f.debug_struct("ApiCore");
        let mut d = d
            .field("client", &self.client)
            .field("base_url", &self.base_url);
        if let Some(r) = self.router.as_ref() {
            d = d.field("router", &r.type_name());
        }
        if let Some(s) = self.signature.as_ref() {
            d = d.field("signature", &s.type_name());
        }
        d.finish()
    }
}

impl ApiCore {
    pub fn rebase(&self, base_url: impl IntoUrl) -> ApiResult<Self> {
        Ok(Self {
            client: self.client.clone(),
            base_url: base_url.into_url().map_err(ApiError::InvalidUrl)?,
            router: self.router.clone(),
            signature: self.signature.clone(),
        })
    }

    /// Build a new request url
    /// - path: relative path to base_uri
    ///
    /// Return error when failed to retrieve valid endpoint from ApiRouter
    pub async fn build_url(&self, path: impl AsRef<str>) -> ApiResult<(Url, bool)> {
        let endpoint = match self.router.as_ref() {
            Some(router) => router.next_endpoint().await?,
            None => Box::new(OriginalEndpoint::default()),
        };
        endpoint
            .build_url(&self.base_url, path.as_ref())
            .map(|url| (url, endpoint.reserve_original_host()))
            .map_err(|e| e.into())
    }

    /// Build a new HTTP request
    /// - method: HTTP method
    /// - path: relative path to base_uri
    pub async fn build_request(
        &self,
        method: Method,
        path: impl AsRef<str>,
    ) -> ApiResult<RequestBuilder> {
        let (url, reserve_original_host) = self.build_url(path).await?;
        let mut req = self.client.request(method, url);

        // Keep original HOST if required
        if reserve_original_host {
            if let Some(host) = self.base_url.host_str() {
                req = req.with_extension(RewriteHost::new(host));
            }
        }

        match self.signature.clone() {
            Some(signature) => Ok(req.with_extension(signature)),
            None => Ok(req),
        }
    }
}