Skip to main content

onspring/
client.rs

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
11/// Client for interacting with the Onspring API v2.
12pub struct OnspringClient {
13  http_client: reqwest::Client,
14  base_url: String,
15  api_key: String,
16}
17
18/// Builder for constructing an [`OnspringClient`].
19pub 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  /// Creates a new builder with the given API key.
28  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  /// Sets the base URL for the API.
38  pub fn base_url(mut self, url: impl Into<String>) -> Self {
39    self.base_url = url.into();
40    self
41  }
42
43  /// Sets a custom `reqwest::Client` to use for HTTP requests.
44  pub fn http_client(mut self, client: reqwest::Client) -> Self {
45    self.http_client = Some(client);
46    self
47  }
48
49  /// Sets the request timeout.
50  pub fn timeout(mut self, timeout: Duration) -> Self {
51    self.timeout = Some(timeout);
52    self
53  }
54
55  /// Builds the [`OnspringClient`].
56  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  /// Creates a new [`OnspringClientBuilder`].
74  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}