use harmont_cloud_raw::Client as RawClient;
use crate::{HarmontError, Result};
pub const DEFAULT_BASE_URL: &str = "https://api.harmont.dev";
#[derive(Clone, Debug)]
pub struct HarmontClient {
pub(crate) raw: RawClient,
pub(crate) http: reqwest::Client,
pub(crate) base: String,
}
impl HarmontClient {
pub fn new(token: impl Into<String>) -> Self {
Self::with_base_url(token, DEFAULT_BASE_URL)
}
pub fn with_base_url(token: impl Into<String>, base: impl Into<String>) -> Self {
let token = token.into();
let base = base.into();
let mut headers = reqwest::header::HeaderMap::new();
let mut auth = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
.expect("API token contains characters invalid for an HTTP Authorization header");
auth.set_sensitive(true); headers.insert(reqwest::header::AUTHORIZATION, auth);
let http = reqwest::Client::builder()
.default_headers(headers)
.build()
.expect("reqwest client builds with static config");
let raw = RawClient::new_with_client(&base, http.clone());
Self { raw, http, base }
}
pub fn anonymous(base: impl Into<String>) -> Self {
let base = base.into();
let http = reqwest::Client::builder()
.build()
.expect("reqwest client builds with static config");
let raw = RawClient::new_with_client(&base, http.clone());
Self { raw, http, base }
}
pub fn base_url(&self) -> &str {
&self.base
}
pub fn raw(&self) -> &RawClient {
&self.raw
}
pub(crate) async fn parse_json<T: serde::de::DeserializeOwned>(
&self, resp: reqwest::Response,
) -> Result<T> {
let status = resp.status();
if status == reqwest::StatusCode::UNAUTHORIZED {
return Err(HarmontError::Unauthorized);
}
let bytes = resp.bytes().await?;
if status.is_success() {
return serde_json::from_slice(&bytes).map_err(|e| HarmontError::Decode(e.to_string()));
}
if status == reqwest::StatusCode::NOT_FOUND {
return Err(HarmontError::NotFound(String::from_utf8_lossy(&bytes).into()));
}
let (code, message) = parse_error_body(&bytes);
Err(HarmontError::Api { status: status.as_u16(), code, message })
}
pub(crate) async fn parse_json_structured<T: serde::de::DeserializeOwned>(
&self,
resp: reqwest::Response,
) -> Result<T> {
let status = resp.status();
if status == reqwest::StatusCode::UNAUTHORIZED {
return Err(HarmontError::Unauthorized);
}
let bytes = resp.bytes().await?;
if status.is_success() {
return serde_json::from_slice(&bytes).map_err(|e| HarmontError::Decode(e.to_string()));
}
let (code, message) = parse_error_body(&bytes);
Err(HarmontError::Api { status: status.as_u16(), code, message })
}
}
fn parse_error_body(bytes: &[u8]) -> (String, String) {
let v: serde_json::Value = serde_json::from_slice(bytes).unwrap_or(serde_json::Value::Null);
let obj = v.get("error").unwrap_or(&v);
let code = obj.get("code").and_then(|c| c.as_str()).unwrap_or("unknown").to_string();
let message = obj.get("message").and_then(|m| m.as_str())
.unwrap_or_else(|| std::str::from_utf8(bytes).unwrap_or("")).to_string();
(code, message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_base_url_is_prod() {
let c = HarmontClient::new("hm_test");
assert_eq!(c.base_url(), "https://api.harmont.dev");
}
#[test]
fn custom_base_url_is_used() {
let c = HarmontClient::with_base_url("hm_test", "http://localhost:4000");
assert_eq!(c.base_url(), "http://localhost:4000");
}
}