1use reqwest::header::USER_AGENT;
2use serde::{Serialize, de::DeserializeOwned};
3use std::time::Duration;
4use thiserror::Error;
5
6#[cfg(feature = "async")]
7use crate::endpoints::{
8 BankIdNoEndpoint, BankIdSeEndpoint, DocumentEndpoint, FrejaEndpoint, FtnEndpoint,
9 MitIdEndpoint, VippsEndpoint,
10};
11
12#[cfg(feature = "blocking")]
13use crate::endpoints::{
14 BankIdNoBlockingEndpoint, BankIdSeBlockingEndpoint, DocumentBlockingEndpoint,
15 FrejaBlockingEndpoint, FtnBlockingEndpoint, MitIdBlockingEndpoint, VippsBlockingEndpoint,
16};
17
18#[non_exhaustive]
20#[derive(Debug, Error)]
21pub enum IdkollenError {
22 #[error("HTTP error: {0}")]
24 Http(
25 #[from]
26 #[source]
27 reqwest::Error,
28 ),
29 #[error("API error {status}: {message}")]
31 Api { status: u16, message: String },
32 #[error("JSON error: {0}")]
34 Deserialization(
35 #[from]
36 #[source]
37 serde_path_to_error::Error<serde_json::Error>,
38 ),
39}
40
41#[non_exhaustive]
43#[derive(Debug, Error)]
44pub enum WaitError {
45 #[error("Poll timed out without reaching a terminal state")]
47 Timeout,
48 #[error(transparent)]
50 Client(#[from] IdkollenError),
51}
52
53#[non_exhaustive]
55#[derive(Debug, Clone)]
56pub enum Environment {
57 Production,
59 Staging,
61}
62
63impl Environment {
64 #[inline]
65 #[must_use]
66 fn base_url(&self) -> &'static str {
67 match self {
68 Self::Production => "https://api.idkollen.se",
69 Self::Staging => "https://stgapi.idkollen.se",
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct PollOptions {
77 pub interval: Duration,
79 pub timeout: Duration,
81}
82
83impl Default for PollOptions {
84 #[inline]
85 fn default() -> Self {
86 Self {
87 interval: Duration::from_secs(2),
88 timeout: Duration::from_secs(300),
89 }
90 }
91}
92
93pub struct IdkollenClientBuilder {
95 environment: Environment,
96 base_url: Option<String>,
97 client_id: String,
98 client_secret: String,
99 user_agent: String,
100 #[cfg(feature = "async")]
101 http_client: Option<reqwest::Client>,
102 #[cfg(feature = "blocking")]
103 blocking_http_client: Option<reqwest::blocking::Client>,
104}
105
106impl IdkollenClientBuilder {
107 #[must_use]
108 pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
109 Self {
110 environment: Environment::Production,
111 base_url: None,
112 client_id: client_id.into(),
113 client_secret: client_secret.into(),
114 user_agent: format!("idkollen-client-rs/{}", env!("CARGO_PKG_VERSION")),
115 #[cfg(feature = "async")]
116 http_client: None,
117 #[cfg(feature = "blocking")]
118 blocking_http_client: None,
119 }
120 }
121
122 #[inline]
123 #[must_use]
124 pub fn environment(mut self, env: Environment) -> Self {
125 self.environment = env;
126 self
127 }
128
129 #[inline]
130 #[must_use]
131 pub fn base_url(mut self, url: impl Into<String>) -> Self {
132 self.base_url = Some(url.into());
133 self
134 }
135
136 #[inline]
138 #[must_use]
139 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
140 self.user_agent = ua.into();
141 self
142 }
143
144 #[cfg(feature = "async")]
145 #[inline]
146 #[must_use]
147 pub fn http_client(mut self, client: reqwest::Client) -> Self {
148 self.http_client = Some(client);
149 self
150 }
151
152 #[cfg(feature = "blocking")]
153 #[inline]
154 #[must_use]
155 pub fn blocking_http_client(mut self, client: reqwest::blocking::Client) -> Self {
156 self.blocking_http_client = Some(client);
157 self
158 }
159
160 #[cfg(feature = "async")]
161 pub fn build(self) -> Result<IdkollenClient, IdkollenError> {
162 let base_url = self
163 .base_url
164 .unwrap_or_else(|| self.environment.base_url().to_owned());
165 let http = self.http_client.map(Ok).unwrap_or_else(|| {
166 reqwest::Client::builder()
167 .timeout(Duration::from_secs(30))
168 .build()
169 })?;
170
171 Ok(IdkollenClient {
172 http,
173 base_url,
174 client_id: self.client_id,
175 client_secret: self.client_secret,
176 user_agent: self.user_agent,
177 })
178 }
179
180 #[cfg(feature = "blocking")]
181 pub fn build_blocking(self) -> Result<IdkollenBlockingClient, IdkollenError> {
182 let base_url = self
183 .base_url
184 .unwrap_or_else(|| self.environment.base_url().to_owned());
185 let http = self.blocking_http_client.map(Ok).unwrap_or_else(|| {
186 reqwest::blocking::Client::builder()
187 .timeout(Duration::from_secs(30))
188 .build()
189 })?;
190
191 Ok(IdkollenBlockingClient {
192 http,
193 base_url,
194 client_id: self.client_id,
195 client_secret: self.client_secret,
196 user_agent: self.user_agent,
197 })
198 }
199}
200
201#[cfg(feature = "async")]
202pub struct IdkollenClient {
203 pub(crate) http: reqwest::Client,
204 pub(crate) base_url: String,
205 pub(crate) client_id: String,
206 pub(crate) client_secret: String,
207 pub(crate) user_agent: String,
208}
209
210#[cfg(feature = "async")]
211impl IdkollenClient {
212 #[inline]
213 #[must_use]
214 pub fn bankid_se(&self) -> BankIdSeEndpoint<'_> {
215 BankIdSeEndpoint(self)
216 }
217
218 #[inline]
219 #[must_use]
220 pub fn bankid_no(&self) -> BankIdNoEndpoint<'_> {
221 BankIdNoEndpoint(self)
222 }
223
224 #[inline]
225 #[must_use]
226 pub fn freja(&self) -> FrejaEndpoint<'_> {
227 FrejaEndpoint(self)
228 }
229
230 #[inline]
231 #[must_use]
232 pub fn mitid(&self) -> MitIdEndpoint<'_> {
233 MitIdEndpoint(self)
234 }
235
236 #[inline]
237 #[must_use]
238 pub fn ftn(&self) -> FtnEndpoint<'_> {
239 FtnEndpoint(self)
240 }
241
242 #[inline]
243 #[must_use]
244 pub fn vipps(&self) -> VippsEndpoint<'_> {
245 VippsEndpoint(self)
246 }
247
248 #[inline]
249 #[must_use]
250 pub fn document(&self) -> DocumentEndpoint<'_> {
251 DocumentEndpoint(self)
252 }
253
254 #[inline]
255 pub(crate) fn url(&self, path: &str) -> String {
256 format!("{}{}", self.base_url, path)
257 }
258
259 pub(crate) async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, IdkollenError> {
260 let resp = self
261 .http
262 .get(self.url(path))
263 .basic_auth(&self.client_id, Some(&self.client_secret))
264 .header(USER_AGENT, &self.user_agent)
265 .send()
266 .await?;
267
268 parse_response(resp).await
269 }
270
271 pub(crate) async fn post<B: Serialize, T: DeserializeOwned>(
272 &self,
273 path: &str,
274 body: &B,
275 ) -> Result<T, IdkollenError> {
276 let resp = self
277 .http
278 .post(self.url(path))
279 .basic_auth(&self.client_id, Some(&self.client_secret))
280 .header(USER_AGENT, &self.user_agent)
281 .json(body)
282 .send()
283 .await?;
284
285 parse_response(resp).await
286 }
287
288 pub(crate) async fn delete(&self, path: &str) -> Result<(), IdkollenError> {
289 let resp = self
290 .http
291 .delete(self.url(path))
292 .basic_auth(&self.client_id, Some(&self.client_secret))
293 .header(USER_AGENT, &self.user_agent)
294 .send()
295 .await?;
296
297 if resp.status().is_success() {
298 Ok(())
299 } else {
300 let status = resp.status().as_u16();
301 let message = resp.text().await.unwrap_or_default();
302
303 Err(IdkollenError::Api { status, message })
304 }
305 }
306
307 pub(crate) async fn post_multipart<T: DeserializeOwned>(
308 &self,
309 path: &str,
310 form: reqwest::multipart::Form,
311 ) -> Result<T, IdkollenError> {
312 let resp = self
313 .http
314 .post(self.url(path))
315 .basic_auth(&self.client_id, Some(&self.client_secret))
316 .header(USER_AGENT, &self.user_agent)
317 .multipart(form)
318 .send()
319 .await?;
320
321 parse_response(resp).await
322 }
323
324 pub(crate) async fn get_bytes(&self, path: &str) -> Result<Vec<u8>, IdkollenError> {
325 let resp = self
326 .http
327 .get(self.url(path))
328 .basic_auth(&self.client_id, Some(&self.client_secret))
329 .header(USER_AGENT, &self.user_agent)
330 .send()
331 .await?;
332
333 if resp.status().is_success() {
334 Ok(resp.bytes().await?.to_vec())
335 } else {
336 let status = resp.status().as_u16();
337 let message = resp.text().await.unwrap_or_default();
338
339 Err(IdkollenError::Api { status, message })
340 }
341 }
342}
343
344#[cfg(feature = "async")]
345async fn parse_response<T: DeserializeOwned>(resp: reqwest::Response) -> Result<T, IdkollenError> {
346 if resp.status().is_success() {
347 let text = resp.text().await?;
348 let deserializer = &mut serde_json::Deserializer::from_str(&text);
349
350 Ok(serde_path_to_error::deserialize(deserializer)?)
351 } else {
352 let status = resp.status().as_u16();
353 let message = resp.text().await.unwrap_or_default();
354
355 Err(IdkollenError::Api { status, message })
356 }
357}
358
359#[cfg(feature = "blocking")]
360pub struct IdkollenBlockingClient {
361 pub(crate) http: reqwest::blocking::Client,
362 pub(crate) base_url: String,
363 pub(crate) client_id: String,
364 pub(crate) client_secret: String,
365 pub(crate) user_agent: String,
366}
367
368#[cfg(feature = "blocking")]
369impl IdkollenBlockingClient {
370 #[inline]
371 #[must_use]
372 pub fn bankid_se(&self) -> BankIdSeBlockingEndpoint<'_> {
373 BankIdSeBlockingEndpoint(self)
374 }
375
376 #[inline]
377 #[must_use]
378 pub fn bankid_no(&self) -> BankIdNoBlockingEndpoint<'_> {
379 BankIdNoBlockingEndpoint(self)
380 }
381
382 #[inline]
383 #[must_use]
384 pub fn freja(&self) -> FrejaBlockingEndpoint<'_> {
385 FrejaBlockingEndpoint(self)
386 }
387
388 #[inline]
389 #[must_use]
390 pub fn mitid(&self) -> MitIdBlockingEndpoint<'_> {
391 MitIdBlockingEndpoint(self)
392 }
393
394 #[inline]
395 #[must_use]
396 pub fn ftn(&self) -> FtnBlockingEndpoint<'_> {
397 FtnBlockingEndpoint(self)
398 }
399
400 #[inline]
401 #[must_use]
402 pub fn vipps(&self) -> VippsBlockingEndpoint<'_> {
403 VippsBlockingEndpoint(self)
404 }
405
406 #[inline]
407 #[must_use]
408 pub fn document(&self) -> DocumentBlockingEndpoint<'_> {
409 DocumentBlockingEndpoint(self)
410 }
411
412 #[inline]
413 pub(crate) fn url(&self, path: &str) -> String {
414 format!("{}{}", self.base_url, path)
415 }
416
417 pub(crate) fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, IdkollenError> {
418 let resp = self
419 .http
420 .get(self.url(path))
421 .basic_auth(&self.client_id, Some(&self.client_secret))
422 .header(USER_AGENT, &self.user_agent)
423 .send()?;
424
425 parse_blocking_response(resp)
426 }
427
428 pub(crate) fn post<B: Serialize, T: DeserializeOwned>(
429 &self,
430 path: &str,
431 body: &B,
432 ) -> Result<T, IdkollenError> {
433 let resp = self
434 .http
435 .post(self.url(path))
436 .basic_auth(&self.client_id, Some(&self.client_secret))
437 .header(USER_AGENT, &self.user_agent)
438 .json(body)
439 .send()?;
440
441 parse_blocking_response(resp)
442 }
443
444 pub(crate) fn delete(&self, path: &str) -> Result<(), IdkollenError> {
445 let resp = self
446 .http
447 .delete(self.url(path))
448 .basic_auth(&self.client_id, Some(&self.client_secret))
449 .header(USER_AGENT, &self.user_agent)
450 .send()?;
451
452 if resp.status().is_success() {
453 Ok(())
454 } else {
455 let status = resp.status().as_u16();
456 let message = resp.text().unwrap_or_default();
457
458 Err(IdkollenError::Api { status, message })
459 }
460 }
461
462 pub(crate) fn post_multipart<T: DeserializeOwned>(
463 &self,
464 path: &str,
465 form: reqwest::blocking::multipart::Form,
466 ) -> Result<T, IdkollenError> {
467 let resp = self
468 .http
469 .post(self.url(path))
470 .basic_auth(&self.client_id, Some(&self.client_secret))
471 .header(USER_AGENT, &self.user_agent)
472 .multipart(form)
473 .send()?;
474
475 parse_blocking_response(resp)
476 }
477
478 pub(crate) fn get_bytes(&self, path: &str) -> Result<Vec<u8>, IdkollenError> {
479 let resp = self
480 .http
481 .get(self.url(path))
482 .basic_auth(&self.client_id, Some(&self.client_secret))
483 .header(USER_AGENT, &self.user_agent)
484 .send()?;
485
486 if resp.status().is_success() {
487 Ok(resp.bytes()?.to_vec())
488 } else {
489 let status = resp.status().as_u16();
490 let message = resp.text().unwrap_or_default();
491
492 Err(IdkollenError::Api { status, message })
493 }
494 }
495}
496
497#[cfg(feature = "blocking")]
498fn parse_blocking_response<T: DeserializeOwned>(
499 resp: reqwest::blocking::Response,
500) -> Result<T, IdkollenError> {
501 if resp.status().is_success() {
502 let text = resp.text()?;
503 let deserializer = &mut serde_json::Deserializer::from_str(&text);
504
505 Ok(serde_path_to_error::deserialize(deserializer)?)
506 } else {
507 let status = resp.status().as_u16();
508 let message = resp.text().unwrap_or_default();
509
510 Err(IdkollenError::Api { status, message })
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use crate::models::{BankIdSePhoneAuthRequest, CallInitiator, Pno};
518
519 #[tokio::test]
520 async fn test() {
521 let client = IdkollenClientBuilder::new("494c2afa-fb68-4891-9b3b-8a0056771707", "123456")
522 .environment(Environment::Staging)
523 .build()
524 .unwrap();
525
526 let response = client
527 .bankid_se()
528 .phone_auth(BankIdSePhoneAuthRequest::new(
529 Pno::parse("9012073731").unwrap(),
530 CallInitiator::User,
531 ))
532 .await
533 .unwrap();
534
535 println!("{:#?}", response);
536 }
537}