ddapi_rs/api/
mod.rs

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