ddapi_rs/api/
mod.rs

1use crate::error::ApiError;
2use anyhow::{Context, Result};
3#[cfg(feature = "cache")]
4use moka::future::Cache;
5use reqwest::Client;
6use serde::de::DeserializeOwned;
7#[allow(unused_imports)]
8use std::time::Duration;
9
10#[derive(Clone, Default)]
11pub struct DDApi {
12    client: Client,
13    #[cfg(feature = "cache")]
14    cache: Option<Cache<String, String>>,
15}
16
17impl DDApi {
18    /// Creates a new DDApi instance with default settings
19    ///
20    /// # Examples
21    ///
22    /// ```
23    /// use ddapi_rs::prelude::*;
24    ///
25    /// let api = DDApi::new();
26    /// ```
27    pub fn new() -> Self {
28        DDApi {
29            client: Client::new(),
30            #[cfg(feature = "cache")]
31            cache: None,
32        }
33    }
34
35    /// Creates a new DDApi instance with a custom HTTP client
36    ///
37    /// This allows you to configure your own client with custom timeouts,
38    /// headers, or other settings.
39    ///
40    /// # Arguments
41    ///
42    /// * `client` - A pre-configured `reqwest::Client` instance
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use ddapi_rs::prelude::*;
48    /// use reqwest::Client;
49    ///
50    /// let client = Client::builder()
51    ///     .timeout(std::time::Duration::from_secs(10))
52    ///     .build()
53    ///     .unwrap();
54    /// let api = DDApi::new_with_client(client);
55    /// ```
56    pub fn new_with_client(client: Client) -> Self {
57        DDApi {
58            client,
59            #[cfg(feature = "cache")]
60            cache: None,
61        }
62    }
63
64    /// Configures caching for API responses
65    ///
66    /// When the `cache` feature is enabled, this method allows you to set up
67    /// an in-memory cache to reduce API calls and improve performance.
68    ///
69    /// # Arguments
70    ///
71    /// * `capacity` - Maximum number of entries to store in the cache
72    /// * `time_to_live` - Time in seconds before cached entries expire
73    ///
74    /// # Examples
75    ///
76    ///
77    /// ```ignore
78    /// use ddapi_rs::prelude::*;
79    /// use std::time::Duration;
80    ///
81    /// let mut api = DDApi::new();
82    /// api.set_cache(1000, Duration::from_mins(5)); // Cache 1000 items for 5 minutes
83    /// ```
84    #[cfg(feature = "cache")]
85    pub fn set_cache(&mut self, capacity: u64, time_to_live: Duration) {
86        self.cache = Some(
87            Cache::builder()
88                .max_capacity(capacity)
89                .time_to_live(time_to_live)
90                .build(),
91        );
92    }
93
94    /// Sends an HTTP GET request to the specified URL and returns the response text
95    ///
96    /// # Arguments
97    ///
98    /// * `url` - The URL to send the request to
99    ///
100    /// # Returns
101    ///
102    /// Returns `Result<String>` with the response body on success
103    async fn send_request(&self, url: &str) -> Result<String> {
104        let response = self
105            .client
106            .get(url)
107            .send()
108            .await
109            .context("Failed to send request")?;
110
111        let text = response
112            .text()
113            .await
114            .context("Failed to read response body")?;
115
116        if text.is_empty() {
117            anyhow::bail!("API returned empty response");
118        }
119
120        #[cfg(feature = "ddnet")]
121        if text == "{}" {
122            return Err(anyhow::Error::from(ApiError::NotFound));
123        }
124
125        Ok(text)
126    }
127
128    /// Executes an API request and deserializes the JSON response
129    ///
130    /// This method handles API requests and automatically deserializes the JSON response
131    /// into the specified type. The caching behavior is determined by the `cache` feature flag.
132    ///
133    /// # Type Parameters
134    ///
135    /// * `T` - The type to deserialize the response into. Must implement
136    ///   `DeserializeOwned + Send + Sync + 'static`
137    ///
138    /// # Arguments
139    ///
140    /// * `url` - The API endpoint URL to request
141    /// # Returns
142    ///
143    /// # Returns
144    ///
145    /// `Result<T>` containing the deserialized data on success, or an error on failure
146    pub async fn _generator<T>(&self, url: &str) -> Result<T>
147    where
148        T: DeserializeOwned + Send + Sync + 'static,
149    {
150        #[cfg(feature = "cache")]
151        {
152            self._generator_cached(url).await
153        }
154        #[cfg(not(feature = "cache"))]
155        {
156            self._generator_no_cache(url).await
157        }
158    }
159
160    /// Executes a cached API request
161    ///
162    /// Checks the cache first for existing responses. If not found in cache,
163    /// fetches from the API and stores the response in cache.
164    ///
165    /// # Type Parameters
166    ///
167    /// * `T` - The type to deserialize the response into
168    ///
169    /// # Arguments
170    ///
171    /// * `url` - The API endpoint URL to request
172    ///
173    /// # Returns
174    ///
175    /// Returns `Result<T>` with deserialized data from cache or API
176    #[cfg(feature = "cache")]
177    async fn _generator_cached<T>(&self, url: &str) -> Result<T>
178    where
179        T: DeserializeOwned + Send + Sync + 'static,
180    {
181        let type_name = std::any::type_name::<T>();
182        let cache_key = format!("{}:{}", type_name, url);
183
184        match &self.cache {
185            Some(cache) => {
186                if let Some(value) = cache.get(&cache_key).await {
187                    self.parse_response::<T>(&value)
188                } else {
189                    let response_text = self.send_request(url).await?;
190                    cache.insert(cache_key, response_text.clone()).await;
191                    self.parse_response::<T>(&response_text)
192                }
193            }
194            None => self._generator_no_cache(url).await,
195        }
196    }
197
198    /// Executes an API request without caching
199    ///
200    /// Always fetches fresh data from the API, bypassing any cache.
201    ///
202    /// # Type Parameters
203    ///
204    /// * `T` - The type to deserialize the response into
205    ///
206    /// # Arguments
207    ///
208    /// * `url` - The API endpoint URL to request
209    ///
210    /// # Returns
211    ///
212    /// Returns `Result<T>` with freshly fetched deserialized data
213    pub async fn _generator_no_cache<T>(&self, url: &str) -> Result<T>
214    where
215        T: DeserializeOwned,
216    {
217        let response_text = self.send_request(url).await?;
218        self.parse_response::<T>(&response_text)
219    }
220
221    /// Parses the API response text into the desired type
222    ///
223    /// # Type Parameters
224    ///
225    /// * `T` - The type to deserialize the response into
226    ///
227    /// # Arguments
228    ///
229    /// * `response_text` - The raw response text from the API
230    ///
231    /// # Returns
232    ///
233    /// Returns `Result<T>` with the deserialized data on success, or an appropriate error
234    fn parse_response<T>(&self, response_text: &str) -> Result<T>
235    where
236        T: DeserializeOwned,
237    {
238        // ddnet
239        #[cfg(feature = "ddnet")]
240        if response_text == "{}" {
241            return Err(anyhow::Error::from(ApiError::NotFound));
242        }
243
244        // ddstats
245        #[cfg(feature = "ddstats")]
246        if let Ok(error_response) = serde_json::from_str::<serde_json::Value>(response_text) {
247            if let Some(error_msg) = error_response.get("error").and_then(|e| e.as_str()) {
248                return match error_msg.to_lowercase().as_str() {
249                    "player not found" => Err(anyhow::Error::from(ApiError::NotFound)),
250                    _ => Err(anyhow::anyhow!(error_msg.to_string())),
251                };
252            }
253        }
254
255        serde_json::from_str(response_text).map_err(Into::into)
256    }
257}
258
259#[cfg(feature = "ddnet")]
260pub mod ddnet;
261
262#[cfg(feature = "ddstats")]
263pub mod ddstats;