use reqwest::Client;
use serde::{de::DeserializeOwned, Serialize};
const DEFAULT_HTTP_URL: &str = "http://127.0.0.1:7878";
pub fn discover_origin_url(cli_url: Option<String>) -> String {
if let Some(url) = cli_url {
return url;
}
DEFAULT_HTTP_URL.to_string()
}
#[derive(Clone)]
pub struct OriginClient {
client: Client,
base_url: String,
}
impl OriginClient {
pub fn new(base_url: String) -> Self {
Self {
client: Client::new(),
base_url,
}
}
pub async fn get<R: DeserializeOwned>(&self, path: &str) -> Result<R, OriginError> {
let url = format!("{}{}", self.base_url, path);
let resp = self
.client
.get(&url)
.send()
.await
.map_err(|e| OriginError::Unreachable(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(OriginError::Api { status, body });
}
let bytes = resp.bytes().await.map_err(|e| {
OriginError::Deserialize(format!("failed to read response body: {e:#}"))
})?;
serde_json::from_slice::<R>(&bytes).map_err(|e| {
let preview = std::str::from_utf8(&bytes)
.unwrap_or("<non-utf8>")
.chars()
.take(512)
.collect::<String>();
OriginError::Deserialize(format!("{e} (body preview: {preview})"))
})
}
pub async fn post<B: Serialize, R: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> Result<R, OriginError> {
let url = format!("{}{}", self.base_url, path);
let resp = self
.client
.post(&url)
.json(body)
.send()
.await
.map_err(|e| OriginError::Unreachable(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(OriginError::Api { status, body });
}
let bytes = resp.bytes().await.map_err(|e| {
OriginError::Deserialize(format!("failed to read response body: {e:#}"))
})?;
serde_json::from_slice::<R>(&bytes).map_err(|e| {
let preview = std::str::from_utf8(&bytes)
.unwrap_or("<non-utf8>")
.chars()
.take(512)
.collect::<String>();
OriginError::Deserialize(format!("{e} (body preview: {preview})"))
})
}
pub async fn delete<R: DeserializeOwned>(&self, path: &str) -> Result<R, OriginError> {
let url = format!("{}{}", self.base_url, path);
let resp = self
.client
.delete(&url)
.send()
.await
.map_err(|e| OriginError::Unreachable(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(OriginError::Api { status, body });
}
let bytes = resp.bytes().await.map_err(|e| {
OriginError::Deserialize(format!("failed to read response body: {e:#}"))
})?;
serde_json::from_slice::<R>(&bytes).map_err(|e| {
let preview = std::str::from_utf8(&bytes)
.unwrap_or("<non-utf8>")
.chars()
.take(512)
.collect::<String>();
OriginError::Deserialize(format!("{e} (body preview: {preview})"))
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum OriginError {
#[error("Origin is not reachable: {0}")]
Unreachable(String),
#[error("Origin API error (HTTP {status}): {body}")]
Api { status: u16, body: String },
#[error("Failed to parse Origin response: {0}")]
Deserialize(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discover_url_prefers_cli_flag() {
let url = discover_origin_url(Some("http://localhost:9999".into()));
assert_eq!(url, "http://localhost:9999");
}
#[test]
fn test_discover_url_falls_back_to_http() {
let url = discover_origin_url(None);
assert_eq!(url, "http://127.0.0.1:7878");
}
}