1use crate::{Result, SockudoError, Token};
2use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
3use std::time::Duration;
4use zeroize::{Zeroize, ZeroizeOnDrop};
5
6#[derive(Clone, Debug)]
8pub struct Config {
9 scheme: String,
10 host: String,
11 port: Option<u16>,
12 app_id: String,
13 token: Token,
14 timeout: Duration,
15 encryption_master_key: Option<EncryptionKey>,
16 pool_max_idle_per_host: usize,
17 enable_retry: bool,
18 max_retries: u32,
19 auto_idempotency_key: bool,
20}
21
22#[derive(Clone, Zeroize, ZeroizeOnDrop)]
24struct EncryptionKey(Vec<u8>);
25
26impl std::fmt::Debug for EncryptionKey {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "EncryptionKey([REDACTED])")
29 }
30}
31
32impl Config {
33 pub fn builder() -> ConfigBuilder {
35 ConfigBuilder::default()
36 }
37
38 pub fn new(
40 app_id: impl Into<String>,
41 key: impl Into<String>,
42 secret: impl Into<String>,
43 ) -> Self {
44 ConfigBuilder::default()
45 .app_id(app_id)
46 .key(key)
47 .secret(secret)
48 .build()
49 .expect("Basic config should always be valid")
50 }
51
52 pub fn validate(&self) -> Result<()> {
54 if self.app_id.is_empty() {
55 return Err(SockudoError::Config {
56 message: "App ID cannot be empty".to_string(),
57 });
58 }
59
60 if self.token.key.is_empty() {
61 return Err(SockudoError::Config {
62 message: "App key cannot be empty".to_string(),
63 });
64 }
65
66 if let Some(ref key) = self.encryption_master_key
67 && key.0.len() != 32
68 {
69 return Err(SockudoError::Config {
70 message: format!("Encryption key must be 32 bytes, got {}", key.0.len()),
71 });
72 }
73
74 Ok(())
75 }
76
77 pub fn scheme(&self) -> &str {
79 &self.scheme
80 }
81
82 pub fn host(&self) -> &str {
83 &self.host
84 }
85
86 pub fn port(&self) -> Option<u16> {
87 self.port
88 }
89
90 pub fn app_id(&self) -> &str {
91 &self.app_id
92 }
93
94 pub fn token(&self) -> &Token {
95 &self.token
96 }
97
98 pub fn timeout(&self) -> Duration {
99 self.timeout
100 }
101
102 pub fn encryption_master_key(&self) -> Option<&[u8]> {
103 self.encryption_master_key.as_ref().map(|k| k.0.as_slice())
104 }
105
106 pub fn pool_max_idle_per_host(&self) -> usize {
107 self.pool_max_idle_per_host
108 }
109
110 pub fn enable_retry(&self) -> bool {
111 self.enable_retry
112 }
113
114 pub fn max_retries(&self) -> u32 {
115 self.max_retries
116 }
117
118 pub fn auto_idempotency_key(&self) -> bool {
119 self.auto_idempotency_key
120 }
121
122 pub fn base_url(&self) -> String {
124 let port = match self.port {
125 Some(port) => format!(":{}", port),
126 None => String::new(),
127 };
128 format!("{}://{}{}", self.scheme, self.host, port)
129 }
130
131 pub fn prefix_path(&self, sub_path: &str) -> String {
133 format!("/apps/{}{}", self.app_id, sub_path)
134 }
135}
136
137#[derive(Default)]
139pub struct ConfigBuilder {
140 scheme: Option<String>,
141 host: Option<String>,
142 port: Option<u16>,
143 app_id: Option<String>,
144 key: Option<String>,
145 secret: Option<String>,
146 timeout: Option<Duration>,
147 encryption_master_key: Option<EncryptionKey>,
148 pool_max_idle_per_host: Option<usize>,
149 enable_retry: Option<bool>,
150 max_retries: Option<u32>,
151 auto_idempotency_key: Option<bool>,
152}
153
154impl ConfigBuilder {
155 pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
157 self.app_id = Some(app_id.into());
158 self
159 }
160
161 pub fn key(mut self, key: impl Into<String>) -> Self {
163 self.key = Some(key.into());
164 self
165 }
166
167 pub fn secret(mut self, secret: impl Into<String>) -> Self {
169 self.secret = Some(secret.into());
170 self
171 }
172
173 pub fn cluster(mut self, cluster: impl AsRef<str>) -> Self {
175 self.host = Some(format!("api-{}.sockudo.io", cluster.as_ref()));
176 self
177 }
178
179 pub fn host(mut self, host: impl Into<String>) -> Self {
181 self.host = Some(host.into());
182 self
183 }
184
185 pub fn use_tls(mut self, use_tls: bool) -> Self {
187 self.scheme = Some(if use_tls { "https" } else { "http" }.to_string());
188 self
189 }
190
191 pub fn port(mut self, port: u16) -> Self {
193 self.port = Some(port);
194 self
195 }
196
197 pub fn timeout(mut self, timeout: Duration) -> Self {
199 self.timeout = Some(timeout);
200 self
201 }
202
203 pub fn encryption_master_key(mut self, key: Vec<u8>) -> Result<Self> {
205 if key.len() != 32 {
206 return Err(SockudoError::Config {
207 message: format!("Encryption key must be 32 bytes, got {}", key.len()),
208 });
209 }
210 self.encryption_master_key = Some(EncryptionKey(key));
211 Ok(self)
212 }
213
214 pub fn encryption_master_key_base64(self, key_base64: impl AsRef<str>) -> Result<Self> {
216 let decoded = BASE64
217 .decode(key_base64.as_ref())
218 .map_err(|e| SockudoError::Config {
219 message: format!("Invalid base64 encryption key: {}", e),
220 })?;
221
222 self.encryption_master_key(decoded)
223 }
224
225 pub fn pool_max_idle_per_host(mut self, max: usize) -> Self {
227 self.pool_max_idle_per_host = Some(max);
228 self
229 }
230
231 pub fn enable_retry(mut self, enable: bool) -> Self {
233 self.enable_retry = Some(enable);
234 self
235 }
236
237 pub fn max_retries(mut self, max: u32) -> Self {
239 self.max_retries = Some(max);
240 self
241 }
242
243 pub fn auto_idempotency_key(mut self, enable: bool) -> Self {
248 self.auto_idempotency_key = Some(enable);
249 self
250 }
251
252 pub fn build(self) -> Result<Config> {
254 let app_id = self.app_id.ok_or_else(|| SockudoError::Config {
255 message: "App ID is required".to_string(),
256 })?;
257
258 let key = self.key.ok_or_else(|| SockudoError::Config {
259 message: "App key is required".to_string(),
260 })?;
261
262 let secret = self.secret.ok_or_else(|| SockudoError::Config {
263 message: "App secret is required".to_string(),
264 })?;
265
266 let config = Config {
267 scheme: self.scheme.unwrap_or_else(|| "https".to_string()),
268 host: self.host.unwrap_or_else(|| "api.sockudo.io".to_string()),
269 port: self.port,
270 app_id,
271 token: Token::new(key, secret),
272 timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
273 encryption_master_key: self.encryption_master_key,
274 pool_max_idle_per_host: self.pool_max_idle_per_host.unwrap_or(10),
275 enable_retry: self.enable_retry.unwrap_or(true),
276 max_retries: self.max_retries.unwrap_or(3),
277 auto_idempotency_key: self.auto_idempotency_key.unwrap_or(true),
278 };
279
280 config.validate()?;
281 Ok(config)
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_config_builder() {
291 let config = Config::builder()
292 .app_id("123")
293 .key("key")
294 .secret("secret")
295 .cluster("eu")
296 .timeout(Duration::from_secs(10))
297 .enable_retry(false)
298 .build()
299 .unwrap();
300
301 assert_eq!(config.app_id(), "123");
302 assert_eq!(config.host(), "api-eu.sockudo.io");
303 assert_eq!(config.timeout(), Duration::from_secs(10));
304 assert!(!config.enable_retry());
305 }
306
307 #[test]
308 fn test_config_validation() {
309 assert!(Config::builder().build().is_err());
310 assert!(Config::builder().app_id("123").build().is_err());
311 assert!(
312 Config::builder()
313 .app_id("123")
314 .key("key")
315 .secret("secret")
316 .build()
317 .is_ok()
318 );
319 }
320
321 #[test]
322 fn test_encryption_key_validation() {
323 let config = Config::builder()
324 .app_id("123")
325 .key("key")
326 .secret("secret")
327 .encryption_master_key(vec![0u8; 32])
328 .unwrap()
329 .build()
330 .unwrap();
331
332 assert!(config.encryption_master_key().is_some());
333
334 assert!(
336 Config::builder()
337 .app_id("123")
338 .key("key")
339 .secret("secret")
340 .encryption_master_key(vec![0u8; 16])
341 .is_err()
342 );
343 }
344}