ubi-rs 0.1.3

A Rust cli and library for ubicloud API
Documentation
use crate::errors::UbiClientError;
use percent_encoding::{NON_ALPHANUMERIC, percent_encode};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone)]
pub struct HTTPClient {
    client: reqwest::Client,
    base_url: reqwest::Url,
    version: String,
}

#[derive(Deserialize, Serialize, Debug, Default)]
pub struct APIError {
    pub code: Option<u16>,
    pub message: Option<String>,

    #[serde(rename = "type")]
    pub _type: Option<String>,
    pub details: Option<String>,
}

#[derive(Deserialize, Serialize, Debug, Default)]
pub struct ResponseAPIError {
    pub error: APIError,
}

/// Percent encode an incoming parameter
pub(crate) fn encode_param(param: &str) -> String {
    percent_encode(param.as_bytes(), NON_ALPHANUMERIC).to_string()
}

/// Make a http request by providing a json-body
#[macro_export]
/// Macro for making a JSON request using the specified HTTP method, URL, and request body.
///
/// # Arguments
///
/// * `$sel`: The selector for the HTTP client.
/// * `$method`: The HTTP method to use for the request.
/// * `$url`: The URL to send the request to.
/// * `$body`: The request body.
/// * `$query`: Optional query parameters to include in the request.
/// * `$response_ty`: The expected response type.
///
/// # Returns
///
/// Returns a `Result` containing the JSON response if the request is successful, or an `APIError` if the request fails.
///
macro_rules! make_json_request {
    ($sel:ident, $method:path, $url:expr, $body:expr, $query:expr, $response_ty:ty) => {{
        use $crate::{client::ResponseAPIError, errors::UbiClientError};
        use tracing::{debug, error};

        debug!(
            method = stringify!($method),
            url = $url,
            body = ?$body,
            "Sending JSON request"
        );

        let response = $sel
            .http_client
            .inner($method, $url)?
            .json(&$body)
            .query(&$query)
            .send()
            .await?;

        let status = response.status();
        if status.as_u16() == 204 {
            return Ok(Default::default());
        }

        if !status.is_success() {
            error!(status = status.as_u16(), url = $url, "Non-success response");

            let value: serde_json::Value = response.json().await?;
            debug!(?value, "Received error response");

            let parsed_error: ResponseAPIError = serde_json::from_value(value)?;

            return Err(UbiClientError::APIResponseError {
                etype: parsed_error.error._type.unwrap_or_default(),
                message: parsed_error.error.message.unwrap_or_default(),
                details: parsed_error.error.details,
            });
        }

        #[cfg(feature = "debug")]
        {
            let json: serde_json::Value = response.json().await?;
            debug!(?json, "Parsed JSON response");
            Ok(serde_json::from_value::<$response_ty>(json)?)
        }

        #[cfg(not(feature = "debug"))]
        {
            Ok(response.json::<$response_ty>().await?)
        }
    }};
}

/// Make a http request without json body.
#[macro_export]
macro_rules! make_request {
    ($sel:ident, $method:path, $url:expr) => {{
        use reqwest::Response;
        use tracing::debug;
        use $crate::client::ResponseAPIError;

        debug!(method = stringify!($method), url = $url);

        let response: Response = $sel.http_client.inner($method, $url)?.send().await?;
        let status_code = response.status().as_u16();

        debug!("Received HTTP status code: {}", status_code);
        debug!("Sending request to URL: {}", $url);

        match status_code {
            200..=299 => {
                #[cfg(feature = "debug")]
                {
                    if status_code == 204 {
                        return Ok(Default::default());
                    }
                    let res: serde_json::Value = response.json().await?;
                    debug!("Response: {:#?}", res);
                    Ok(serde_json::from_value(res)?)
                }
                #[cfg(not(feature = "debug"))]
                {
                    if status_code == 204 {
                        return Ok(Default::default());
                    }
                    Ok(response.json().await?)
                }
            }

            _ => {
                let json_body: serde_json::Value = response.json().await?;
                debug!("Received API error response: {:#?}", json_body);

                let api_error: ResponseAPIError =
                    serde_json::from_value(json_body).unwrap_or_else(|_| ResponseAPIError {
                        error: Default::default(),
                    });

                Err(UbiClientError::APIResponseError {
                    etype: api_error.error._type.unwrap_or_default(),
                    message: api_error.error.message.unwrap_or_default(),
                    details: api_error.error.details,
                })
            }
        }
    }};
}

// #[macro_export]
// macro_rules! make_request {
//     ($sel:ident, $method:path, $url:expr) => {{
//         use reqwest;
//         tracing::debug!(method = stringify!($method), url = $url,);
//         let response: reqwest::Response = $sel.http_client.inner($method, $url)?.send().await?;
//         use $crate::client::ResponseAPIError;

//         let status_code = &response.status().as_u16();
//         tracing::debug!("Received http status code: {}", status_code);
//         tracing::debug!("Sending requests to url: {}", $url);

//         if !(*status_code >= 200 && *status_code < 300) {
//             if *status_code == 204 {
//                 return Ok(());
//             }
//             let api_response: serde_json::Value = response.json().await?;
//             tracing::debug!("Received api response: {:#?}", api_response);
//             let api_response: ResponseAPIError = serde_json::from_value(api_response).unwrap();
//             return Err(UbiClientError::APIResponseError {
//                 etype: api_response.error._type.unwrap_or_default(),
//                 message: api_response.error.message.unwrap_or_default(),
//                 details: api_response.error.details,
//             });
//         }

//         #[cfg(feature = "debug")]
//         {
//             let res: serde_json::Value = response.json().await?;
//             tracing::debug!("Response {:?}", res);
//             Ok(serde_json::from_value(res)?)
//         }

//         #[cfg(not(feature = "debug"))]
//         {
//             Ok(response.json().await?)
//         }
//     }};
// }

/// Represents an HTTP client for making requests to a specific base URL and API version.
///
/// The `HTTPClient` struct provides a convenient way to make HTTP requests to a specific base URL and API version.
/// It wraps the `reqwest` library to provide a higher-level interface for making requests.
///
/// The `new` function creates a new `HTTPClient` instance by parsing the provided base URL and API version.
/// The `inner` function is used to create a `reqwest::RequestBuilder` for a specific HTTP method and URL path.
///
/// This implementation is an internal detail of the crate and is not intended to be used directly by end-users.
impl HTTPClient {
    pub fn new<S, T>(base_url: S, client: reqwest::Client, version: T) -> HTTPClient
    where
        S: Into<String>,
        T: Into<String>,
    {
        let parsed_url =
            reqwest::Url::parse(&base_url.into()).expect("Failed to parse the base_url");

        let ver = format!("{}/", version.into().replace('/', ""));
        tracing::debug!("API Version is {}", &ver);
        HTTPClient {
            base_url: parsed_url,
            client,
            version: ver,
        }
    }

    pub(crate) fn inner(
        &self,
        method: reqwest::Method,
        query_url: &str,
    ) -> Result<reqwest::RequestBuilder, UbiClientError> {
        let qurl = query_url.trim_start_matches('/');
        let url = self.base_url.join(&self.version)?.join(qurl)?;
        tracing::debug!("URL is {:?}", &url);

        // dbg!(&url);
        let request_with_url_and_header: Result<reqwest::RequestBuilder, UbiClientError> =
            match method {
                reqwest::Method::GET => Ok(self.client.get(url)),
                reqwest::Method::PUT => Ok(self.client.put(url)),
                reqwest::Method::POST => Ok(self.client.post(url)),
                reqwest::Method::DELETE => Ok(self.client.delete(url)),
                _ => return Err(UbiClientError::UnsupportedMethod),
            };
        request_with_url_and_header
    }
}