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;