1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3use std::time::{Duration, Instant};
4
5use serde::de::DeserializeOwned;
6
7use crate::error::{Error, Result};
8use crate::ratelimit::{RateLimit, SharedRateLimit};
9
10const DEFAULT_BASE_URL: &str = "https://api.hypixel.net";
11const API_KEY_HEADER: &str = "API-Key";
12
13#[derive(Debug, Clone)]
19pub struct HypixelClient {
20 http: reqwest::Client,
21 base_url: String,
22 api_key: Option<String>,
23 rate_limit: SharedRateLimit,
24 retry_rate_limited: u32,
25 cache: Option<ResponseCache>,
26}
27
28type CacheEntries = Arc<Mutex<HashMap<String, (Instant, Arc<[u8]>)>>>;
29
30#[derive(Debug, Clone)]
31struct ResponseCache {
32 ttl: Duration,
33 entries: CacheEntries,
34}
35
36impl ResponseCache {
37 fn get(&self, key: &str) -> Option<Arc<[u8]>> {
38 let entries = self.entries.lock().ok()?;
39 let (stored_at, bytes) = entries.get(key)?;
40 (stored_at.elapsed() < self.ttl).then(|| bytes.clone())
41 }
42
43 fn put(&self, key: String, bytes: Arc<[u8]>) {
44 if let Ok(mut entries) = self.entries.lock() {
45 entries.retain(|_, (stored_at, _)| stored_at.elapsed() < self.ttl);
46 entries.insert(key, (Instant::now(), bytes));
47 }
48 }
49}
50
51impl HypixelClient {
52 pub fn new(api_key: impl Into<String>) -> Self {
56 Self::builder().api_key(api_key).build()
57 }
58
59 pub fn unauthenticated() -> Self {
66 Self::builder().build()
67 }
68
69 pub fn builder() -> ClientBuilder {
71 ClientBuilder::default()
72 }
73
74 pub fn rate_limit(&self) -> RateLimit {
76 self.rate_limit.load()
77 }
78
79 pub(crate) async fn request<T: DeserializeOwned>(
80 &self,
81 path: &str,
82 query: &[(&str, &str)],
83 requires_key: bool,
84 ) -> Result<T> {
85 if requires_key && self.api_key.is_none() {
86 return Err(Error::MissingApiKey);
87 }
88
89 let cache_key = self.cache.as_ref().map(|cache| {
90 let mut key = path.to_string();
91 for (name, value) in query {
92 key.push_str(&format!("&{name}={value}"));
93 }
94 (cache, key)
95 });
96 if let Some((cache, key)) = &cache_key {
97 if let Some(bytes) = cache.get(key) {
98 return Ok(serde_json::from_slice(&bytes)?);
99 }
100 }
101
102 let mut attempt = 0;
103 let bytes = loop {
104 match self.send(path, query).await {
105 Ok(bytes) => break bytes,
106 Err(Error::RateLimited { retry_after }) if attempt < self.retry_rate_limited => {
107 attempt += 1;
108 tokio::time::sleep(retry_after.unwrap_or(Duration::from_secs(1))).await;
109 }
110 Err(err) => return Err(err),
111 }
112 };
113
114 if let Some((cache, key)) = cache_key {
115 cache.put(key, bytes.clone());
116 }
117 Ok(serde_json::from_slice(&bytes)?)
118 }
119
120 async fn send(&self, path: &str, query: &[(&str, &str)]) -> Result<Arc<[u8]>> {
121 let url = format!("{}{}", self.base_url, path);
122 let mut request = self.http.get(url);
123 if let Some(key) = &self.api_key {
124 request = request.header(API_KEY_HEADER, key);
125 }
126 if !query.is_empty() {
127 request = request.query(query);
128 }
129
130 let response = request.send().await?;
131 let status = response.status();
132 let headers = response.headers().clone();
133 self.rate_limit.store(RateLimit::from_headers(&headers));
134
135 if status.is_success() {
136 let bytes = response.bytes().await?;
137 return Ok(Arc::from(bytes.as_ref()));
138 }
139
140 match status.as_u16() {
141 403 => Err(Error::InvalidApiKey),
142 429 => {
143 let retry_after = headers
144 .get("retry-after")
145 .and_then(|v| v.to_str().ok())
146 .and_then(|v| v.trim().parse().ok())
147 .map(Duration::from_secs);
148 Err(Error::RateLimited { retry_after })
149 }
150 code => {
151 let cause = response
152 .json::<ErrorBody>()
153 .await
154 .ok()
155 .and_then(|b| b.cause)
156 .unwrap_or_else(|| "unknown error".to_string());
157 Err(Error::Api {
158 status: code,
159 cause,
160 })
161 }
162 }
163 }
164}
165
166#[derive(serde::Deserialize)]
167struct ErrorBody {
168 cause: Option<String>,
169}
170
171#[derive(Debug, Default)]
173pub struct ClientBuilder {
174 api_key: Option<String>,
175 base_url: Option<String>,
176 http: Option<reqwest::Client>,
177 timeout: Option<Duration>,
178 retry_rate_limited: u32,
179 cache_ttl: Option<Duration>,
180}
181
182impl ClientBuilder {
183 pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
185 self.api_key = Some(api_key.into());
186 self
187 }
188
189 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
191 self.base_url = Some(base_url.into());
192 self
193 }
194
195 pub fn http_client(mut self, http: reqwest::Client) -> Self {
197 self.http = Some(http);
198 self
199 }
200
201 pub fn timeout(mut self, timeout: Duration) -> Self {
203 self.timeout = Some(timeout);
204 self
205 }
206
207 pub fn retry_on_rate_limit(mut self, retries: u32) -> Self {
211 self.retry_rate_limited = retries;
212 self
213 }
214
215 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
223 self.cache_ttl = Some(ttl);
224 self
225 }
226
227 pub fn build(self) -> HypixelClient {
229 let http = self.http.unwrap_or_else(|| {
230 let mut builder = reqwest::Client::builder();
231 if let Some(timeout) = self.timeout {
232 builder = builder.timeout(timeout);
233 }
234 builder.build().unwrap_or_default()
235 });
236 HypixelClient {
237 http,
238 base_url: self
239 .base_url
240 .unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
241 api_key: self.api_key,
242 rate_limit: SharedRateLimit::default(),
243 retry_rate_limited: self.retry_rate_limited,
244 cache: self.cache_ttl.map(|ttl| ResponseCache {
245 ttl,
246 entries: Arc::default(),
247 }),
248 }
249 }
250}