1use reqwest::header::{self, HeaderMap, HeaderValue};
11use serde::Serialize;
12use serde::de::DeserializeOwned;
13
14use crate::constants::API_BASE_URL;
15use crate::error::{ApiErrorBody, DhanError, Result};
16
17#[derive(Debug, Clone)]
36pub struct DhanClient {
37 http: reqwest::Client,
38 client_id: String,
40 access_token: String,
42 base_url: String,
44 auth_header_token: HeaderValue,
46 auth_header_client_id: HeaderValue,
47}
48
49impl DhanClient {
50 pub fn new(client_id: impl Into<String>, access_token: impl Into<String>) -> Self {
54 Self::with_base_url(client_id, access_token, API_BASE_URL)
55 }
56
57 pub fn with_base_url(
61 client_id: impl Into<String>,
62 access_token: impl Into<String>,
63 base_url: impl Into<String>,
64 ) -> Self {
65 let http = reqwest::Client::builder()
66 .default_headers(Self::default_headers())
67 .build()
68 .expect("failed to build reqwest client");
69
70 let access_token = access_token.into();
71 let client_id = client_id.into();
72
73 let auth_header_token = HeaderValue::from_str(&access_token)
74 .expect("access token contains invalid header characters");
75 let auth_header_client_id = HeaderValue::from_str(&client_id)
76 .expect("client id contains invalid header characters");
77
78 Self {
79 http,
80 client_id,
81 access_token,
82 base_url: base_url.into().trim_end_matches('/').to_owned(),
83 auth_header_token,
84 auth_header_client_id,
85 }
86 }
87
88 pub fn http(&self) -> &reqwest::Client {
90 &self.http
91 }
92
93 pub fn client_id(&self) -> &str {
95 &self.client_id
96 }
97
98 pub fn access_token(&self) -> &str {
100 &self.access_token
101 }
102
103 pub fn set_access_token(&mut self, token: impl Into<String>) {
105 self.access_token = token.into();
106 self.auth_header_token = HeaderValue::from_str(&self.access_token)
107 .expect("access token contains invalid header characters");
108 }
109
110 pub fn base_url(&self) -> &str {
112 &self.base_url
113 }
114
115 pub async fn get<R: DeserializeOwned>(&self, path: &str) -> Result<R> {
121 let url = self.url(path);
122 tracing::debug!(%url, "GET");
123
124 let resp = self
125 .http
126 .get(&url)
127 .headers(self.auth_headers())
128 .send()
129 .await?;
130
131 self.handle_response(resp).await
132 }
133
134 pub async fn post<B: Serialize, R: DeserializeOwned>(&self, path: &str, body: &B) -> Result<R> {
136 let url = self.url(path);
137 tracing::debug!(%url, "POST");
138
139 let resp = self
140 .http
141 .post(&url)
142 .headers(self.auth_headers())
143 .json(body)
144 .send()
145 .await?;
146
147 self.handle_response(resp).await
148 }
149
150 pub async fn put<B: Serialize, R: DeserializeOwned>(&self, path: &str, body: &B) -> Result<R> {
152 let url = self.url(path);
153 tracing::debug!(%url, "PUT");
154
155 let resp = self
156 .http
157 .put(&url)
158 .headers(self.auth_headers())
159 .json(body)
160 .send()
161 .await?;
162
163 self.handle_response(resp).await
164 }
165
166 pub async fn delete<R: DeserializeOwned>(&self, path: &str) -> Result<R> {
168 let url = self.url(path);
169 tracing::debug!(%url, "DELETE");
170
171 let resp = self
172 .http
173 .delete(&url)
174 .headers(self.auth_headers())
175 .send()
176 .await?;
177
178 self.handle_response(resp).await
179 }
180
181 pub async fn delete_no_content(&self, path: &str) -> Result<()> {
183 let url = self.url(path);
184 tracing::debug!(%url, "DELETE (no content)");
185
186 let resp = self
187 .http
188 .delete(&url)
189 .headers(self.auth_headers())
190 .send()
191 .await?;
192
193 let status = resp.status();
194 if status.is_success() {
195 Ok(())
196 } else {
197 let body = resp.text().await.unwrap_or_default();
198 Err(self.parse_error_body(status, &body))
199 }
200 }
201
202 pub async fn get_no_content(&self, path: &str) -> Result<()> {
204 let url = self.url(path);
205 tracing::debug!(%url, "GET (no content)");
206
207 let resp = self
208 .http
209 .get(&url)
210 .headers(self.auth_headers())
211 .send()
212 .await?;
213
214 let status = resp.status();
215 if status.is_success() {
216 Ok(())
217 } else {
218 let body = resp.text().await.unwrap_or_default();
219 Err(self.parse_error_body(status, &body))
220 }
221 }
222
223 pub async fn post_no_content<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
225 let url = self.url(path);
226 tracing::debug!(%url, "POST (no content)");
227
228 let resp = self
229 .http
230 .post(&url)
231 .headers(self.auth_headers())
232 .json(body)
233 .send()
234 .await?;
235
236 let status = resp.status();
237 if status.is_success() {
238 Ok(())
239 } else {
240 let body = resp.text().await.unwrap_or_default();
241 Err(self.parse_error_body(status, &body))
242 }
243 }
244
245 fn url(&self, path: &str) -> String {
251 if path.starts_with('/') {
252 format!("{}{}", self.base_url, path)
253 } else {
254 format!("{}/{}", self.base_url, path)
255 }
256 }
257
258 fn default_headers() -> HeaderMap {
260 let mut headers = HeaderMap::new();
261 headers.insert(
262 header::CONTENT_TYPE,
263 HeaderValue::from_static("application/json"),
264 );
265 headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
266 headers
267 }
268
269 fn auth_headers(&self) -> HeaderMap {
272 let mut headers = HeaderMap::with_capacity(2);
273 headers.insert("access-token", self.auth_header_token.clone());
274 headers.insert("client-id", self.auth_header_client_id.clone());
275 headers
276 }
277
278 async fn handle_response<R: DeserializeOwned>(&self, resp: reqwest::Response) -> Result<R> {
283 let status = resp.status();
284 let bytes = resp.bytes().await.unwrap_or_default();
285
286 if status.is_success() {
287 serde_json::from_slice(&bytes).map_err(DhanError::Json)
288 } else {
289 let body = String::from_utf8_lossy(&bytes);
291 Err(self.parse_error_body(status, &body))
292 }
293 }
294
295 pub(crate) fn parse_error_body(&self, status: reqwest::StatusCode, body: &str) -> DhanError {
298 if let Ok(api_err) = serde_json::from_str::<ApiErrorBody>(body) {
299 if api_err.error_code.is_some() || api_err.error_message.is_some() {
300 return DhanError::Api(api_err);
301 }
302 }
303 DhanError::HttpStatus {
304 status,
305 body: body.to_owned(),
306 }
307 }
308}