ddapi_rs/api/
mod.rs

1use anyhow::{anyhow, Context, Result};
2#[cfg(feature = "cache")]
3use moka::future::Cache;
4use reqwest::Client;
5use serde::de::DeserializeOwned;
6use crate::error::ApiError;
7
8#[derive(Clone, Default)]
9pub struct DDApi {
10    client: Client,
11    #[cfg(feature = "cache")]
12    cache: Option<Cache<String, String>>,
13}
14
15impl DDApi {
16    pub fn new() -> Self {
17        DDApi {
18            client: Client::new(),
19            #[cfg(feature = "cache")]
20            cache: None,
21        }
22    }
23
24    pub fn new_with_client(client: Client) -> Self {
25        DDApi {
26            client,
27            #[cfg(feature = "cache")]
28            cache: None,
29        }
30    }
31
32    #[cfg(feature = "cache")]
33    pub fn set_cache(&mut self, capacity: u64, time_to_live: u64) {
34        use std::time::Duration;
35
36        self.cache = Some(
37            Cache::builder()
38                .max_capacity(capacity)
39                .time_to_live(Duration::from_secs(time_to_live))
40                .build(),
41        );
42    }
43
44    async fn send_request(&self, uri: &str) -> Result<String> {
45        let response = self
46            .client
47            .get(uri)
48            .send()
49            .await
50            .context("Failed to send request")?;
51
52        let text = response
53            .text()
54            .await
55            .context("Failed to read response body")?;
56
57        if text.is_empty() {
58            anyhow::bail!("API returned empty response");
59        }
60
61        Ok(text)
62    }
63
64    #[allow(unused_variables)]
65    pub async fn _generator<T>(&self, uri: &str, cache: bool) -> Result<T>
66    where
67        T: DeserializeOwned + Send + Sync + 'static,
68    {
69        #[cfg(feature = "cache")]
70        if cache {
71            self._generator_cached(uri).await
72        } else {
73            self._generator_no_cache(uri).await
74        }
75        #[cfg(not(feature = "cache"))]
76        self._generator_no_cache(uri).await
77    }
78
79    #[cfg(feature = "cache")]
80    async fn _generator_cached<T>(&self, uri: &str) -> Result<T>
81    where
82        T: DeserializeOwned + Send + Sync + 'static,
83    {
84        let type_name = std::any::type_name::<T>();
85        let cache_key = format!("{}:{}", type_name, uri);
86
87        match &self.cache {
88            Some(cache) => {
89                if let Some(value) = cache.get(&cache_key).await {
90                    self.parse_response::<T>(&value)
91                } else {
92                    let response_text = self.send_request(uri).await?;
93                    cache.insert(cache_key, response_text.clone()).await;
94                    self.parse_response::<T>(&response_text)
95                }
96            }
97            None => self._generator_no_cache(uri).await,
98        }
99    }
100
101    async fn _generator_no_cache<T>(&self, uri: &str) -> Result<T>
102    where
103        T: DeserializeOwned,
104    {
105        let response_text = self.send_request(uri).await?;
106        self.parse_response::<T>(&response_text)
107    }
108
109    fn parse_response<T>(&self, response_text: &str) -> Result<T>
110    where
111        T: DeserializeOwned,
112    {
113        if let Ok(error_response) = serde_json::from_str::<serde_json::Value>(response_text) {
114            if let Some(error_msg) = error_response.get("error").and_then(|e| e.as_str()) {
115                return match error_msg.to_lowercase().as_str() {
116                    "player not found" => Err(anyhow::Error::from(ApiError::NotFound)),
117                    _ => Err(anyhow!(error_msg.to_string())),
118                };
119            }
120        }
121
122        serde_json::from_str(response_text).map_err(Into::into)
123    }
124}
125
126#[cfg(feature = "ddnet")]
127pub mod ddnet;
128
129#[cfg(feature = "ddstats")]
130pub mod ddstats;