context69-sdk 0.5.0

Async Rust SDK for the Context69 HTTP API.
Documentation
use context69_contracts::ApiErrorResponse;
use reqwest::{
    Method, RequestBuilder, Response, Url,
    header::AUTHORIZATION,
    multipart::{Form, Part},
};
use uuid::Uuid;

use super::{Context69Client, PERSONAL_ACCESS_TOKEN_PREFIX};
use crate::Error;

impl Context69Client {
    pub(crate) async fn authorized_request(
        &self,
        method: Method,
        path: &str,
    ) -> Result<RequestBuilder, Error> {
        let token = self
            .session
            .read()
            .await
            .personal_access_token
            .clone()
            .ok_or(Error::AuthenticationRequired)?;
        Ok(self
            .client
            .request(method, self.url(path)?)
            .header(AUTHORIZATION, format!("Bearer {token}")))
    }

    pub(crate) async fn authorized_url_request(
        &self,
        method: Method,
        url: Url,
    ) -> Result<RequestBuilder, Error> {
        let token = self
            .session
            .read()
            .await
            .personal_access_token
            .clone()
            .ok_or(Error::AuthenticationRequired)?;
        Ok(self
            .client
            .request(method, url)
            .header(AUTHORIZATION, format!("Bearer {token}")))
    }

    pub(crate) fn url(&self, path: &str) -> Result<Url, Error> {
        self.base_url
            .join(path.trim_start_matches('/'))
            .map_err(|source| Error::UrlJoin {
                path: path.to_string(),
                source,
            })
    }

    pub(crate) async fn execute_json<T: serde::de::DeserializeOwned>(
        &self,
        request: RequestBuilder,
    ) -> Result<T, Error> {
        self.read_json_response(request.send().await?).await
    }

    pub(crate) async fn execute_empty(&self, request: RequestBuilder) -> Result<(), Error> {
        let response = request.send().await?;
        if response.status().is_success() {
            return Ok(());
        }
        Err(self.build_http_error(response).await)
    }

    pub(crate) async fn read_json_response<T: serde::de::DeserializeOwned>(
        &self,
        response: Response,
    ) -> Result<T, Error> {
        if !response.status().is_success() {
            return Err(self.build_http_error(response).await);
        }
        Ok(response.json::<T>().await?)
    }

    async fn build_http_error(&self, response: Response) -> Error {
        let status = response.status();
        let body = response.text().await.unwrap_or_default();
        Error::HttpStatus {
            status,
            api_error: parse_api_error_message(&body),
            body,
        }
    }
}

pub(crate) fn encode_path_component(value: &str) -> String {
    url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
}

pub(crate) fn group_path(group_path: &str, suffix: &str) -> String {
    format!(
        "/v1/groups/by-path/{}{}",
        encode_path_component(group_path),
        suffix
    )
}

pub(crate) fn file_upload_form(folder_id: Option<Uuid>, files: Vec<Part>) -> Form {
    let mut form = Form::new();
    if let Some(folder_id) = folder_id {
        form = form.text("folder_id", folder_id.to_string());
    }
    for file in files {
        form = form.part("files", file);
    }
    form
}

pub(crate) fn validate_personal_access_token(token: String) -> Result<String, Error> {
    let trimmed = token.trim();
    if trimmed.is_empty() {
        return Err(Error::InvalidPersonalAccessToken(
            "personal access token must not be empty".to_string(),
        ));
    }
    if !trimmed.starts_with(PERSONAL_ACCESS_TOKEN_PREFIX) {
        return Err(Error::InvalidPersonalAccessToken(format!(
            "personal access token must start with {PERSONAL_ACCESS_TOKEN_PREFIX}"
        )));
    }
    Ok(trimmed.to_string())
}

pub(crate) fn parse_api_error_message(body: &str) -> Option<String> {
    serde_json::from_str::<ApiErrorResponse>(body)
        .ok()
        .map(|response| response.error)
}