synth-ai 0.9.1

Rust SDK for Synth AI - serverless post-training APIs
Documentation
use serde::de::DeserializeOwned;
use serde::Serialize;

use crate::types::{Result, SynthError};

#[derive(Clone)]
pub struct Transport {
    base_url: String,
    api_key: String,
    http: reqwest::Client,
}

impl Transport {
    pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Result<Self> {
        let api_key = api_key.into();
        let base_url = base_url.into().trim_end_matches('/').to_string();
        let http = reqwest::Client::builder()
            .build()
            .map_err(SynthError::Http)?;
        Ok(Self {
            base_url,
            api_key,
            http,
        })
    }

    pub fn base_url(&self) -> &str {
        &self.base_url
    }

    fn url(&self, path: &str) -> String {
        if path.starts_with("http://") || path.starts_with("https://") {
            return path.to_string();
        }
        format!("{}{}", self.base_url, path)
    }

    fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
        self.http
            .request(method, self.url(path))
            .header("Authorization", format!("Bearer {}", self.api_key))
            .header("Content-Type", "application/json")
    }

    async fn parse_json<R: DeserializeOwned>(resp: reqwest::Response) -> Result<R> {
        let status = resp.status();
        let bytes = resp.bytes().await.map_err(SynthError::Http)?;
        if status.is_success() {
            if bytes.is_empty() {
                return serde_json::from_str("{}")
                    .map_err(|e| SynthError::UnexpectedResponse(format!("empty JSON response: {e}")));
            }
            serde_json::from_slice::<R>(&bytes).map_err(SynthError::Json)
        } else {
            let body = String::from_utf8_lossy(&bytes).to_string();
            Err(SynthError::Api {
                status: status.as_u16(),
                body,
            })
        }
    }

    pub async fn get_json<R: DeserializeOwned>(&self, path: &str) -> Result<R> {
        let resp = self
            .request(reqwest::Method::GET, path)
            .send()
            .await
            .map_err(SynthError::Http)?;
        Self::parse_json(resp).await
    }

    pub async fn get_json_with_query<Q: Serialize, R: DeserializeOwned>(
        &self,
        path: &str,
        query: &Q,
    ) -> Result<R> {
        let resp = self
            .request(reqwest::Method::GET, path)
            .query(query)
            .send()
            .await
            .map_err(SynthError::Http)?;
        Self::parse_json(resp).await
    }

    pub async fn post_json<B: Serialize, R: DeserializeOwned>(&self, path: &str, body: &B) -> Result<R> {
        let resp = self
            .request(reqwest::Method::POST, path)
            .json(body)
            .send()
            .await
            .map_err(SynthError::Http)?;
        Self::parse_json(resp).await
    }

    pub async fn patch_json<B: Serialize, R: DeserializeOwned>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<R> {
        let resp = self
            .request(reqwest::Method::PATCH, path)
            .json(body)
            .send()
            .await
            .map_err(SynthError::Http)?;
        Self::parse_json(resp).await
    }

    pub async fn put_json<B: Serialize, R: DeserializeOwned>(&self, path: &str, body: &B) -> Result<R> {
        let resp = self
            .request(reqwest::Method::PUT, path)
            .json(body)
            .send()
            .await
            .map_err(SynthError::Http)?;
        Self::parse_json(resp).await
    }

    pub async fn delete_empty(&self, path: &str) -> Result<()> {
        let resp = self
            .request(reqwest::Method::DELETE, path)
            .send()
            .await
            .map_err(SynthError::Http)?;
        let status = resp.status();
        if status.is_success() {
            return Ok(());
        }
        let body = resp.text().await.unwrap_or_default();
        Err(SynthError::Api {
            status: status.as_u16(),
            body,
        })
    }

    pub async fn get_bytes(&self, path: &str) -> Result<Vec<u8>> {
        let resp = self
            .request(reqwest::Method::GET, path)
            .send()
            .await
            .map_err(SynthError::Http)?;
        let status = resp.status();
        let bytes = resp.bytes().await.map_err(SynthError::Http)?;
        if status.is_success() {
            return Ok(bytes.to_vec());
        }
        Err(SynthError::Api {
            status: status.as_u16(),
            body: String::from_utf8_lossy(&bytes).to_string(),
        })
    }
}