1use std::time::Duration;
4use std::sync::{Arc, Mutex};
5
6use crate::error::ModelsDevError;
7
8#[derive(Debug, Clone)]
10struct CacheMetadata {
11 etag: Option<String>,
12 last_modified: Option<String>,
13}
14
15#[derive(Debug, Clone)]
20pub struct ModelsDevClient {
21 http_client: reqwest::Client,
23 api_base_url: String,
25 timeout: Duration,
27 cache_metadata: Arc<Mutex<Option<CacheMetadata>>>,
29}
30
31impl ModelsDevClient {
32 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn with_base_url(api_base_url: impl Into<String>) -> Self {
50 Self {
51 http_client: reqwest::Client::builder()
52 .timeout(Duration::from_secs(30))
53 .build()
54 .expect("Failed to build HTTP client"),
55 api_base_url: api_base_url.into(),
56 timeout: Duration::from_secs(30),
57 cache_metadata: Arc::new(Mutex::new(None)),
58 }
59 }
60
61 pub async fn fetch_providers(&self) -> Result<crate::types::ModelsDevResponse, ModelsDevError> {
82 let url = format!("{}/api.json", self.api_base_url);
83
84 let head_response = self.http_client.head(&url).timeout(Duration::from_secs(5)).send().await;
86
87 if let Ok(head_resp) = head_response {
88 if let Some(etag) = head_resp.headers().get("etag") {
89 let cached_etag = self.get_cached_etag().await?;
90
91 if cached_etag == Some(etag.to_str().unwrap_or("").to_string()) {
92 return self.load_cached_response().await;
94 }
95 }
96 }
97
98 let response = self.http_client.get(&url).timeout(self.timeout).send().await?;
100 let api_response = self.process_response(response).await?;
101
102 self.update_cache_metadata(&api_response).await?;
104
105 Ok(api_response)
106 }
107
108 async fn process_response(
110 &self,
111 response: reqwest::Response,
112 ) -> Result<crate::types::ModelsDevResponse, ModelsDevError> {
113 if !response.status().is_success() {
114 let error_msg = response
115 .text()
116 .await
117 .unwrap_or_else(|_| "Unknown error".to_string());
118 return Err(ModelsDevError::ApiError(error_msg));
119 }
120
121 let response_text = response.text().await?;
122 let api_response: crate::types::ModelsDevResponse =
123 serde_json::from_str(&response_text).map_err(ModelsDevError::JsonError)?;
124
125 Ok(api_response)
126 }
127
128 async fn get_cached_etag(&self) -> Result<Option<String>, ModelsDevError> {
130 let cache_meta = self.cache_metadata.lock().unwrap();
131 Ok(cache_meta.as_ref().and_then(|m| m.etag.clone()))
132 }
133
134 async fn update_cache_metadata(
136 &self,
137 _response: &crate::types::ModelsDevResponse,
138 ) -> Result<(), ModelsDevError> {
139 let metadata = CacheMetadata {
143 etag: Some(format!("\"{}\"", std::time::SystemTime::now()
144 .duration_since(std::time::UNIX_EPOCH)
145 .unwrap()
146 .as_secs())), last_modified: None,
148 };
149
150 let mut cache_meta = self.cache_metadata.lock().unwrap();
151 *cache_meta = Some(metadata);
152
153 Ok(())
154 }
155
156 async fn load_cached_response(&self) -> Result<crate::types::ModelsDevResponse, ModelsDevError> {
158 Err(ModelsDevError::CacheError(
161 "Cached response not available".to_string(),
162 ))
163 }
164
165 pub fn api_base_url(&self) -> &str {
170 &self.api_base_url
171 }
172
173 pub fn timeout(&self) -> Duration {
178 self.timeout
179 }
180
181 pub fn clear_cache(&self) -> Result<(), ModelsDevError> {
192 let mut cache_meta = self.cache_metadata.lock().unwrap();
193 *cache_meta = None;
194 Ok(())
195 }
196
197 pub fn cache_info(&self) -> CacheInfo {
201 let cache_meta = self.cache_metadata.lock().unwrap();
202 CacheInfo {
203 has_metadata: cache_meta.is_some(),
204 etag: cache_meta.as_ref().and_then(|m| m.etag.clone()),
205 last_modified: cache_meta.as_ref().and_then(|m| m.last_modified.clone()),
206 }
207 }
208}
209
210#[derive(Debug, Clone)]
212pub struct CacheInfo {
213 pub has_metadata: bool,
214 pub etag: Option<String>,
215 pub last_modified: Option<String>,
216}
217
218impl Default for ModelsDevClient {
219 fn default() -> Self {
220 Self {
221 http_client: reqwest::Client::builder()
222 .timeout(Duration::from_secs(30))
223 .build()
224 .expect("Failed to build HTTP client"),
225 api_base_url: "https://models.dev".to_string(),
226 timeout: Duration::from_secs(30),
227 cache_metadata: Arc::new(Mutex::new(None)),
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_client_creation() {
238 let client = ModelsDevClient::new();
239 assert_eq!(client.api_base_url(), "https://models.dev");
240 assert_eq!(client.timeout(), Duration::from_secs(30));
241 }
242
243 #[test]
244 fn test_client_with_custom_base_url() {
245 let client = ModelsDevClient::with_base_url("https://custom.api.com");
246 assert_eq!(client.api_base_url(), "https://custom.api.com");
247 }
248
249 #[test]
250 fn test_client_default() {
251 let client = ModelsDevClient::default();
252 assert_eq!(client.api_base_url(), "https://models.dev");
253 assert_eq!(client.timeout(), Duration::from_secs(30));
254 }
255
256 #[test]
257 fn test_client_is_send_and_sync() {
258 fn assert_send_sync<T: Send + Sync>() {}
259 assert_send_sync::<ModelsDevClient>();
260 }
261
262 #[test]
263 fn test_client_debug_format() {
264 let client = ModelsDevClient::new();
265 let debug_str = format!("{:?}", client);
266 assert!(debug_str.contains("ModelsDevClient"));
267 assert!(debug_str.contains("models.dev"));
268 }
269
270 #[test]
271 fn test_cache_info() {
272 let client = ModelsDevClient::new();
273 let info = client.cache_info();
274 assert!(!info.has_metadata);
275 assert!(info.etag.is_none());
276 assert!(info.last_modified.is_none());
277 }
278
279 #[test]
280 fn test_clear_cache() {
281 let client = ModelsDevClient::new();
282 assert!(client.clear_cache().is_ok());
283 }
284
285 #[test]
286 fn test_cache_metadata() {
287 let metadata = CacheMetadata {
288 etag: Some("\"abc123\"".to_string()),
289 last_modified: Some("Wed, 21 Oct 2015 07:28:00 GMT".to_string()),
290 };
291
292 assert_eq!(metadata.etag, Some("\"abc123\"".to_string()));
293 assert_eq!(metadata.last_modified, Some("Wed, 21 Oct 2015 07:28:00 GMT".to_string()));
294 }
295}