Skip to main content

hyperi_rustlib/http_client/
config.rs

1// Project:   hyperi-rustlib
2// File:      src/http_client/config.rs
3// Purpose:   HTTP client configuration
4// Language:  Rust
5//
6// License:   BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! HTTP client configuration with config cascade support.
10
11use serde::{Deserialize, Serialize};
12
13/// Configuration for the HTTP client.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct HttpClientConfig {
16    /// Request timeout in seconds. Default: 30.
17    #[serde(default = "default_timeout_secs")]
18    pub timeout_secs: u64,
19
20    /// Connection timeout in seconds. Default: 10.
21    #[serde(default = "default_connect_timeout_secs")]
22    pub connect_timeout_secs: u64,
23
24    /// Maximum number of retries for transient errors. Default: 3.
25    #[serde(default = "default_max_retries")]
26    pub max_retries: u32,
27
28    /// Minimum retry interval in milliseconds. Default: 100.
29    #[serde(default = "default_min_retry_interval_ms")]
30    pub min_retry_interval_ms: u64,
31
32    /// Maximum retry interval in milliseconds. Default: 30000 (30s).
33    #[serde(default = "default_max_retry_interval_ms")]
34    pub max_retry_interval_ms: u64,
35
36    /// Custom User-Agent header. Default: None (uses reqwest default).
37    #[serde(default)]
38    pub user_agent: Option<String>,
39}
40
41fn default_timeout_secs() -> u64 {
42    30
43}
44fn default_connect_timeout_secs() -> u64 {
45    10
46}
47fn default_max_retries() -> u32 {
48    3
49}
50fn default_min_retry_interval_ms() -> u64 {
51    100
52}
53fn default_max_retry_interval_ms() -> u64 {
54    30_000
55}
56
57impl Default for HttpClientConfig {
58    fn default() -> Self {
59        Self {
60            timeout_secs: default_timeout_secs(),
61            connect_timeout_secs: default_connect_timeout_secs(),
62            max_retries: default_max_retries(),
63            min_retry_interval_ms: default_min_retry_interval_ms(),
64            max_retry_interval_ms: default_max_retry_interval_ms(),
65            user_agent: None,
66        }
67    }
68}
69
70impl HttpClientConfig {
71    /// Load from the config cascade under the `http_client` key.
72    #[must_use]
73    pub fn from_cascade() -> Self {
74        #[cfg(feature = "config")]
75        {
76            if let Some(cfg) = crate::config::try_get()
77                && let Ok(hc) = cfg.unmarshal_key_registered::<Self>("http_client")
78            {
79                return hc;
80            }
81        }
82        Self::default()
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn defaults_are_sensible() {
92        let config = HttpClientConfig::default();
93        assert_eq!(config.timeout_secs, 30);
94        assert_eq!(config.connect_timeout_secs, 10);
95        assert_eq!(config.max_retries, 3);
96        assert_eq!(config.min_retry_interval_ms, 100);
97        assert_eq!(config.max_retry_interval_ms, 30_000);
98        assert!(config.user_agent.is_none());
99    }
100
101    #[test]
102    fn deserialise_from_yaml() {
103        let yaml = r#"
104timeout_secs: 60
105max_retries: 5
106user_agent: "my-app/1.0"
107"#;
108        let config: HttpClientConfig = serde_yaml_ng::from_str(yaml).unwrap();
109        assert_eq!(config.timeout_secs, 60);
110        assert_eq!(config.max_retries, 5);
111        assert_eq!(config.user_agent.as_deref(), Some("my-app/1.0"));
112        // Defaults for unset fields
113        assert_eq!(config.connect_timeout_secs, 10);
114    }
115}