boreholeio 0.1.0

A library for interacting with borehole.io, a subsurface data management, delivery and visualisation platform
Documentation
use anyhow::Context;
use reqwest::{Client, StatusCode};
use response::ApiResponse;
use serde::{de::DeserializeOwned, Serialize};

use crate::{
    client::errors::{ClientError, RequestFailedError},
    schema::WebError,
};

use super::{
    authentication::Authentication,
    errors::{
        AuthenticationError, ParameterError, ResourceNotFoundError, ServerError, UnknownError,
    },
    Result,
};

pub(crate) mod response;

pub(crate) struct InternalWebClient {
    pub(crate) client: Client,
    pub(crate) auth: Authentication,
}

#[expect(unused)]
pub(crate) enum HttpRequestType {
    Post,
    Get,
    Put,
    Patch,
    Delete,
}

impl InternalWebClient {
    pub fn new(auth: Authentication) -> Self {
        Self {
            client: Client::new(),
            auth,
        }
    }

    async fn send_request_internal<I: Serialize>(
        &self,
        url: &str,
        request_type: HttpRequestType,
        request_body: I,
    ) -> Result<(String, StatusCode)> {
        let mut request_builder = match request_type {
            HttpRequestType::Post => self.client.post(url),
            HttpRequestType::Get => self.client.get(url),
            HttpRequestType::Put => self.client.put(url),
            HttpRequestType::Patch => self.client.patch(url),
            HttpRequestType::Delete => self.client.delete(url),
        }
        .json(&request_body);

        match &self.auth {
            Authentication::None => {}
            Authentication::Application(application_auth) => {
                request_builder = request_builder.bearer_auth(application_auth.jwt.clone());
            }
            Authentication::Organisation(_organisation_auth) => {
                todo!("Organisation API Keys not yet implemented.")
            }
        }

        let response = request_builder.send().await?;

        let status = response.status();

        let response_body = response.text().await?;

        Ok((response_body, status))
    }

    pub(crate) async fn send_request_no_data<I: Serialize>(
        &self,
        url: &str,
        request_type: HttpRequestType,
        input_body: I,
    ) -> Result<()> {
        let (response_body, response_status_code) = self
            .send_request_internal(url, request_type, input_body)
            .await?;

        let response: ApiResponse<()> = serde_json::from_str(response_body.as_str())
            .with_context(|| {
                format!(
                    "Unable to deserialise response. response body was: \"{}\"",
                    response_body
                )
            })
            .map_err(|error| -> ClientError { RequestFailedError::new(error.into()).into() })?;

        match response {
            ApiResponse::Success(_) => Ok(()),
            ApiResponse::Error(error_response) => Err(map_error_response(
                response_status_code,
                error_response.errors,
            )),
        }
    }

    pub(crate) async fn send_request_with_data<I: Serialize, O: DeserializeOwned + 'static>(
        &self,
        url: &str,
        request_type: HttpRequestType,
        input_body: I,
    ) -> Result<O> {
        let (response_body, response_status_code) = self
            .send_request_internal(url, request_type, input_body)
            .await?;

        let response: ApiResponse<O> = serde_json::from_str(response_body.as_str())
            .with_context(|| {
                format!(
                    "Unable to deserialise response. response body was: \"{}\"",
                    response_body
                )
            })
            .map_err(|error| -> ClientError { RequestFailedError::new(error.into()).into() })?;

        match response {
            ApiResponse::Success(success_response) => {
                let data = success_response.data.ok_or(RequestFailedError::new(
                    "response did not contain data".into(),
                ))?;

                Ok(data)
            }
            ApiResponse::Error(error_response) => Err(map_error_response(
                response_status_code,
                error_response.errors,
            )),
        }
    }
}

fn map_error_response(status_code: StatusCode, errors: Vec<WebError>) -> ClientError {
    match status_code {
        StatusCode::BAD_REQUEST
        | StatusCode::PAYMENT_REQUIRED
        | StatusCode::METHOD_NOT_ALLOWED
        | StatusCode::NOT_ACCEPTABLE
        | StatusCode::CONFLICT
        | StatusCode::LENGTH_REQUIRED
        | StatusCode::PRECONDITION_FAILED
        | StatusCode::PAYLOAD_TOO_LARGE
        | StatusCode::URI_TOO_LONG
        | StatusCode::UNSUPPORTED_MEDIA_TYPE
        | StatusCode::RANGE_NOT_SATISFIABLE
        | StatusCode::EXPECTATION_FAILED
        | StatusCode::UNPROCESSABLE_ENTITY
        | StatusCode::FAILED_DEPENDENCY
        | StatusCode::UPGRADE_REQUIRED
        | StatusCode::PRECONDITION_REQUIRED
        | StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE => ParameterError::from(errors).into(),

        StatusCode::UNAUTHORIZED
        | StatusCode::FORBIDDEN
        | StatusCode::PROXY_AUTHENTICATION_REQUIRED => AuthenticationError::from(errors).into(),

        StatusCode::REQUEST_TIMEOUT
        | StatusCode::MISDIRECTED_REQUEST
        | StatusCode::TOO_MANY_REQUESTS => RequestFailedError::from(errors).into(),

        StatusCode::NOT_FOUND
        | StatusCode::GONE
        | StatusCode::LOCKED
        | StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => ResourceNotFoundError::from(errors).into(),

        _ => {
            if status_code.is_server_error() {
                ServerError::from(errors).into()
            } else {
                UnknownError::from(errors).into()
            }
        }
    }
}