1use crate::error::{Result, WeChatError};
41use serde::{Deserialize, Serialize};
42use std::time::Duration;
43
44#[derive(Debug, Clone, Serialize, Deserialize, Default)]
46pub struct Config {
47 pub security: SecurityConfig,
49 pub performance: PerformanceConfig,
51 pub http: HttpConfig,
53 pub cache: CacheConfig,
55 pub retry: RetryConfig,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SecurityConfig {
62 pub max_upload_size: u64,
64 pub max_download_size: u64,
66 pub validate_file_paths: bool,
68 pub sanitize_filenames: bool,
70 pub blocked_extensions: Vec<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct PerformanceConfig {
77 pub max_concurrent_uploads: usize,
79 pub cache_ttl_minutes: u64,
81 pub max_cache_entries: usize,
83 pub enable_parallel_processing: bool,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct HttpConfig {
90 pub request_timeout_secs: u64,
92 pub connect_timeout_secs: u64,
94 pub base_url: String,
96 pub user_agent: String,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct CacheConfig {
103 pub enable_material_cache: bool,
105 pub enable_token_cache: bool,
107 pub cleanup_interval_minutes: u64,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct RetryConfig {
114 pub max_attempts: u32,
116 pub base_delay_ms: u64,
118 pub max_delay_secs: u64,
120 pub backoff_factor: f64,
122 pub enable_jitter: bool,
124}
125
126impl Default for SecurityConfig {
127 fn default() -> Self {
128 Self {
129 max_upload_size: 10 * 1024 * 1024, max_download_size: 20 * 1024 * 1024, validate_file_paths: true,
132 sanitize_filenames: true,
133 blocked_extensions: vec![
134 "exe".to_string(),
135 "bat".to_string(),
136 "cmd".to_string(),
137 "com".to_string(),
138 "pif".to_string(),
139 "scr".to_string(),
140 "vbs".to_string(),
141 "js".to_string(),
142 "jar".to_string(),
143 "sh".to_string(),
144 "php".to_string(),
145 "asp".to_string(),
146 "aspx".to_string(),
147 "jsp".to_string(),
148 ],
149 }
150 }
151}
152
153impl Default for PerformanceConfig {
154 fn default() -> Self {
155 Self {
156 max_concurrent_uploads: 5,
157 cache_ttl_minutes: 15,
158 max_cache_entries: 1000,
159 enable_parallel_processing: true,
160 }
161 }
162}
163
164impl Default for HttpConfig {
165 fn default() -> Self {
166 Self {
167 request_timeout_secs: 30,
168 connect_timeout_secs: 10,
169 base_url: "https://api.weixin.qq.com".to_string(),
170 user_agent: format!("wechat-pub-rs/{}", env!("CARGO_PKG_VERSION")),
171 }
172 }
173}
174
175impl Default for CacheConfig {
176 fn default() -> Self {
177 Self {
178 enable_material_cache: true,
179 enable_token_cache: true,
180 cleanup_interval_minutes: 60,
181 }
182 }
183}
184
185impl Default for RetryConfig {
186 fn default() -> Self {
187 Self {
188 max_attempts: 3,
189 base_delay_ms: 500,
190 max_delay_secs: 30,
191 backoff_factor: 2.0,
192 enable_jitter: true,
193 }
194 }
195}
196
197impl Config {
198 pub fn builder() -> ConfigBuilder {
200 ConfigBuilder::default()
201 }
202
203 pub fn from_env() -> Result<Self> {
205 let mut config = Self::default();
206
207 if let Ok(val) = std::env::var("WECHAT_MAX_UPLOAD_SIZE") {
209 config.security.max_upload_size = val
210 .parse()
211 .map_err(|_| WeChatError::config_error("Invalid WECHAT_MAX_UPLOAD_SIZE value"))?;
212 }
213
214 if let Ok(val) = std::env::var("WECHAT_MAX_DOWNLOAD_SIZE") {
215 config.security.max_download_size = val
216 .parse()
217 .map_err(|_| WeChatError::config_error("Invalid WECHAT_MAX_DOWNLOAD_SIZE value"))?;
218 }
219
220 if let Ok(val) = std::env::var("WECHAT_MAX_CONCURRENT_UPLOADS") {
222 config.performance.max_concurrent_uploads = val.parse().map_err(|_| {
223 WeChatError::config_error("Invalid WECHAT_MAX_CONCURRENT_UPLOADS value")
224 })?;
225 }
226
227 if let Ok(val) = std::env::var("WECHAT_CACHE_TTL_MINUTES") {
228 config.performance.cache_ttl_minutes = val
229 .parse()
230 .map_err(|_| WeChatError::config_error("Invalid WECHAT_CACHE_TTL_MINUTES value"))?;
231 }
232
233 if let Ok(val) = std::env::var("WECHAT_REQUEST_TIMEOUT") {
235 config.http.request_timeout_secs = val
236 .parse()
237 .map_err(|_| WeChatError::config_error("Invalid WECHAT_REQUEST_TIMEOUT value"))?;
238 }
239
240 if let Ok(val) = std::env::var("WECHAT_BASE_URL") {
241 config.http.base_url = val;
242 }
243
244 if let Ok(val) = std::env::var("WECHAT_MAX_RETRIES") {
246 config.retry.max_attempts = val
247 .parse()
248 .map_err(|_| WeChatError::config_error("Invalid WECHAT_MAX_RETRIES value"))?;
249 }
250
251 config.validate()?;
252 Ok(config)
253 }
254
255 pub fn validate(&self) -> Result<()> {
257 if self.security.max_upload_size == 0 {
259 return Err(WeChatError::config_error(
260 "max_upload_size must be greater than 0",
261 ));
262 }
263
264 if self.security.max_download_size == 0 {
265 return Err(WeChatError::config_error(
266 "max_download_size must be greater than 0",
267 ));
268 }
269
270 if self.performance.max_concurrent_uploads == 0 {
272 return Err(WeChatError::config_error(
273 "max_concurrent_uploads must be greater than 0",
274 ));
275 }
276
277 if self.performance.max_concurrent_uploads > 20 {
278 return Err(WeChatError::config_error(
279 "max_concurrent_uploads should not exceed 20",
280 ));
281 }
282
283 if self.http.request_timeout_secs == 0 {
285 return Err(WeChatError::config_error(
286 "request_timeout_secs must be greater than 0",
287 ));
288 }
289
290 if self.http.connect_timeout_secs == 0 {
291 return Err(WeChatError::config_error(
292 "connect_timeout_secs must be greater than 0",
293 ));
294 }
295
296 if self.http.base_url.is_empty() {
297 return Err(WeChatError::config_error("base_url cannot be empty"));
298 }
299
300 if self.retry.max_attempts == 0 {
302 return Err(WeChatError::config_error(
303 "max_attempts must be greater than 0",
304 ));
305 }
306
307 if self.retry.backoff_factor < 1.0 {
308 return Err(WeChatError::config_error("backoff_factor must be >= 1.0"));
309 }
310
311 Ok(())
312 }
313
314 pub fn retry_base_delay(&self) -> Duration {
316 Duration::from_millis(self.retry.base_delay_ms)
317 }
318
319 pub fn retry_max_delay(&self) -> Duration {
321 Duration::from_secs(self.retry.max_delay_secs)
322 }
323
324 pub fn request_timeout(&self) -> Duration {
326 Duration::from_secs(self.http.request_timeout_secs)
327 }
328
329 pub fn connect_timeout(&self) -> Duration {
331 Duration::from_secs(self.http.connect_timeout_secs)
332 }
333
334 pub fn cache_ttl(&self) -> Duration {
336 Duration::from_secs(self.performance.cache_ttl_minutes * 60)
337 }
338}
339
340#[derive(Debug, Default)]
342pub struct ConfigBuilder {
343 security: Option<SecurityConfig>,
344 performance: Option<PerformanceConfig>,
345 http: Option<HttpConfig>,
346 cache: Option<CacheConfig>,
347 retry: Option<RetryConfig>,
348}
349
350impl ConfigBuilder {
351 pub fn security(mut self, security: SecurityConfig) -> Self {
353 self.security = Some(security);
354 self
355 }
356
357 pub fn performance(mut self, performance: PerformanceConfig) -> Self {
359 self.performance = Some(performance);
360 self
361 }
362
363 pub fn http(mut self, http: HttpConfig) -> Self {
365 self.http = Some(http);
366 self
367 }
368
369 pub fn cache(mut self, cache: CacheConfig) -> Self {
371 self.cache = Some(cache);
372 self
373 }
374
375 pub fn retry(mut self, retry: RetryConfig) -> Self {
377 self.retry = Some(retry);
378 self
379 }
380
381 pub fn build(self) -> Config {
383 Config {
384 security: self.security.unwrap_or_default(),
385 performance: self.performance.unwrap_or_default(),
386 http: self.http.unwrap_or_default(),
387 cache: self.cache.unwrap_or_default(),
388 retry: self.retry.unwrap_or_default(),
389 }
390 }
391}
392
393impl SecurityConfig {
396 pub fn builder() -> SecurityConfigBuilder {
398 SecurityConfigBuilder::default()
399 }
400}
401
402impl PerformanceConfig {
403 pub fn builder() -> PerformanceConfigBuilder {
405 PerformanceConfigBuilder::default()
406 }
407}
408
409impl HttpConfig {
410 pub fn builder() -> HttpConfigBuilder {
412 HttpConfigBuilder::default()
413 }
414}
415
416impl CacheConfig {
417 pub fn builder() -> CacheConfigBuilder {
419 CacheConfigBuilder::default()
420 }
421}
422
423impl RetryConfig {
424 pub fn builder() -> RetryConfigBuilder {
426 RetryConfigBuilder::default()
427 }
428}
429
430#[derive(Debug, Default)]
432pub struct SecurityConfigBuilder {
433 max_upload_size: Option<u64>,
434 max_download_size: Option<u64>,
435 validate_file_paths: Option<bool>,
436 sanitize_filenames: Option<bool>,
437 blocked_extensions: Option<Vec<String>>,
438}
439
440impl SecurityConfigBuilder {
441 pub fn max_upload_size(mut self, size: u64) -> Self {
442 self.max_upload_size = Some(size);
443 self
444 }
445
446 pub fn max_download_size(mut self, size: u64) -> Self {
447 self.max_download_size = Some(size);
448 self
449 }
450
451 pub fn validate_file_paths(mut self, validate: bool) -> Self {
452 self.validate_file_paths = Some(validate);
453 self
454 }
455
456 pub fn sanitize_filenames(mut self, sanitize: bool) -> Self {
457 self.sanitize_filenames = Some(sanitize);
458 self
459 }
460
461 pub fn blocked_extensions(mut self, extensions: Vec<String>) -> Self {
462 self.blocked_extensions = Some(extensions);
463 self
464 }
465
466 pub fn build(self) -> SecurityConfig {
467 let default = SecurityConfig::default();
468 SecurityConfig {
469 max_upload_size: self.max_upload_size.unwrap_or(default.max_upload_size),
470 max_download_size: self.max_download_size.unwrap_or(default.max_download_size),
471 validate_file_paths: self
472 .validate_file_paths
473 .unwrap_or(default.validate_file_paths),
474 sanitize_filenames: self
475 .sanitize_filenames
476 .unwrap_or(default.sanitize_filenames),
477 blocked_extensions: self
478 .blocked_extensions
479 .unwrap_or(default.blocked_extensions),
480 }
481 }
482}
483
484#[derive(Debug, Default)]
486pub struct PerformanceConfigBuilder {
487 max_concurrent_uploads: Option<usize>,
488 cache_ttl_minutes: Option<u64>,
489 max_cache_entries: Option<usize>,
490 enable_parallel_processing: Option<bool>,
491}
492
493impl PerformanceConfigBuilder {
494 pub fn max_concurrent_uploads(mut self, count: usize) -> Self {
495 self.max_concurrent_uploads = Some(count);
496 self
497 }
498
499 pub fn cache_ttl_minutes(mut self, minutes: u64) -> Self {
500 self.cache_ttl_minutes = Some(minutes);
501 self
502 }
503
504 pub fn max_cache_entries(mut self, entries: usize) -> Self {
505 self.max_cache_entries = Some(entries);
506 self
507 }
508
509 pub fn enable_parallel_processing(mut self, enable: bool) -> Self {
510 self.enable_parallel_processing = Some(enable);
511 self
512 }
513
514 pub fn build(self) -> PerformanceConfig {
515 let default = PerformanceConfig::default();
516 PerformanceConfig {
517 max_concurrent_uploads: self
518 .max_concurrent_uploads
519 .unwrap_or(default.max_concurrent_uploads),
520 cache_ttl_minutes: self.cache_ttl_minutes.unwrap_or(default.cache_ttl_minutes),
521 max_cache_entries: self.max_cache_entries.unwrap_or(default.max_cache_entries),
522 enable_parallel_processing: self
523 .enable_parallel_processing
524 .unwrap_or(default.enable_parallel_processing),
525 }
526 }
527}
528
529#[derive(Debug, Default)]
531pub struct HttpConfigBuilder {
532 request_timeout_secs: Option<u64>,
533 connect_timeout_secs: Option<u64>,
534 base_url: Option<String>,
535 user_agent: Option<String>,
536}
537
538impl HttpConfigBuilder {
539 pub fn request_timeout_secs(mut self, timeout: u64) -> Self {
540 self.request_timeout_secs = Some(timeout);
541 self
542 }
543
544 pub fn connect_timeout_secs(mut self, timeout: u64) -> Self {
545 self.connect_timeout_secs = Some(timeout);
546 self
547 }
548
549 pub fn base_url(mut self, url: String) -> Self {
550 self.base_url = Some(url);
551 self
552 }
553
554 pub fn user_agent(mut self, agent: String) -> Self {
555 self.user_agent = Some(agent);
556 self
557 }
558
559 pub fn build(self) -> HttpConfig {
560 let default = HttpConfig::default();
561 HttpConfig {
562 request_timeout_secs: self
563 .request_timeout_secs
564 .unwrap_or(default.request_timeout_secs),
565 connect_timeout_secs: self
566 .connect_timeout_secs
567 .unwrap_or(default.connect_timeout_secs),
568 base_url: self.base_url.unwrap_or(default.base_url),
569 user_agent: self.user_agent.unwrap_or(default.user_agent),
570 }
571 }
572}
573
574#[derive(Debug, Default)]
576pub struct CacheConfigBuilder {
577 enable_material_cache: Option<bool>,
578 enable_token_cache: Option<bool>,
579 cleanup_interval_minutes: Option<u64>,
580}
581
582impl CacheConfigBuilder {
583 pub fn enable_material_cache(mut self, enable: bool) -> Self {
584 self.enable_material_cache = Some(enable);
585 self
586 }
587
588 pub fn enable_token_cache(mut self, enable: bool) -> Self {
589 self.enable_token_cache = Some(enable);
590 self
591 }
592
593 pub fn cleanup_interval_minutes(mut self, minutes: u64) -> Self {
594 self.cleanup_interval_minutes = Some(minutes);
595 self
596 }
597
598 pub fn build(self) -> CacheConfig {
599 let default = CacheConfig::default();
600 CacheConfig {
601 enable_material_cache: self
602 .enable_material_cache
603 .unwrap_or(default.enable_material_cache),
604 enable_token_cache: self
605 .enable_token_cache
606 .unwrap_or(default.enable_token_cache),
607 cleanup_interval_minutes: self
608 .cleanup_interval_minutes
609 .unwrap_or(default.cleanup_interval_minutes),
610 }
611 }
612}
613
614#[derive(Debug, Default)]
616pub struct RetryConfigBuilder {
617 max_attempts: Option<u32>,
618 base_delay_ms: Option<u64>,
619 max_delay_secs: Option<u64>,
620 backoff_factor: Option<f64>,
621 enable_jitter: Option<bool>,
622}
623
624impl RetryConfigBuilder {
625 pub fn max_attempts(mut self, attempts: u32) -> Self {
626 self.max_attempts = Some(attempts);
627 self
628 }
629
630 pub fn base_delay_ms(mut self, delay: u64) -> Self {
631 self.base_delay_ms = Some(delay);
632 self
633 }
634
635 pub fn max_delay_secs(mut self, delay: u64) -> Self {
636 self.max_delay_secs = Some(delay);
637 self
638 }
639
640 pub fn backoff_factor(mut self, factor: f64) -> Self {
641 self.backoff_factor = Some(factor);
642 self
643 }
644
645 pub fn enable_jitter(mut self, enable: bool) -> Self {
646 self.enable_jitter = Some(enable);
647 self
648 }
649
650 pub fn build(self) -> RetryConfig {
651 let default = RetryConfig::default();
652 RetryConfig {
653 max_attempts: self.max_attempts.unwrap_or(default.max_attempts),
654 base_delay_ms: self.base_delay_ms.unwrap_or(default.base_delay_ms),
655 max_delay_secs: self.max_delay_secs.unwrap_or(default.max_delay_secs),
656 backoff_factor: self.backoff_factor.unwrap_or(default.backoff_factor),
657 enable_jitter: self.enable_jitter.unwrap_or(default.enable_jitter),
658 }
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn test_default_config() {
668 let config = Config::default();
669 assert!(config.validate().is_ok());
670
671 assert_eq!(config.security.max_upload_size, 10 * 1024 * 1024);
673 assert_eq!(config.performance.max_concurrent_uploads, 5);
674 assert_eq!(config.http.request_timeout_secs, 30);
675 assert_eq!(config.retry.max_attempts, 3);
676 }
677
678 #[test]
679 fn test_config_builder() {
680 let config = Config::builder()
681 .security(
682 SecurityConfig::builder()
683 .max_upload_size(5 * 1024 * 1024)
684 .validate_file_paths(false)
685 .build(),
686 )
687 .performance(
688 PerformanceConfig::builder()
689 .max_concurrent_uploads(10)
690 .cache_ttl_minutes(30)
691 .build(),
692 )
693 .build();
694
695 assert_eq!(config.security.max_upload_size, 5 * 1024 * 1024);
696 assert!(!config.security.validate_file_paths);
697 assert_eq!(config.performance.max_concurrent_uploads, 10);
698 assert_eq!(config.performance.cache_ttl_minutes, 30);
699 }
700
701 #[test]
702 fn test_config_validation() {
703 let mut config = Config::default();
704 config.security.max_upload_size = 0;
705 assert!(config.validate().is_err());
706
707 let mut config = Config::default();
708 config.performance.max_concurrent_uploads = 0;
709 assert!(config.validate().is_err());
710
711 let mut config = Config::default();
712 config.performance.max_concurrent_uploads = 25;
713 assert!(config.validate().is_err());
714
715 let mut config = Config::default();
716 config.retry.backoff_factor = 0.5;
717 assert!(config.validate().is_err());
718 }
719
720 #[test]
721 fn test_duration_conversions() {
722 let config = Config::default();
723
724 assert_eq!(config.retry_base_delay(), Duration::from_millis(500));
725 assert_eq!(config.retry_max_delay(), Duration::from_secs(30));
726 assert_eq!(config.request_timeout(), Duration::from_secs(30));
727 assert_eq!(config.connect_timeout(), Duration::from_secs(10));
728 assert_eq!(config.cache_ttl(), Duration::from_secs(15 * 60));
729 }
730
731 #[test]
732 fn test_environment_loading() {
733 unsafe {
735 std::env::set_var("WECHAT_MAX_UPLOAD_SIZE", "5242880"); std::env::set_var("WECHAT_MAX_CONCURRENT_UPLOADS", "10");
737 std::env::set_var("WECHAT_REQUEST_TIMEOUT", "60");
738 }
739
740 let config = Config::from_env().unwrap();
741
742 assert_eq!(config.security.max_upload_size, 5242880);
743 assert_eq!(config.performance.max_concurrent_uploads, 10);
744 assert_eq!(config.http.request_timeout_secs, 60);
745
746 unsafe {
748 std::env::remove_var("WECHAT_MAX_UPLOAD_SIZE");
749 std::env::remove_var("WECHAT_MAX_CONCURRENT_UPLOADS");
750 std::env::remove_var("WECHAT_REQUEST_TIMEOUT");
751 }
752 }
753
754 #[test]
755 fn test_invalid_environment_values() {
756 unsafe {
757 std::env::set_var("WECHAT_MAX_UPLOAD_SIZE", "invalid");
758 }
759 assert!(Config::from_env().is_err());
760 unsafe {
761 std::env::remove_var("WECHAT_MAX_UPLOAD_SIZE");
762 }
763 }
764}