secret_store_sdk/
config.rs

1use crate::{auth::Auth, cache::CacheConfig, errors::Result, telemetry::TelemetryConfig, Error};
2use std::time::Duration;
3
4/// Client configuration
5#[derive(Debug, Clone)]
6pub struct ClientConfig {
7    /// Base URL of the secret store service
8    pub base_url: String,
9    /// Authentication configuration
10    pub auth: Auth,
11    /// Request timeout
12    pub timeout: Duration,
13    /// Number of retries
14    pub retries: u32,
15    /// User agent suffix
16    pub user_agent_suffix: Option<String>,
17    /// Cache configuration
18    pub cache_config: CacheConfig,
19    /// Telemetry configuration
20    pub telemetry_config: TelemetryConfig,
21    /// Allow insecure HTTP (only with danger-insecure-http feature)
22    pub allow_insecure_http: bool,
23}
24
25/// Builder for creating a configured Client
26#[derive(Debug)]
27pub struct ClientBuilder {
28    base_url: String,
29    auth: Option<Auth>,
30    timeout_ms: u64,
31    retries: u32,
32    user_agent_suffix: Option<String>,
33    cache_enabled: bool,
34    cache_max_entries: u64,
35    cache_ttl_secs: u64,
36    telemetry_config: TelemetryConfig,
37    allow_insecure_http: bool,
38}
39
40impl ClientBuilder {
41    /// Create a new client builder with the given base URL
42    ///
43    /// # Arguments
44    ///
45    /// * `base_url` - Base URL of the secret store service (e.g., `"https://secret.example.com"`)
46    pub fn new(base_url: impl Into<String>) -> Self {
47        Self {
48            base_url: base_url.into(),
49            auth: None,
50            timeout_ms: crate::DEFAULT_TIMEOUT_MS,
51            retries: crate::DEFAULT_RETRIES,
52            user_agent_suffix: None,
53            cache_enabled: true,
54            cache_max_entries: crate::DEFAULT_CACHE_MAX_ENTRIES,
55            cache_ttl_secs: crate::DEFAULT_CACHE_TTL_SECS,
56            telemetry_config: TelemetryConfig::default(),
57            allow_insecure_http: false,
58        }
59    }
60
61    /// Set the authentication method
62    pub fn auth(mut self, auth: Auth) -> Self {
63        self.auth = Some(auth);
64        self
65    }
66
67    /// Set the request timeout in milliseconds
68    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
69        self.timeout_ms = timeout_ms;
70        self
71    }
72
73    /// Set the number of retries for failed requests
74    pub fn retries(mut self, retries: u32) -> Self {
75        self.retries = retries;
76        self
77    }
78
79    /// Add a custom user agent suffix
80    pub fn user_agent_extra(mut self, suffix: impl Into<String>) -> Self {
81        self.user_agent_suffix = Some(suffix.into());
82        self
83    }
84
85    /// Enable or disable caching (enabled by default)
86    pub fn enable_cache(mut self, enabled: bool) -> Self {
87        self.cache_enabled = enabled;
88        self
89    }
90
91    /// Set the maximum number of cache entries
92    pub fn cache_max_entries(mut self, max_entries: u64) -> Self {
93        self.cache_max_entries = max_entries;
94        self
95    }
96
97    /// Set the default cache TTL in seconds
98    pub fn cache_ttl_secs(mut self, ttl_secs: u64) -> Self {
99        self.cache_ttl_secs = ttl_secs;
100        self
101    }
102
103    /// Configure telemetry/metrics
104    #[cfg(feature = "metrics")]
105    pub fn with_telemetry(mut self, config: TelemetryConfig) -> Self {
106        self.telemetry_config = config;
107        self
108    }
109
110    /// Enable telemetry with default settings
111    #[cfg(feature = "metrics")]
112    pub fn enable_telemetry(mut self) -> Self {
113        self.telemetry_config.enabled = true;
114        self
115    }
116
117    /// Allow insecure HTTP connections (requires danger-insecure-http feature)
118    #[cfg(feature = "danger-insecure-http")]
119    pub fn allow_insecure_http(mut self) -> Self {
120        self.allow_insecure_http = true;
121        self
122    }
123
124    /// Build the client with the configured options
125    pub fn build(self) -> Result<crate::Client> {
126        // Validate base URL
127        let url = self.base_url.trim_end_matches('/');
128
129        // Check for insecure HTTP
130        if url.starts_with("http://") && !self.allow_insecure_http {
131            #[cfg(feature = "danger-insecure-http")]
132            return Err(Error::Config(
133                "HTTP URLs are not allowed by default. Use .allow_insecure_http() to enable (dangerous!)".to_string()
134            ));
135
136            #[cfg(not(feature = "danger-insecure-http"))]
137            return Err(Error::Config(
138                "HTTP URLs are not allowed. Enable the 'danger-insecure-http' feature and use .allow_insecure_http() (dangerous!)".to_string()
139            ));
140        }
141
142        // Require authentication
143        let auth = self.auth.ok_or_else(|| {
144            Error::Config(
145                "Authentication is required. Use .auth() to set authentication method".to_string(),
146            )
147        })?;
148
149        // Validate URL format
150        if !url.starts_with("http://") && !url.starts_with("https://") {
151            return Err(Error::Config(
152                "Base URL must start with http:// or https://".to_string(),
153            ));
154        }
155
156        let config = ClientConfig {
157            base_url: url.to_string(),
158            auth,
159            timeout: Duration::from_millis(self.timeout_ms),
160            retries: self.retries,
161            user_agent_suffix: self.user_agent_suffix,
162            cache_config: CacheConfig {
163                enabled: self.cache_enabled,
164                max_entries: self.cache_max_entries,
165                default_ttl_secs: self.cache_ttl_secs,
166            },
167            telemetry_config: self.telemetry_config,
168            allow_insecure_http: self.allow_insecure_http,
169        };
170
171        crate::client::Client::new(config)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_builder_requires_auth() {
181        let result = ClientBuilder::new("https://example.com").build();
182        assert!(result.is_err());
183        assert!(matches!(result.unwrap_err(), Error::Config(_)));
184    }
185
186    #[test]
187    fn test_builder_validates_url() {
188        let result = ClientBuilder::new("not-a-url")
189            .auth(Auth::bearer("token"))
190            .build();
191        assert!(result.is_err());
192    }
193
194    #[test]
195    #[cfg(not(feature = "danger-insecure-http"))]
196    fn test_builder_rejects_http() {
197        let result = ClientBuilder::new("http://example.com")
198            .auth(Auth::bearer("token"))
199            .build();
200        assert!(result.is_err());
201        assert!(matches!(result.unwrap_err(), Error::Config(_)));
202    }
203}