1use harmont_cloud_raw::Client as RawClient;
4use crate::{HarmontError, Result};
5
6pub const DEFAULT_BASE_URL: &str = "https://api.harmont.dev";
8
9#[derive(Clone, Debug)]
15pub struct HarmontClient {
16 pub(crate) raw: RawClient,
17 pub(crate) http: reqwest::Client,
18 pub(crate) base: String,
19}
20
21impl HarmontClient {
22 pub fn new(token: impl Into<String>) -> Self {
24 Self::with_base_url(token, DEFAULT_BASE_URL)
25 }
26
27 pub fn with_base_url(token: impl Into<String>, base: impl Into<String>) -> Self {
34 let token = token.into();
35 let base = base.into();
36 let mut headers = reqwest::header::HeaderMap::new();
37 let mut auth = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
38 .expect("API token contains characters invalid for an HTTP Authorization header");
39 auth.set_sensitive(true); headers.insert(reqwest::header::AUTHORIZATION, auth);
41 let http = reqwest::Client::builder()
42 .default_headers(headers)
43 .build()
44 .expect("reqwest client builds with static config");
45 let raw = RawClient::new_with_client(&base, http.clone());
46 Self { raw, http, base }
47 }
48
49 pub fn anonymous(base: impl Into<String>) -> Self {
52 let base = base.into();
53 let http = reqwest::Client::builder()
54 .build()
55 .expect("reqwest client builds with static config");
56 let raw = RawClient::new_with_client(&base, http.clone());
57 Self { raw, http, base }
58 }
59
60 pub fn base_url(&self) -> &str {
62 &self.base
63 }
64
65 pub fn raw(&self) -> &RawClient {
67 &self.raw
68 }
69
70 pub(crate) async fn parse_json<T: serde::de::DeserializeOwned>(
72 &self, resp: reqwest::Response,
73 ) -> Result<T> {
74 let status = resp.status();
75 if status == reqwest::StatusCode::UNAUTHORIZED {
76 return Err(HarmontError::Unauthorized);
77 }
78 let bytes = resp.bytes().await?;
79 if status.is_success() {
80 return serde_json::from_slice(&bytes).map_err(|e| HarmontError::Decode(e.to_string()));
81 }
82 if status == reqwest::StatusCode::NOT_FOUND {
83 return Err(HarmontError::NotFound(String::from_utf8_lossy(&bytes).into()));
84 }
85 let (code, message) = parse_error_body(&bytes);
86 Err(HarmontError::Api { status: status.as_u16(), code, message })
87 }
88
89 pub(crate) async fn parse_json_structured<T: serde::de::DeserializeOwned>(
94 &self,
95 resp: reqwest::Response,
96 ) -> Result<T> {
97 let status = resp.status();
98 if status == reqwest::StatusCode::UNAUTHORIZED {
99 return Err(HarmontError::Unauthorized);
100 }
101 let bytes = resp.bytes().await?;
102 if status.is_success() {
103 return serde_json::from_slice(&bytes).map_err(|e| HarmontError::Decode(e.to_string()));
104 }
105 let (code, message) = parse_error_body(&bytes);
106 Err(HarmontError::Api { status: status.as_u16(), code, message })
107 }
108}
109
110fn parse_error_body(bytes: &[u8]) -> (String, String) {
111 let v: serde_json::Value = serde_json::from_slice(bytes).unwrap_or(serde_json::Value::Null);
112 let obj = v.get("error").unwrap_or(&v);
113 let code = obj.get("code").and_then(|c| c.as_str()).unwrap_or("unknown").to_string();
114 let message = obj.get("message").and_then(|m| m.as_str())
115 .unwrap_or_else(|| std::str::from_utf8(bytes).unwrap_or("")).to_string();
116 (code, message)
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn default_base_url_is_prod() {
125 let c = HarmontClient::new("hm_test");
126 assert_eq!(c.base_url(), "https://api.harmont.dev");
127 }
128
129 #[test]
130 fn custom_base_url_is_used() {
131 let c = HarmontClient::with_base_url("hm_test", "http://localhost:4000");
132 assert_eq!(c.base_url(), "http://localhost:4000");
133 }
134}