Files
alpha-id/src/ionos.rs
T

63 lines
2.2 KiB
Rust

use crate::model::AppError;
pub struct IonosClient {
base_url: String,
token: String,
http: reqwest::blocking::Client,
}
fn expand_tilde(p: &str) -> String {
if let Some(rest) = p.strip_prefix("~/") {
if let Some(home) = std::env::var_os("HOME") {
return format!("{}/{}", home.to_string_lossy(), rest);
}
}
p.to_string()
}
impl IonosClient {
pub fn new(base_url: &str, token_path: &str) -> Result<Self, AppError> {
let path = expand_tilde(token_path);
let token = std::fs::read_to_string(&path)
.map_err(|e| AppError::Config(format!("token {path}: {e}")))?
.trim().to_string();
if token.is_empty() {
return Err(AppError::Config(format!("empty token at {path}")));
}
let http = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()
.map_err(|e| AppError::Ionos(format!("client: {e}")))?;
Ok(Self { base_url: base_url.trim_end_matches('/').to_string(), token, http })
}
/// POST with up to 3 retries on transport / 5xx, exponential backoff.
pub fn post_json<T: serde::de::DeserializeOwned>(
&self, path: &str, body: &serde_json::Value,
) -> Result<T, AppError> {
let url = format!("{}{}", self.base_url, path);
let mut last = String::new();
for attempt in 0..3 {
let r = self.http.post(&url)
.bearer_auth(&self.token)
.json(body)
.send();
match r {
Ok(resp) if resp.status().is_success() => {
return resp.json::<T>()
.map_err(|e| AppError::Ionos(format!("decode: {e}")));
}
Ok(resp) => {
last = format!("HTTP {}", resp.status());
if !resp.status().is_server_error() {
return Err(AppError::Ionos(last));
}
}
Err(e) => last = format!("transport: {e}"),
}
std::thread::sleep(std::time::Duration::from_millis(300 * (1 << attempt)));
}
Err(AppError::Ionos(format!("giving up after retries: {last}")))
}
}