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(),
});
}
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
}