pubmed_client/config.rs
1use crate::cache::CacheConfig;
2use crate::rate_limit::RateLimiter;
3use crate::retry::RetryConfig;
4use crate::time::Duration;
5
6/// Configuration options for PubMed and PMC clients
7///
8/// This configuration allows customization of rate limiting, API keys,
9/// timeouts, and other client behavior to comply with NCBI guidelines
10/// and optimize performance.
11#[derive(Clone)]
12pub struct ClientConfig {
13 /// NCBI E-utilities API key for increased rate limits
14 ///
15 /// With an API key:
16 /// - Rate limit increases from 3 to 10 requests per second
17 /// - Better stability and reduced chance of blocking
18 /// - Required for high-volume applications
19 ///
20 /// Get your API key at: <https://ncbiinsights.ncbi.nlm.nih.gov/2017/11/02/new-api-keys-for-the-e-utilities/>
21 pub api_key: Option<String>,
22
23 /// Rate limit in requests per second
24 ///
25 /// Default values:
26 /// - 3.0 without API key (NCBI guideline)
27 /// - 10.0 with API key (NCBI guideline)
28 ///
29 /// Setting this value overrides the automatic selection based on API key presence.
30 pub rate_limit: Option<f64>,
31
32 /// HTTP request timeout
33 ///
34 /// Default: 30 seconds
35 pub timeout: Duration,
36
37 /// Custom User-Agent string for HTTP requests
38 ///
39 /// Default: "pubmed-client-rs/{version}"
40 pub user_agent: Option<String>,
41
42 /// Base URL for NCBI E-utilities
43 ///
44 /// Default: <https://eutils.ncbi.nlm.nih.gov/entrez/eutils>
45 /// This should rarely need to be changed unless using a proxy or test environment.
46 pub base_url: Option<String>,
47
48 /// Email address for identification (recommended by NCBI)
49 ///
50 /// NCBI recommends including an email address in requests for contact
51 /// in case of problems. This is automatically added to requests.
52 pub email: Option<String>,
53
54 /// Tool name for identification (recommended by NCBI)
55 ///
56 /// NCBI recommends including a tool name in requests.
57 /// Default: "pubmed-client-rs"
58 pub tool: Option<String>,
59
60 /// Retry configuration for handling transient failures
61 ///
62 /// Default: 3 retries with exponential backoff starting at 1 second
63 pub retry_config: RetryConfig,
64
65 /// Cache configuration for response caching
66 ///
67 /// Default: Memory-only cache with 1000 items max
68 pub cache_config: Option<CacheConfig>,
69}
70
71impl ClientConfig {
72 /// Create a new configuration with default settings
73 ///
74 /// # Example
75 ///
76 /// ```
77 /// use pubmed_client_rs::config::ClientConfig;
78 ///
79 /// let config = ClientConfig::new();
80 /// ```
81 pub fn new() -> Self {
82 Self {
83 api_key: None,
84 rate_limit: None,
85 timeout: Duration::from_secs(30),
86 user_agent: None,
87 base_url: None,
88 email: None,
89 tool: None,
90 retry_config: RetryConfig::default(),
91 cache_config: None,
92 }
93 }
94
95 /// Set the NCBI API key
96 ///
97 /// # Arguments
98 ///
99 /// * `api_key` - Your NCBI E-utilities API key
100 ///
101 /// # Example
102 ///
103 /// ```
104 /// use pubmed_client_rs::config::ClientConfig;
105 ///
106 /// let config = ClientConfig::new()
107 /// .with_api_key("your_api_key_here");
108 /// ```
109 pub fn with_api_key<S: Into<String>>(mut self, api_key: S) -> Self {
110 self.api_key = Some(api_key.into());
111 self
112 }
113
114 /// Set a custom rate limit
115 ///
116 /// # Arguments
117 ///
118 /// * `rate` - Requests per second (must be positive)
119 ///
120 /// # Example
121 ///
122 /// ```
123 /// use pubmed_client_rs::config::ClientConfig;
124 ///
125 /// // Custom rate limit of 5 requests per second
126 /// let config = ClientConfig::new()
127 /// .with_rate_limit(5.0);
128 /// ```
129 pub fn with_rate_limit(mut self, rate: f64) -> Self {
130 if rate > 0.0 {
131 self.rate_limit = Some(rate);
132 }
133 self
134 }
135
136 /// Set the HTTP request timeout
137 ///
138 /// # Arguments
139 ///
140 /// * `timeout` - Maximum time to wait for HTTP responses
141 ///
142 /// # Example
143 ///
144 /// ```
145 /// use pubmed_client_rs::config::ClientConfig;
146 /// use pubmed_client_rs::time::Duration;
147 ///
148 /// let config = ClientConfig::new()
149 /// .with_timeout(Duration::from_secs(60));
150 /// ```
151 pub fn with_timeout(mut self, timeout: Duration) -> Self {
152 self.timeout = timeout;
153 self
154 }
155
156 /// Set the HTTP request timeout in seconds (convenience method)
157 ///
158 /// # Arguments
159 ///
160 /// * `timeout_seconds` - Maximum time to wait for HTTP responses in seconds
161 ///
162 /// # Example
163 ///
164 /// ```
165 /// use pubmed_client_rs::config::ClientConfig;
166 ///
167 /// let config = ClientConfig::new()
168 /// .with_timeout_seconds(60);
169 /// ```
170 pub fn with_timeout_seconds(mut self, timeout_seconds: u64) -> Self {
171 self.timeout = Duration::from_secs(timeout_seconds);
172 self
173 }
174
175 /// Set a custom User-Agent string
176 ///
177 /// # Arguments
178 ///
179 /// * `user_agent` - Custom User-Agent for HTTP requests
180 ///
181 /// # Example
182 ///
183 /// ```
184 /// use pubmed_client_rs::config::ClientConfig;
185 ///
186 /// let config = ClientConfig::new()
187 /// .with_user_agent("MyApp/1.0");
188 /// ```
189 pub fn with_user_agent<S: Into<String>>(mut self, user_agent: S) -> Self {
190 self.user_agent = Some(user_agent.into());
191 self
192 }
193
194 /// Set a custom base URL for NCBI E-utilities
195 ///
196 /// # Arguments
197 ///
198 /// * `base_url` - Base URL for E-utilities API
199 ///
200 /// # Example
201 ///
202 /// ```
203 /// use pubmed_client_rs::config::ClientConfig;
204 ///
205 /// let config = ClientConfig::new()
206 /// .with_base_url("https://proxy.example.com/eutils");
207 /// ```
208 pub fn with_base_url<S: Into<String>>(mut self, base_url: S) -> Self {
209 self.base_url = Some(base_url.into());
210 self
211 }
212
213 /// Set email address for NCBI identification
214 ///
215 /// # Arguments
216 ///
217 /// * `email` - Your email address for NCBI contact
218 ///
219 /// # Example
220 ///
221 /// ```
222 /// use pubmed_client_rs::config::ClientConfig;
223 ///
224 /// let config = ClientConfig::new()
225 /// .with_email("researcher@university.edu");
226 /// ```
227 pub fn with_email<S: Into<String>>(mut self, email: S) -> Self {
228 self.email = Some(email.into());
229 self
230 }
231
232 /// Set tool name for NCBI identification
233 ///
234 /// # Arguments
235 ///
236 /// * `tool` - Your application/tool name
237 ///
238 /// # Example
239 ///
240 /// ```
241 /// use pubmed_client_rs::config::ClientConfig;
242 ///
243 /// let config = ClientConfig::new()
244 /// .with_tool("BioinformaticsApp");
245 /// ```
246 pub fn with_tool<S: Into<String>>(mut self, tool: S) -> Self {
247 self.tool = Some(tool.into());
248 self
249 }
250
251 /// Set retry configuration for handling transient failures
252 ///
253 /// # Arguments
254 ///
255 /// * `retry_config` - Custom retry configuration
256 ///
257 /// # Example
258 ///
259 /// ```
260 /// use pubmed_client_rs::config::ClientConfig;
261 /// use pubmed_client_rs::retry::RetryConfig;
262 /// use pubmed_client_rs::time::Duration;
263 ///
264 /// let retry_config = RetryConfig::new()
265 /// .with_max_retries(5)
266 /// .with_initial_delay(Duration::from_secs(2));
267 ///
268 /// let config = ClientConfig::new()
269 /// .with_retry_config(retry_config);
270 /// ```
271 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
272 self.retry_config = retry_config;
273 self
274 }
275
276 /// Enable caching with default configuration
277 ///
278 /// # Example
279 ///
280 /// ```
281 /// use pubmed_client_rs::config::ClientConfig;
282 ///
283 /// let config = ClientConfig::new()
284 /// .with_cache();
285 /// ```
286 pub fn with_cache(mut self) -> Self {
287 self.cache_config = Some(CacheConfig::default());
288 self
289 }
290
291 /// Set cache configuration
292 ///
293 /// # Arguments
294 ///
295 /// * `cache_config` - Custom cache configuration
296 ///
297 /// # Example
298 ///
299 /// ```
300 /// use pubmed_client_rs::config::ClientConfig;
301 /// use pubmed_client_rs::cache::CacheConfig;
302 ///
303 /// let cache_config = CacheConfig {
304 /// max_capacity: 5000,
305 /// ..Default::default()
306 /// };
307 ///
308 /// let config = ClientConfig::new()
309 /// .with_cache_config(cache_config);
310 /// ```
311 pub fn with_cache_config(mut self, cache_config: CacheConfig) -> Self {
312 self.cache_config = Some(cache_config);
313 self
314 }
315
316 /// Disable all caching
317 ///
318 /// # Example
319 ///
320 /// ```
321 /// use pubmed_client_rs::config::ClientConfig;
322 ///
323 /// let config = ClientConfig::new()
324 /// .without_cache();
325 /// ```
326 pub fn without_cache(mut self) -> Self {
327 self.cache_config = None;
328 self
329 }
330
331 /// Get the effective rate limit based on configuration
332 ///
333 /// Returns the configured rate limit, or the appropriate default
334 /// based on whether an API key is present.
335 ///
336 /// # Returns
337 ///
338 /// - Custom rate limit if set
339 /// - 10.0 requests/second if API key is present
340 /// - 3.0 requests/second if no API key
341 pub fn effective_rate_limit(&self) -> f64 {
342 self.rate_limit.unwrap_or_else(|| {
343 if self.api_key.is_some() {
344 10.0 // NCBI rate limit with API key
345 } else {
346 3.0 // NCBI rate limit without API key
347 }
348 })
349 }
350
351 /// Create a rate limiter based on this configuration
352 ///
353 /// # Returns
354 ///
355 /// A `RateLimiter` configured with the appropriate rate limit
356 ///
357 /// # Example
358 ///
359 /// ```
360 /// use pubmed_client_rs::config::ClientConfig;
361 ///
362 /// let config = ClientConfig::new().with_api_key("your_key");
363 /// let rate_limiter = config.create_rate_limiter();
364 /// ```
365 pub fn create_rate_limiter(&self) -> RateLimiter {
366 RateLimiter::new(self.effective_rate_limit())
367 }
368
369 /// Get the base URL for E-utilities
370 ///
371 /// Returns the configured base URL or the default NCBI E-utilities URL.
372 pub fn effective_base_url(&self) -> &str {
373 self.base_url
374 .as_deref()
375 .unwrap_or("https://eutils.ncbi.nlm.nih.gov/entrez/eutils")
376 }
377
378 /// Get the User-Agent string
379 ///
380 /// Returns the configured User-Agent or a default based on the crate name and version.
381 pub fn effective_user_agent(&self) -> String {
382 self.user_agent.clone().unwrap_or_else(|| {
383 let version = env!("CARGO_PKG_VERSION");
384 format!("pubmed-client-rs/{version}")
385 })
386 }
387
388 /// Get the tool name for NCBI identification
389 ///
390 /// Returns the configured tool name or the default.
391 pub fn effective_tool(&self) -> &str {
392 self.tool.as_deref().unwrap_or("pubmed-client-rs")
393 }
394
395 /// Build query parameters for NCBI API requests
396 ///
397 /// This includes API key, email, and tool parameters when configured.
398 pub fn build_api_params(&self) -> Vec<(String, String)> {
399 let mut params = Vec::new();
400
401 if let Some(ref api_key) = self.api_key {
402 params.push(("api_key".to_string(), api_key.clone()));
403 }
404
405 if let Some(ref email) = self.email {
406 params.push(("email".to_string(), email.clone()));
407 }
408
409 params.push(("tool".to_string(), self.effective_tool().to_string()));
410
411 params
412 }
413}
414
415impl Default for ClientConfig {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use std::mem;
424
425 use super::*;
426
427 #[test]
428 fn test_default_config() {
429 let config = ClientConfig::new();
430 assert!(config.api_key.is_none());
431 assert!(config.rate_limit.is_none());
432 assert_eq!(config.timeout, Duration::from_secs(30));
433 assert_eq!(config.effective_rate_limit(), 3.0);
434 }
435
436 #[test]
437 fn test_config_with_api_key() {
438 let config = ClientConfig::new().with_api_key("test_key");
439 assert_eq!(config.api_key.as_ref().unwrap(), "test_key");
440 assert_eq!(config.effective_rate_limit(), 10.0);
441 }
442
443 #[test]
444 fn test_custom_rate_limit() {
445 let config = ClientConfig::new().with_rate_limit(5.0);
446 assert_eq!(config.effective_rate_limit(), 5.0);
447
448 // Custom rate limit overrides API key default
449 let config_with_key = ClientConfig::new()
450 .with_api_key("test")
451 .with_rate_limit(7.0);
452 assert_eq!(config_with_key.effective_rate_limit(), 7.0);
453 }
454
455 #[test]
456 fn test_invalid_rate_limit() {
457 let config = ClientConfig::new().with_rate_limit(-1.0);
458 assert!(config.rate_limit.is_none());
459 assert_eq!(config.effective_rate_limit(), 3.0);
460 }
461
462 #[test]
463 fn test_fluent_interface() {
464 let config = ClientConfig::new()
465 .with_api_key("test_key")
466 .with_rate_limit(5.0)
467 .with_timeout(Duration::from_secs(60))
468 .with_email("test@example.com")
469 .with_tool("TestApp");
470
471 assert_eq!(config.api_key.as_ref().unwrap(), "test_key");
472 assert_eq!(config.effective_rate_limit(), 5.0);
473 assert_eq!(config.timeout, Duration::from_secs(60));
474 assert_eq!(config.email.as_ref().unwrap(), "test@example.com");
475 assert_eq!(config.effective_tool(), "TestApp");
476 }
477
478 #[test]
479 fn test_api_params() {
480 let config = ClientConfig::new()
481 .with_api_key("test_key")
482 .with_email("test@example.com")
483 .with_tool("TestApp");
484
485 let params = config.build_api_params();
486 assert_eq!(params.len(), 3);
487
488 assert!(params.contains(&("api_key".to_string(), "test_key".to_string())));
489 assert!(params.contains(&("email".to_string(), "test@example.com".to_string())));
490 assert!(params.contains(&("tool".to_string(), "TestApp".to_string())));
491 }
492
493 #[test]
494 fn test_effective_values() {
495 let config = ClientConfig::new();
496
497 assert_eq!(
498 config.effective_base_url(),
499 "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
500 );
501 assert!(config
502 .effective_user_agent()
503 .starts_with("pubmed-client-rs/"));
504 assert_eq!(config.effective_tool(), "pubmed-client-rs");
505 }
506
507 #[test]
508 fn test_rate_limiter_creation() {
509 let config = ClientConfig::new().with_rate_limit(5.0);
510 let rate_limiter = config.create_rate_limiter();
511 // The rate limiter creation should succeed
512 assert!(mem::size_of_val(&rate_limiter) > 0);
513 }
514}