1use reqwest::{
22 header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE},
23 Method, Response,
24};
25use serde::de::DeserializeOwned;
26use serde::Serialize;
27
28use crate::error::Error;
29use nordnet_model::auth::Session;
30
31#[derive(Debug, Clone)]
34pub struct Client {
35 http: reqwest::Client,
36 base_url: String,
37 session: Option<Session>,
38}
39
40impl Client {
41 pub fn new(base_url: impl Into<String>) -> Result<Self, Error> {
45 let http = reqwest::Client::builder()
46 .build()
47 .map_err(Error::Transport)?;
48 Ok(Self {
49 http,
50 base_url: base_url.into().trim_end_matches('/').to_owned(),
51 session: None,
52 })
53 }
54
55 pub fn with_session(mut self, session: Session) -> Self {
58 self.session = Some(session);
59 self
60 }
61
62 pub fn session(&self) -> Option<&Session> {
64 self.session.as_ref()
65 }
66
67 pub fn base_url(&self) -> &str {
69 &self.base_url
70 }
71
72 pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
74 self.send::<T, ()>(Method::GET, path, None).await
75 }
76
77 pub async fn post<T: DeserializeOwned, B: Serialize>(
79 &self,
80 path: &str,
81 body: &B,
82 ) -> Result<T, Error> {
83 self.send(Method::POST, path, Some(Body::Json(body))).await
84 }
85
86 pub async fn put<T: DeserializeOwned, B: Serialize>(
88 &self,
89 path: &str,
90 body: &B,
91 ) -> Result<T, Error> {
92 self.send(Method::PUT, path, Some(Body::Json(body))).await
93 }
94
95 pub async fn post_form<T: DeserializeOwned, B: Serialize>(
102 &self,
103 path: &str,
104 body: &B,
105 ) -> Result<T, Error> {
106 self.send(Method::POST, path, Some(Body::Form(body))).await
107 }
108
109 pub async fn put_form<T: DeserializeOwned, B: Serialize>(
115 &self,
116 path: &str,
117 body: &B,
118 ) -> Result<T, Error> {
119 self.send(Method::PUT, path, Some(Body::Form(body))).await
120 }
121
122 pub async fn put_empty<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
126 self.send::<T, ()>(Method::PUT, path, None).await
127 }
128
129 pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
131 self.send::<T, ()>(Method::DELETE, path, None).await
132 }
133
134 pub fn url(&self, path: &str) -> String {
137 if path.starts_with('/') {
138 format!("{}{}", self.base_url, path)
139 } else {
140 format!("{}/{}", self.base_url, path)
141 }
142 }
143
144 fn auth_headers(&self) -> Result<HeaderMap, Error> {
145 let mut headers = HeaderMap::new();
146 if let Some(session) = &self.session {
147 let value = session.basic_auth_header();
148 let header =
149 HeaderValue::from_str(&value).map_err(|e| Error::InvalidHeader(e.to_string()))?;
150 headers.insert(AUTHORIZATION, header);
151 }
152 Ok(headers)
153 }
154
155 async fn send<T: DeserializeOwned, B: Serialize>(
156 &self,
157 method: Method,
158 path: &str,
159 body: Option<Body<'_, B>>,
160 ) -> Result<T, Error> {
161 let url = self.url(path);
162 let headers = self.auth_headers()?;
163 let response = self.execute_once(method, &url, headers, body).await?;
164 parse_response::<T>(response).await
165 }
166
167 async fn execute_once<B: Serialize>(
168 &self,
169 method: Method,
170 url: &str,
171 headers: HeaderMap,
172 body: Option<Body<'_, B>>,
173 ) -> Result<Response, Error> {
174 let mut req = self.http.request(method, url).headers(headers);
175 match body {
176 Some(Body::Json(b)) => {
177 req = req.header(CONTENT_TYPE, "application/json").json(b);
178 }
179 Some(Body::Form(b)) => {
180 let encoded =
184 serde_urlencoded::to_string(b).map_err(|e| Error::EncodeForm(e.to_string()))?;
185 req = req
186 .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
187 .body(encoded);
188 }
189 None => {}
190 }
191 req.send().await.map_err(Error::Transport)
192 }
193}
194
195enum Body<'a, B: Serialize> {
199 Json(&'a B),
200 Form(&'a B),
201}
202
203async fn parse_response<T: DeserializeOwned>(response: Response) -> Result<T, Error> {
205 let status = response.status();
206 let body = response.text().await.map_err(Error::Transport)?;
207
208 if status.is_success() {
209 serde_json::from_str::<T>(&body).map_err(|source| Error::Decode { source, body })
210 } else {
211 Err(Error::from_status(status.as_u16(), body))
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn url_handles_leading_slash() {
221 let c = Client::new("http://example.com/api/2").unwrap();
222 assert_eq!(c.url("/accounts"), "http://example.com/api/2/accounts");
223 assert_eq!(c.url("accounts"), "http://example.com/api/2/accounts");
224 }
225
226 #[test]
227 fn url_strips_trailing_slash_on_base() {
228 let c = Client::new("http://example.com/api/2/").unwrap();
229 assert_eq!(c.url("/x"), "http://example.com/api/2/x");
230 }
231
232 #[test]
233 fn no_session_no_auth_header() {
234 let c = Client::new("http://x").unwrap();
235 let h = c.auth_headers().unwrap();
236 assert!(!h.contains_key(AUTHORIZATION));
237 }
238
239 #[test]
240 fn with_session_sets_basic_auth() {
241 let c = Client::new("http://x").unwrap().with_session(Session {
242 session_key: "abc".into(),
243 expires_in: 60,
244 });
245 let h = c.auth_headers().unwrap();
246 assert_eq!(
247 h.get(AUTHORIZATION).unwrap().to_str().unwrap(),
248 "Basic YWJjOmFiYw=="
249 );
250 }
251}