dfns_sdk_rs/utils/
fetch.rs

1// @dfns-sdk-rs/src/utils/fetch.rs
2
3use crate::{
4    error::{DfnsError, PolicyPendingError},
5    models::generic::DfnsBaseApiOptions,
6    utils::nonce::generate_nonce,
7};
8use reqwest::{Client, Method, Response, StatusCode};
9use serde::{de::DeserializeOwned, Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12use url::Url;
13
14const DEFAULT_DFNS_BASE_URL: &str = "https://api.dfns.io";
15const VERSION: &str = env!("CARGO_PKG_VERSION");
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "UPPERCASE")]
19pub enum HttpMethod {
20    GET,
21    POST,
22    PUT,
23    DELETE,
24}
25
26impl From<HttpMethod> for Method {
27    fn from(method: HttpMethod) -> Self {
28        match method {
29            HttpMethod::GET => Method::GET,
30            HttpMethod::POST => Method::POST,
31            HttpMethod::PUT => Method::PUT,
32            HttpMethod::DELETE => Method::DELETE,
33        }
34    }
35}
36
37#[derive(Debug, Clone)]
38pub struct FetchOptions<T> {
39    pub method: HttpMethod,
40    pub headers: Option<HashMap<String, String>>,
41    pub body: Option<Value>,
42    pub api_options: T,
43}
44
45pub type FetchResult = Result<Response, DfnsError>;
46
47pub trait Fetch {
48    #[allow(async_fn_in_trait)]
49    async fn execute(&self, url: &str, options: FetchOptions<DfnsBaseApiOptions>) -> FetchResult;
50}
51
52pub struct DfnsFetch {
53    client: Client,
54}
55
56impl Clone for DfnsFetch {
57    fn clone(&self) -> Self {
58        Self {
59            client: Client::new(),
60        }
61    }
62}
63
64impl std::fmt::Debug for DfnsFetch {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.debug_struct("DfnsFetch")
67            .field("client", &"Client")
68            .finish()
69    }
70}
71
72impl PartialEq for DfnsFetch {
73    fn eq(&self, _other: &Self) -> bool {
74        true
75    }
76}
77
78impl DfnsFetch {
79    pub fn new() -> Self {
80        Self {
81            client: Client::new(),
82        }
83    }
84
85    pub async fn handle_response(&self, response: Response) -> FetchResult {
86        if response.status().is_success() && response.status() != StatusCode::ACCEPTED {
87            Ok(response)
88        } else {
89            let status = response.status();
90            let body: Value = response.json().await.unwrap_or_default();
91
92            if status == StatusCode::ACCEPTED {
93                Err(PolicyPendingError::new(Some(body)).into())
94            } else {
95                let message = body
96                    .get("error")
97                    .and_then(|e| e.get("message"))
98                    .or_else(|| body.get("message"))
99                    .and_then(|m| m.as_str())
100                    .unwrap_or("Unknown error")
101                    .to_string();
102
103                Err(DfnsError::new(status.as_u16(), message, Some(body)))
104            }
105        }
106    }
107}
108
109impl Fetch for DfnsFetch {
110    async fn execute(
111        &self,
112        resource: &str,
113        options: FetchOptions<DfnsBaseApiOptions>,
114    ) -> FetchResult {
115        let base_url = options
116            .api_options
117            .base_url
118            .unwrap_or_else(|| DEFAULT_DFNS_BASE_URL.to_string());
119        let url = Url::parse(&base_url)?.join(resource)?;
120
121        let mut headers = reqwest::header::HeaderMap::new();
122
123        headers.insert("x-dfns-appid", options.api_options.app_id.parse()?);
124        headers.insert("x-dfns-nonce", generate_nonce().parse()?);
125        headers.insert("x-dfns-sdk-version", VERSION.parse()?);
126
127        if let Some(app_secret) = options.api_options.app_secret {
128            headers.insert("x-dfns-appsecret", app_secret.parse()?);
129        }
130
131        if let Some(auth_token) = options.api_options.auth_token {
132            headers.insert("authorization", format!("Bearer {}", auth_token).parse()?);
133        }
134
135        if let Some(custom_headers) = options.headers {
136            for (key, value) in custom_headers {
137                let key_str: &'static str = Box::leak(key.into_boxed_str());
138                headers.insert(key_str, value.parse()?);
139            }
140        }
141
142        let mut request = self
143            .client
144            .request(options.method.into(), url)
145            .headers(headers);
146
147        if let Some(body) = options.body {
148            request = request
149                .header("content-type", "application/json")
150                .json(&body);
151        }
152
153        request.send().await.map_err(|e| e.into())
154    }
155}
156
157pub async fn simple_fetch<T: Serialize + DeserializeOwned>(
158    resource: &str,
159    options: FetchOptions<DfnsBaseApiOptions>,
160) -> Result<T, DfnsError> {
161    let fetch = DfnsFetch::new();
162    let response = fetch.execute(resource, options).await?;
163    response.json::<T>().await.map_err(|e| e.into())
164}