1use std::time::Duration;
2
3use bytes::Bytes;
4use reqwest::header::{HeaderMap, HeaderValue};
5use reqwest::{Method, StatusCode};
6use serde::de::DeserializeOwned;
7use serde::Serialize;
8
9use crate::error::{OnspringError, Result};
10
11pub struct OnspringClient {
13 http_client: reqwest::Client,
14 base_url: String,
15 api_key: String,
16}
17
18pub struct OnspringClientBuilder {
20 base_url: String,
21 api_key: String,
22 http_client: Option<reqwest::Client>,
23 timeout: Option<Duration>,
24}
25
26impl OnspringClientBuilder {
27 pub fn new(api_key: impl Into<String>) -> Self {
29 Self {
30 base_url: "https://api.onspring.com".to_string(),
31 api_key: api_key.into(),
32 http_client: None,
33 timeout: None,
34 }
35 }
36
37 pub fn base_url(mut self, url: impl Into<String>) -> Self {
39 self.base_url = url.into();
40 self
41 }
42
43 pub fn http_client(mut self, client: reqwest::Client) -> Self {
45 self.http_client = Some(client);
46 self
47 }
48
49 pub fn timeout(mut self, timeout: Duration) -> Self {
51 self.timeout = Some(timeout);
52 self
53 }
54
55 pub fn build(self) -> OnspringClient {
57 let http_client = self.http_client.unwrap_or_else(|| {
58 reqwest::Client::builder()
59 .timeout(self.timeout.unwrap_or(Duration::from_secs(120)))
60 .build()
61 .expect("failed to build HTTP client")
62 });
63
64 OnspringClient {
65 http_client,
66 base_url: self.base_url.trim_end_matches('/').to_string(),
67 api_key: self.api_key,
68 }
69 }
70}
71
72impl OnspringClient {
73 pub fn builder(api_key: impl Into<String>) -> OnspringClientBuilder {
75 OnspringClientBuilder::new(api_key)
76 }
77
78 fn default_headers(&self) -> HeaderMap {
79 let mut headers = HeaderMap::new();
80 headers.insert(
81 "x-apikey",
82 HeaderValue::from_str(&self.api_key).expect("invalid API key"),
83 );
84 headers.insert("x-api-version", HeaderValue::from_static("2"));
85 headers
86 }
87
88 pub(crate) async fn request<T: DeserializeOwned>(
89 &self,
90 method: Method,
91 path: &str,
92 query: &[(&str, String)],
93 body: Option<&impl Serialize>,
94 ) -> Result<T> {
95 let url = format!("{}{}", self.base_url, path);
96 let mut req = self
97 .http_client
98 .request(method, &url)
99 .headers(self.default_headers())
100 .query(query);
101
102 if let Some(body) = body {
103 req = req.json(body);
104 }
105
106 let response = req.send().await?;
107 let status = response.status();
108
109 if !status.is_success() {
110 let message = response
111 .json::<serde_json::Value>()
112 .await
113 .ok()
114 .and_then(|v| v.get("message")?.as_str().map(String::from))
115 .unwrap_or_default();
116 return Err(OnspringError::Api {
117 status_code: status.as_u16(),
118 message,
119 });
120 }
121
122 let body = response.text().await?;
123 serde_json::from_str(&body).map_err(OnspringError::Serialization)
124 }
125
126 pub(crate) async fn request_no_content(
127 &self,
128 method: Method,
129 path: &str,
130 query: &[(&str, String)],
131 body: Option<&impl Serialize>,
132 ) -> Result<()> {
133 let url = format!("{}{}", self.base_url, path);
134 let mut req = self
135 .http_client
136 .request(method, &url)
137 .headers(self.default_headers())
138 .query(query);
139
140 if let Some(body) = body {
141 req = req.json(body);
142 }
143
144 let response = req.send().await?;
145 let status = response.status();
146
147 if !status.is_success() {
148 let message = response
149 .json::<serde_json::Value>()
150 .await
151 .ok()
152 .and_then(|v| v.get("message")?.as_str().map(String::from))
153 .unwrap_or_default();
154 return Err(OnspringError::Api {
155 status_code: status.as_u16(),
156 message,
157 });
158 }
159
160 Ok(())
161 }
162
163 pub(crate) async fn request_bytes(
164 &self,
165 method: Method,
166 path: &str,
167 query: &[(&str, String)],
168 ) -> Result<(StatusCode, HeaderMap, Bytes)> {
169 let url = format!("{}{}", self.base_url, path);
170 let response = self
171 .http_client
172 .request(method, &url)
173 .headers(self.default_headers())
174 .query(query)
175 .send()
176 .await?;
177
178 let status = response.status();
179
180 if !status.is_success() {
181 let message = response
182 .json::<serde_json::Value>()
183 .await
184 .ok()
185 .and_then(|v| v.get("message")?.as_str().map(String::from))
186 .unwrap_or_default();
187 return Err(OnspringError::Api {
188 status_code: status.as_u16(),
189 message,
190 });
191 }
192
193 let headers = response.headers().clone();
194 let bytes = response.bytes().await?;
195 Ok((status, headers, bytes))
196 }
197
198 pub(crate) async fn request_multipart<T: DeserializeOwned>(
199 &self,
200 path: &str,
201 form: reqwest::multipart::Form,
202 ) -> Result<T> {
203 let url = format!("{}{}", self.base_url, path);
204 let response = self
205 .http_client
206 .post(&url)
207 .headers(self.default_headers())
208 .multipart(form)
209 .send()
210 .await?;
211
212 let status = response.status();
213
214 if !status.is_success() {
215 let message = response
216 .json::<serde_json::Value>()
217 .await
218 .ok()
219 .and_then(|v| v.get("message")?.as_str().map(String::from))
220 .unwrap_or_default();
221 return Err(OnspringError::Api {
222 status_code: status.as_u16(),
223 message,
224 });
225 }
226
227 let body = response.text().await?;
228 serde_json::from_str(&body).map_err(OnspringError::Serialization)
229 }
230}