Skip to main content

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}