browserbase 0.1.0

Rust SDK for Browserbase - cloud browser automation platform
Documentation
use std::{
    fmt,
    sync::Arc,
    time::{Duration, Instant},
};

use reqwest::{Method, RequestBuilder};
use serde::de::DeserializeOwned;
use url::Url;

use crate::{contexts::Contexts, error::Error, projects::Projects, sessions::Sessions};

const DEFAULT_BASE_URL: &str = "https://api.browserbase.com/v1/";

#[derive(Clone)]
pub struct Browserbase {
    http: reqwest::Client,
    base_url: Url,
    api_key: Arc<str>,
}

impl fmt::Debug for Browserbase {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Browserbase")
            .field("base_url", &self.base_url)
            .field("api_key", &"<redacted>")
            .finish()
    }
}

pub struct BrowserbaseBuilder {
    api_key: String,
    base_url: Url,
    timeout: Duration,
    user_agent: String,
}

impl BrowserbaseBuilder {
    pub fn new(api_key: impl Into<String>) -> Result<Self, Error> {
        Ok(Self {
            api_key: api_key.into(),
            base_url: Url::parse(DEFAULT_BASE_URL)?,
            timeout: Duration::from_secs(60),
            user_agent: format!("browserbase-rust/{}", env!("CARGO_PKG_VERSION")),
        })
    }

    pub fn base_url(mut self, base_url: impl AsRef<str>) -> Result<Self, Error> {
        self.base_url = normalize_base_url(Url::parse(base_url.as_ref())?);
        Ok(self)
    }

    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
        self.user_agent = user_agent.into();
        self
    }

    pub fn build(self) -> Result<Browserbase, Error> {
        let http = reqwest::ClientBuilder::new()
            .timeout(self.timeout)
            .user_agent(self.user_agent)
            .build()?;

        Ok(Browserbase {
            http,
            base_url: self.base_url,
            api_key: Arc::from(self.api_key),
        })
    }
}

impl Browserbase {
    pub fn builder(api_key: impl Into<String>) -> Result<BrowserbaseBuilder, Error> {
        BrowserbaseBuilder::new(api_key)
    }

    pub fn new(api_key: impl Into<String>) -> Result<Self, Error> {
        Self::builder(api_key)?.build()
    }

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

    pub fn sessions(&self) -> Sessions<'_> {
        Sessions::new(self)
    }

    pub fn contexts(&self) -> Contexts<'_> {
        Contexts::new(self)
    }

    pub fn projects(&self) -> Projects<'_> {
        Projects::new(self)
    }

    pub(crate) fn request(&self, method: Method, path: &str) -> Result<RequestBuilder, Error> {
        let url = self.base_url.join(path)?;
        Ok(self
            .http
            .request(method, url)
            .header("X-BB-API-Key", self.api_key.as_ref()))
    }

    pub(crate) async fn send_json<T: DeserializeOwned>(
        &self,
        request: RequestBuilder,
    ) -> Result<T, Error> {
        let started = Instant::now();
        let resp = request.send().await?;
        let status = resp.status();
        let body = resp.bytes().await?;

        if !status.is_success() {
            return Err(Error::Api {
                status,
                body: String::from_utf8_lossy(&body).to_string(),
            });
        }

        // Prevent unused warning on `started` while keeping it around for future timing hooks.
        let _elapsed = started.elapsed();

        Ok(serde_json::from_slice(&body)?)
    }

    pub(crate) async fn send_unit(&self, request: RequestBuilder) -> Result<(), Error> {
        let resp = request.send().await?;
        let status = resp.status();
        let body = resp.bytes().await?;

        if !status.is_success() {
            return Err(Error::Api {
                status,
                body: String::from_utf8_lossy(&body).to_string(),
            });
        }

        Ok(())
    }

    pub(crate) async fn send_bytes(&self, request: RequestBuilder) -> Result<bytes::Bytes, Error> {
        let resp = request.send().await?;
        let status = resp.status();
        let body = resp.bytes().await?;

        if !status.is_success() {
            return Err(Error::Api {
                status,
                body: String::from_utf8_lossy(&body).to_string(),
            });
        }

        Ok(body)
    }
}

fn normalize_base_url(mut url: Url) -> Url {
    if !url.path().ends_with('/') {
        let new_path = format!("{}/", url.path());
        url.set_path(&new_path);
    }
    url
}