use anyhow::Context;
use reqwest::Method;
use reqwest::Proxy;
use reqwest::Response;
use serde_json::{Value, json};
use crate::error::ArcAgiError;
use crate::models::{EnvironmentInfo, FrameData};
use crate::params::{ClientConfig, MakeParams, ScorecardParams, StepParams};
use crate::response::{AnonKeyResponse, EnvironmentScorecard, ScorecardOpenResponse};
pub const DEFAULT_BASE_URL: &str = "https://three.arcprize.org";
#[derive(Debug, Default)]
pub struct ClientBuilder {
config: ClientConfig,
#[cfg(not(target_arch = "wasm32"))]
cookie_store: bool,
#[cfg(not(target_arch = "wasm32"))]
proxy: Option<String>,
}
impl ClientBuilder {
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.config = self.config.api_key(key);
self
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.config = self.config.base_url(url);
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn cookie_store(mut self, enable: bool) -> Self {
self.cookie_store = enable;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn proxy(mut self, url: impl Into<String>) -> Self {
self.proxy = Some(url.into());
self
}
pub fn build(self) -> Result<Client, ArcAgiError> {
let mut builder = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
if self.cookie_store {
builder = builder.cookie_store(true);
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(proxy_url) = self.proxy {
let proxy = Proxy::all(&proxy_url)
.with_context(|| format!("Invalid proxy URL: {proxy_url}"))
.map_err(ArcAgiError::from)?;
builder = builder.proxy(proxy);
}
let client = builder
.build()
.context("Failed to build reqwest HTTP client")
.map_err(ArcAgiError::from)?;
Ok(Client {
client,
config: self.config,
})
}
}
pub struct Client {
client: reqwest::Client,
config: ClientConfig,
}
impl Client {
pub fn new() -> Self {
Client {
client: reqwest::Client::new(),
config: ClientConfig::default(),
}
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn base_url(&self) -> String {
self.config.resolved_base_url()
}
pub fn api_key(&self) -> String {
self.config.resolved_api_key()
}
async fn request(
&self,
method: Method,
url: &str,
params: &[(&str, &str)],
) -> Result<Response, ArcAgiError> {
let api_key = self.api_key();
let resp = self
.client
.request(method, url)
.query(params)
.header("X-API-Key", &api_key)
.header("Accept", "application/json")
.send()
.await
.with_context(|| format!("Failed to send request to {url}"))
.map_err(ArcAgiError::from)?;
let status = resp.status();
let resp = resp
.error_for_status()
.with_context(|| format!("Request to {url} failed (HTTP {status})"))
.map_err(ArcAgiError::from)?;
Ok(resp)
}
async fn post_json(&self, url: &str, body: Value) -> Result<Response, ArcAgiError> {
let api_key = self.api_key();
let resp = self
.client
.post(url)
.header("X-API-Key", &api_key)
.header("Accept", "application/json")
.json(&body)
.send()
.await
.with_context(|| format!("Failed to POST to {url}"))
.map_err(ArcAgiError::from)?;
let status = resp.status();
let resp = resp
.error_for_status()
.with_context(|| format!("POST to {url} failed (HTTP {status})"))
.map_err(ArcAgiError::from)?;
Ok(resp)
}
pub async fn get_anonymous_key(&self) -> Result<String, ArcAgiError> {
let url = format!("{}/api/games/anonkey", self.base_url());
let resp = self
.client
.get(&url)
.header("Accept", "application/json")
.send()
.await
.with_context(|| format!("Failed to GET {url}"))
.map_err(ArcAgiError::from)?
.error_for_status()
.map_err(ArcAgiError::from)?;
let body: AnonKeyResponse = resp
.json()
.await
.context("Failed to deserialise anon key response")
.map_err(ArcAgiError::from)?;
Ok(body.api_key)
}
pub async fn list_environments(&self) -> Result<Vec<EnvironmentInfo>, ArcAgiError> {
let url = format!("{}/api/games", self.base_url());
let resp = self.request(Method::GET, &url, &[]).await?;
resp.json()
.await
.context("Failed to deserialise environment list")
.map_err(ArcAgiError::from)
}
pub async fn get_environment(&self, game_id: &str) -> Result<EnvironmentInfo, ArcAgiError> {
let url = format!("{}/api/games/{}", self.base_url(), game_id);
let resp = self.request(Method::GET, &url, &[]).await?;
resp.json()
.await
.context("Failed to deserialise environment info")
.map_err(ArcAgiError::from)
}
pub async fn open_scorecard(
&self,
params: Option<ScorecardParams>,
) -> Result<String, ArcAgiError> {
let url = format!("{}/api/scorecard/open", self.base_url());
let body = params.unwrap_or_default().to_json_body();
let resp = self.post_json(&url, body).await?;
let open_resp: ScorecardOpenResponse = resp
.json()
.await
.context("Failed to deserialise scorecard open response")
.map_err(ArcAgiError::from)?;
Ok(open_resp.card_id)
}
pub async fn get_scorecard(&self, card_id: &str) -> Result<EnvironmentScorecard, ArcAgiError> {
let url = format!("{}/api/scorecard/{}", self.base_url(), card_id);
let resp = self.request(Method::GET, &url, &[]).await?;
resp.json()
.await
.context("Failed to deserialise scorecard")
.map_err(ArcAgiError::from)
}
pub async fn close_scorecard(
&self,
card_id: &str,
) -> Result<EnvironmentScorecard, ArcAgiError> {
let url = format!("{}/api/scorecard/close", self.base_url());
let body = json!({ "card_id": card_id });
let resp = self.post_json(&url, body).await?;
resp.json()
.await
.context("Failed to deserialise close scorecard response")
.map_err(ArcAgiError::from)
}
pub async fn reset(&self, params: MakeParams) -> Result<FrameData, ArcAgiError> {
let url = format!("{}/api/cmd/RESET", self.base_url());
let mut body = json!({
"game_id": params.game_id,
"card_id": params.scorecard_id,
});
if let Some(guid) = ¶ms.guid {
body["guid"] = Value::String(guid.clone());
}
let resp = self.post_json(&url, body).await?;
resp.json()
.await
.context("Failed to deserialise reset frame")
.map_err(ArcAgiError::from)
}
pub async fn step(&self, params: StepParams) -> Result<FrameData, ArcAgiError> {
let action_segment = if params.action_id == 0 {
"RESET".to_string()
} else {
format!("ACTION{}", params.action_id)
};
let url = format!("{}/api/cmd/{}", self.base_url(), action_segment);
let mut body = json!({
"game_id": params.game_id,
"card_id": params.scorecard_id,
"guid": params.guid,
});
if let Some(data) = ¶ms.data {
if let Some(x) = data.get("x") {
body["x"] = x.clone();
}
if let Some(y) = data.get("y") {
body["y"] = y.clone();
}
}
if let Some(reasoning) = ¶ms.reasoning {
body["reasoning"] = reasoning.clone();
}
let resp = self.post_json(&url, body).await?;
resp.json()
.await
.context("Failed to deserialise step frame")
.map_err(ArcAgiError::from)
}
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}