1use std::time::Duration;
2
3use serde::de::DeserializeOwned;
4
5use crate::error::{Error, Result};
6use crate::ratelimit::{RateLimit, SharedRateLimit};
7
8const DEFAULT_BASE_URL: &str = "https://api.hypixel.net";
9const API_KEY_HEADER: &str = "API-Key";
10
11#[derive(Debug, Clone)]
17pub struct HypixelClient {
18 http: reqwest::Client,
19 base_url: String,
20 api_key: Option<String>,
21 rate_limit: SharedRateLimit,
22}
23
24impl HypixelClient {
25 pub fn new(api_key: impl Into<String>) -> Self {
29 Self::builder().api_key(api_key).build()
30 }
31
32 pub fn unauthenticated() -> Self {
39 Self::builder().build()
40 }
41
42 pub fn builder() -> ClientBuilder {
44 ClientBuilder::default()
45 }
46
47 pub fn rate_limit(&self) -> RateLimit {
49 self.rate_limit.load()
50 }
51
52 pub(crate) async fn request<T: DeserializeOwned>(
53 &self,
54 path: &str,
55 query: &[(&str, &str)],
56 requires_key: bool,
57 ) -> Result<T> {
58 if requires_key && self.api_key.is_none() {
59 return Err(Error::MissingApiKey);
60 }
61
62 let url = format!("{}{}", self.base_url, path);
63 let mut request = self.http.get(url);
64 if let Some(key) = &self.api_key {
65 request = request.header(API_KEY_HEADER, key);
66 }
67 if !query.is_empty() {
68 request = request.query(query);
69 }
70
71 let response = request.send().await?;
72 let status = response.status();
73 let headers = response.headers().clone();
74 self.rate_limit.store(RateLimit::from_headers(&headers));
75
76 if status.is_success() {
77 let bytes = response.bytes().await?;
78 return Ok(serde_json::from_slice(&bytes)?);
79 }
80
81 match status.as_u16() {
82 403 => Err(Error::InvalidApiKey),
83 429 => {
84 let retry_after = headers
85 .get("retry-after")
86 .and_then(|v| v.to_str().ok())
87 .and_then(|v| v.trim().parse().ok())
88 .map(Duration::from_secs);
89 Err(Error::RateLimited { retry_after })
90 }
91 code => {
92 let cause = response
93 .json::<ErrorBody>()
94 .await
95 .ok()
96 .and_then(|b| b.cause)
97 .unwrap_or_else(|| "unknown error".to_string());
98 Err(Error::Api {
99 status: code,
100 cause,
101 })
102 }
103 }
104 }
105}
106
107#[derive(serde::Deserialize)]
108struct ErrorBody {
109 cause: Option<String>,
110}
111
112#[derive(Debug, Default)]
114pub struct ClientBuilder {
115 api_key: Option<String>,
116 base_url: Option<String>,
117 http: Option<reqwest::Client>,
118 timeout: Option<Duration>,
119}
120
121impl ClientBuilder {
122 pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
124 self.api_key = Some(api_key.into());
125 self
126 }
127
128 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
130 self.base_url = Some(base_url.into());
131 self
132 }
133
134 pub fn http_client(mut self, http: reqwest::Client) -> Self {
136 self.http = Some(http);
137 self
138 }
139
140 pub fn timeout(mut self, timeout: Duration) -> Self {
142 self.timeout = Some(timeout);
143 self
144 }
145
146 pub fn build(self) -> HypixelClient {
148 let http = self.http.unwrap_or_else(|| {
149 let mut builder = reqwest::Client::builder();
150 if let Some(timeout) = self.timeout {
151 builder = builder.timeout(timeout);
152 }
153 builder.build().unwrap_or_default()
154 });
155 HypixelClient {
156 http,
157 base_url: self
158 .base_url
159 .unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
160 api_key: self.api_key,
161 rate_limit: SharedRateLimit::default(),
162 }
163 }
164}