1use std::path::Path;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::fs;
16use parking_lot::RwLock;
17use tracing::info;
18
19pub mod env;
20pub use env::{detect_profile, parse_args, merge_env_overrides};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct AppConfig {
25 #[serde(default = "default_app_name")]
27 pub app_name: String,
28
29 #[serde(default = "default_profile")]
31 pub profile: String,
32
33 #[serde(default)]
35 pub server: ServerConfig,
36
37 #[serde(default)]
39 pub log: LogConfig,
40
41 #[serde(default)]
43 pub database: DatabaseConfig,
44
45 #[serde(default)]
47 pub redis: RedisConfig,
48
49 #[serde(default)]
51 pub cache: CacheConfig,
52
53 #[serde(default)]
55 pub middleware: MiddlewareConfig,
56
57 #[serde(default)]
59 pub router: RouterConfig,
60
61 #[serde(default)]
63 pub plugins: PluginsConfig,
64
65 #[serde(default)]
67 pub upload: UploadConfig,
68
69 #[serde(default)]
71 pub download: DownloadConfig,
72
73 #[serde(default)]
75 pub template: TemplateConfig,
76
77 #[serde(default)]
79 pub static_files: StaticConfig,
80
81 #[serde(default)]
83 pub custom: HashMap<String, serde_json::Value>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ServerConfig {
89 #[serde(default = "default_listen")]
91 pub listen: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct LogConfig {
97 #[serde(default = "default_log_level")]
99 pub level: String,
100
101 #[serde(default = "default_log_format")]
103 pub format: String,
104
105 #[serde(default)]
107 pub dir: Option<String>,
108
109 #[serde(default = "default_log_prefix")]
111 pub file_prefix: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct DatabaseConfig {
117 #[serde(default = "default_true")]
119 pub enabled: bool,
120
121 #[serde(default = "default_db_type")]
123 pub r#type: String,
124
125 #[serde(default = "default_host")]
127 pub host: String,
128
129 pub port: Option<u16>,
131
132 #[serde(default)]
134 pub name: String,
135
136 #[serde(default)]
138 pub user: String,
139
140 #[serde(default)]
142 pub password: String,
143
144 #[serde(default)]
146 pub password_encrypted: bool,
147
148 #[serde(default = "default_pool_size")]
150 pub max_connections: u32,
151
152 #[serde(default = "default_min_idle")]
154 pub min_connections: u32,
155
156 #[serde(default = "default_timeout")]
158 pub connect_timeout: u64,
159
160 #[serde(default)]
162 pub sql_logging: bool,
163
164 #[serde(default)]
166 pub slow_query_ms: u64,
167
168 #[serde(default)]
170 pub migration: MigrationConfig,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct RedisConfig {
176 #[serde(default)]
178 pub enabled: bool,
179
180 #[serde(default = "default_redis_url")]
182 pub url: String,
183
184 #[serde(default = "default_pool_size")]
186 pub max_connections: u32,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct CacheConfig {
192 #[serde(default = "default_cache_type")]
194 pub r#type: String,
195
196 #[serde(default = "default_cache_capacity")]
198 pub max_capacity: u64,
199
200 #[serde(default = "default_ttl")]
202 pub default_ttl: u64,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct MiddlewareConfig {
208 #[serde(default)]
210 pub request_id: bool,
211
212 #[serde(default)]
214 pub request_log: bool,
215
216 #[serde(default)]
218 pub request_log_config: RequestLogConfig,
219
220 #[serde(default)]
222 pub auth: AuthMiddlewareConfig,
223
224 #[serde(default)]
226 pub cors: CorsConfig,
227
228 #[serde(default)]
230 pub compression: CompressConfig,
231
232 #[serde(default)]
234 pub rate_limit: RateLimitConfig,
235
236 #[serde(default)]
238 pub security_headers: SecurityHeadersConfig,
239
240 #[serde(default)]
242 pub permission: PermissionConfig,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct RequestLogConfig {
248 #[serde(default)]
250 pub exclude_paths: Vec<String>,
251
252 #[serde(default = "default_true")]
254 pub log_duration: bool,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct AuthMiddlewareConfig {
260 #[serde(default)]
261 pub enabled: bool,
262
263 #[serde(default)]
265 pub ignore_paths: Vec<String>,
266
267 #[serde(default)]
269 pub jwt_secret: String,
270
271 #[serde(default = "default_access_token_expire")]
273 pub access_token_expire_secs: u64,
274
275 #[serde(default = "default_refresh_token_expire")]
277 pub refresh_token_expire_secs: u64,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct CorsConfig {
283 #[serde(default)]
284 pub enabled: bool,
285
286 #[serde(default)]
287 pub allow_origins: Vec<String>,
288
289 #[serde(default)]
290 pub allow_methods: Vec<String>,
291
292 #[serde(default)]
293 pub allow_headers: Vec<String>,
294
295 #[serde(default = "default_true")]
296 pub allow_credentials: bool,
297
298 #[serde(default = "default_cors_max_age")]
299 pub max_age_secs: u64,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct CompressConfig {
305 #[serde(default)]
307 pub enabled: bool,
308
309 #[serde(default = "default_compress_level")]
311 pub level: u32,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct RateLimitConfig {
317 #[serde(default)]
318 pub enabled: bool,
319
320 #[serde(default = "default_rate_limit_requests")]
322 pub requests_per_window: u64,
323
324 #[serde(default = "default_rate_limit_window")]
326 pub window_secs: u64,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct SecurityHeadersConfig {
335 #[serde(default = "default_true")]
337 pub enabled: bool,
338
339 #[serde(default = "default_true")]
341 pub nosniff: bool,
342
343 #[serde(default = "default_true")]
345 pub frame_options: bool,
346
347 #[serde(default = "default_true")]
349 pub hsts: bool,
350
351 #[serde(default = "default_hsts_max_age")]
353 pub hsts_max_age_secs: u64,
354
355 #[serde(default = "default_true")]
357 pub hsts_include_subdomains: bool,
358
359 #[serde(default = "default_true")]
361 pub csp: bool,
362
363 #[serde(default = "default_csp_value")]
365 pub csp_value: String,
366
367 #[serde(default = "default_true")]
369 pub referrer_policy: bool,
370
371 #[serde(default = "default_referrer_policy_value")]
373 pub referrer_policy_value: String,
374
375 #[serde(default)]
377 pub permissions_policy: bool,
378
379 #[serde(default = "default_permissions_policy_value")]
381 pub permissions_policy_value: String,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct PermissionConfig {
391 #[serde(default)]
393 pub enabled: bool,
394
395 #[serde(default)]
397 pub rules: Vec<PermissionRule>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct PermissionRule {
403 pub path: String,
405 #[serde(default)]
407 pub methods: Vec<String>,
408 pub permission: String,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct RouterConfig {
415 #[serde(default)]
417 pub prefix: String,
418 #[serde(default)]
420 pub not_found: NotFoundConfig,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct NotFoundConfig {
426 #[serde(default = "default_true")]
428 pub enabled: bool,
429 #[serde(default = "default_not_found_msg")]
431 pub message: String,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct MigrationConfig {
437 #[serde(default)]
439 pub enabled: bool,
440
441 #[serde(default = "default_migration_path")]
443 pub path: String,
444
445 #[serde(default)]
447 pub auto_migrate: bool,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct UploadConfig {
453 #[serde(default = "default_upload_path")]
455 pub path: String,
456
457 #[serde(default = "default_max_size")]
459 pub max_size_mb: u64,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct DownloadConfig {
465 #[serde(default = "default_download_path")]
467 pub path: String,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct TemplateConfig {
473 #[serde(default = "default_template_path")]
475 pub path: String,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct StaticConfig {
481 #[serde(default = "default_static_path")]
483 pub path: String,
484
485 #[serde(default)]
487 pub enabled: bool,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct PluginsConfig {
493 #[serde(default)]
495 pub enabled: Vec<String>,
496
497 #[serde(default)]
499 pub notification: NotificationConfig,
500
501 #[serde(default)]
503 pub async_task: AsyncTaskConfig,
504
505 #[serde(default)]
507 pub scheduler: SchedulerConfig,
508
509 #[serde(default)]
511 pub serial: SerialConfig,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, Default)]
515pub struct NotificationConfig {
516 #[serde(default)]
518 pub enabled: bool,
519 #[serde(default)]
521 pub smtp_host: String,
522 #[serde(default = "default_smtp_port")]
524 pub smtp_port: u16,
525 #[serde(default)]
527 pub smtp_user: String,
528 #[serde(default)]
530 pub smtp_pass: String,
531 #[serde(default)]
533 pub from_email: String,
534 #[serde(default)]
536 pub from_name: String,
537}
538
539fn default_smtp_port() -> u16 { 587 }
540
541#[derive(Debug, Clone, Serialize, Deserialize, Default)]
542pub struct AsyncTaskConfig {
543 #[serde(default = "default_workers")]
545 pub workers: usize,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize, Default)]
549pub struct SchedulerConfig {
550 #[serde(default = "default_workers")]
552 pub workers: usize,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
557pub struct SerialConfig {
558 #[serde(default = "default_serial_backend")]
560 pub backend: String,
561 #[serde(default)]
563 pub rules: Vec<SerialRuleConfig>,
564}
565
566impl Default for SerialConfig {
567 fn default() -> Self {
568 Self { backend: "memory".into(), rules: Vec::new() }
569 }
570}
571
572fn default_serial_backend() -> String { "memory".into() }
573
574#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct SerialRuleConfig {
577 pub key: String,
579 pub format: String,
581 #[serde(default = "default_cycle")]
583 pub cycle: String,
584 #[serde(default = "default_initial")]
586 pub initial_value: u64,
587 #[serde(default = "default_step_strategy")]
589 pub step: String,
590}
591
592fn default_cycle() -> String { "no_cycle".into() }
593fn default_initial() -> u64 { 1 }
594fn default_step_strategy() -> String { "sequential".into() }
595
596fn default_app_name() -> String { "Alun".into() }
599fn default_profile() -> String { "dev".into() }
600fn default_listen() -> String { "8023".into() }
601fn default_log_level() -> String { "info".into() }
602fn default_log_format() -> String { "text".into() }
603fn default_log_prefix() -> String { "alun".into() }
604fn default_db_type() -> String { "postgres".into() }
605fn default_host() -> String { "localhost".into() }
606fn default_true() -> bool { true }
607fn default_pool_size() -> u32 { 10 }
608fn default_min_idle() -> u32 { 2 }
609fn default_timeout() -> u64 { 10 }
610fn default_workers() -> usize { 4 }
611fn default_redis_url() -> String { "redis://127.0.0.1:6379".into() }
612fn default_cache_type() -> String { "local".into() }
613fn default_cache_capacity() -> u64 { 10000 }
614fn default_ttl() -> u64 { 3600 }
615fn default_access_token_expire() -> u64 { 7200 }
616fn default_refresh_token_expire() -> u64 { 604800 }
617fn default_cors_max_age() -> u64 { 86400 }
618fn default_compress_level() -> u32 { 6 }
619fn default_rate_limit_requests() -> u64 { 100 }
620fn default_rate_limit_window() -> u64 { 60 }
621fn default_hsts_max_age() -> u64 { 31536000 }
622fn default_csp_value() -> String { "default-src 'self'".into() }
623fn default_referrer_policy_value() -> String { "strict-origin-when-cross-origin".into() }
624fn default_permissions_policy_value() -> String {
625 "camera=(), microphone=(), geolocation=()".into()
626}
627fn default_migration_path() -> String { "migrations".into() }
628fn default_upload_path() -> String { "uploads".into() }
629fn default_download_path() -> String { "downloads".into() }
630fn default_template_path() -> String { "templates".into() }
631fn default_static_path() -> String { "static".into() }
632fn default_not_found_msg() -> String { "请求的资源不存在".into() }
633fn default_max_size() -> u64 { 10 }
634
635impl Default for AppConfig {
636 fn default() -> Self {
637 Self {
638 app_name: default_app_name(),
639 profile: default_profile(),
640 server: ServerConfig::default(),
641 log: LogConfig::default(),
642 database: DatabaseConfig::default(),
643 redis: RedisConfig::default(),
644 cache: CacheConfig::default(),
645 middleware: MiddlewareConfig::default(),
646 router: RouterConfig::default(),
647 plugins: PluginsConfig::default(),
648 upload: UploadConfig::default(),
649 download: DownloadConfig::default(),
650 template: TemplateConfig::default(),
651 static_files: StaticConfig::default(),
652 custom: HashMap::new(),
653 }
654 }
655}
656
657impl Default for ServerConfig { fn default() -> Self { Self { listen: default_listen() } } }
658impl Default for LogConfig {
659 fn default() -> Self {
660 Self {
661 level: default_log_level(),
662 format: default_log_format(),
663 dir: None,
664 file_prefix: default_log_prefix(),
665 }
666 }
667}
668impl Default for DatabaseConfig {
669 fn default() -> Self {
670 Self {
671 enabled: false, r#type: default_db_type(), host: default_host(),
672 port: None, name: String::new(), user: String::new(), password: String::new(),
673 password_encrypted: false,
674 max_connections: default_pool_size(), min_connections: default_min_idle(),
675 connect_timeout: default_timeout(), sql_logging: false, slow_query_ms: 0,
676 migration: MigrationConfig::default(),
677 }
678 }
679}
680impl Default for RedisConfig { fn default() -> Self { Self { enabled: false, url: default_redis_url(), max_connections: default_pool_size() } } }
681impl Default for CacheConfig { fn default() -> Self { Self { r#type: default_cache_type(), max_capacity: default_cache_capacity(), default_ttl: default_ttl() } } }
682impl Default for MiddlewareConfig {
683 fn default() -> Self {
684 Self {
685 request_id: false, request_log: false,
686 request_log_config: RequestLogConfig::default(),
687 auth: AuthMiddlewareConfig::default(),
688 cors: CorsConfig::default(),
689 compression: CompressConfig::default(),
690 rate_limit: RateLimitConfig::default(),
691 security_headers: SecurityHeadersConfig::default(),
692 permission: PermissionConfig::default(),
693 }
694 }
695}
696impl Default for RequestLogConfig {
697 fn default() -> Self { Self { exclude_paths: vec![], log_duration: true } }
698}
699impl Default for AuthMiddlewareConfig { fn default() -> Self { Self { enabled: false, ignore_paths: vec![], jwt_secret: String::new(), access_token_expire_secs: default_access_token_expire(), refresh_token_expire_secs: default_refresh_token_expire() } } }
700impl Default for CorsConfig { fn default() -> Self { Self { enabled: false, allow_origins: vec![], allow_methods: vec![], allow_headers: vec![], allow_credentials: true, max_age_secs: default_cors_max_age() } } }
701impl Default for CompressConfig { fn default() -> Self { Self { enabled: false, level: default_compress_level() } } }
702impl Default for RateLimitConfig { fn default() -> Self { Self { enabled: false, requests_per_window: default_rate_limit_requests(), window_secs: default_rate_limit_window() } } }
703impl Default for SecurityHeadersConfig {
704 fn default() -> Self {
705 Self {
706 enabled: true,
707 nosniff: true, frame_options: true,
708 hsts: true, hsts_max_age_secs: default_hsts_max_age(),
709 hsts_include_subdomains: true,
710 csp: true, csp_value: default_csp_value(),
711 referrer_policy: true, referrer_policy_value: default_referrer_policy_value(),
712 permissions_policy: false, permissions_policy_value: default_permissions_policy_value(),
713 }
714 }
715}
716impl Default for PermissionConfig { fn default() -> Self { Self { enabled: false, rules: vec![] } } }
717impl Default for PermissionRule { fn default() -> Self { Self { path: String::new(), methods: vec![], permission: String::new() } } }
718impl Default for RouterConfig { fn default() -> Self { Self { prefix: String::new(), not_found: NotFoundConfig::default() } } }
719impl Default for NotFoundConfig { fn default() -> Self { Self { enabled: true, message: default_not_found_msg() } } }
720impl Default for MigrationConfig { fn default() -> Self { Self { enabled: false, path: default_migration_path(), auto_migrate: false } } }
721impl Default for UploadConfig { fn default() -> Self { Self { path: default_upload_path(), max_size_mb: default_max_size() } } }
722impl Default for DownloadConfig { fn default() -> Self { Self { path: default_download_path() } } }
723impl Default for TemplateConfig { fn default() -> Self { Self { path: default_template_path() } } }
724impl Default for StaticConfig { fn default() -> Self { Self { path: default_static_path(), enabled: false } } }
725impl Default for PluginsConfig { fn default() -> Self { Self { enabled: vec![], notification: NotificationConfig::default(), async_task: AsyncTaskConfig::default(), scheduler: SchedulerConfig::default(), serial: SerialConfig::default() } } }
726
727pub struct ConfigManager {
731 pub static_config: AppConfig,
733 pub dynamic: RwLock<HashMap<String, serde_json::Value>>,
735}
736
737impl ConfigManager {
738 pub fn load(config_dir: Option<&str>) -> Self {
740 let dir = config_dir.unwrap_or("config");
741 let profile = detect_profile();
742
743 let mut cfg = Self::load_file(dir, &profile);
744
745 merge_env_overrides(&mut cfg);
747
748 info!("配置加载完成 profile={}, listen={}", cfg.profile, cfg.server.listen);
749
750 Self {
751 static_config: cfg,
752 dynamic: RwLock::new(HashMap::new()),
753 }
754 }
755
756 fn load_file(dir: &str, profile: &str) -> AppConfig {
757 let base_path = Path::new(dir).join("config.toml");
759 let mut cfg = if base_path.exists() {
760 let content = fs::read_to_string(&base_path)
761 .unwrap_or_else(|_| String::new());
762 toml::from_str(&content).unwrap_or_default()
763 } else {
764 AppConfig::default()
765 };
766
767 let profile_path = Path::new(dir).join(format!("config-{}.toml", profile));
770 if profile_path.exists() {
771 if let Ok(content) = fs::read_to_string(&profile_path) {
772 if let Ok(profile_cfg) = toml::from_str::<AppConfig>(&content) {
773 merge_configs(&mut cfg, &profile_cfg);
774 }
775 }
776 }
777
778 cfg.profile = profile.to_string();
779 cfg
780 }
781
782 pub fn get(&self) -> &AppConfig {
784 &self.static_config
785 }
786
787 pub fn get_dynamic(&self, key: &str) -> Option<serde_json::Value> {
789 self.dynamic.read().get(key).cloned()
790 }
791
792 pub fn set_dynamic(&self, key: &str, value: serde_json::Value) {
794 self.dynamic.write().insert(key.to_string(), value);
795 }
796
797 pub fn remove_dynamic(&self, key: &str) {
799 self.dynamic.write().remove(key);
800 }
801
802 pub fn generate_default(dir: &str) -> std::io::Result<()> {
804 let config_dir = Path::new(dir);
805 fs::create_dir_all(config_dir)?;
806
807 let cfg = AppConfig::default();
808 let toml_str = toml::to_string_pretty(&cfg)
809 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
810
811 let header = r#"# Alun 默认配置文件
812# 修改后保存即可生效(需重启服务)
813#
814# 使用 --gen-config 参数可重新生成此文件到 config/config.toml
815# 多环境:创建 config/config-dev.toml, config/config-prod.toml
816# 通过环境变量或命令行 --profile=prod 指定
817
818"#;
819
820 fs::write(config_dir.join("config.toml"), format!("{}{}", header, toml_str))?;
821 info!("默认配置文件已生成到 {}/config.toml", dir);
822 Ok(())
823 }
824}
825
826fn merge_configs(base: &mut AppConfig, overlay: &AppConfig) {
828 if overlay.server.listen != default_listen() { base.server.listen = overlay.server.listen.clone(); }
829 if overlay.log.level != default_log_level() { base.log.level = overlay.log.level.clone(); }
830 if overlay.log.format != default_log_format() { base.log.format = overlay.log.format.clone(); }
831 if overlay.log.dir.is_some() { base.log.dir = overlay.log.dir.clone(); }
832 if overlay.log.file_prefix != default_log_prefix() { base.log.file_prefix = overlay.log.file_prefix.clone(); }
833 if overlay.database.host != default_host() || !overlay.database.name.is_empty() {
834 base.database = overlay.database.clone();
835 }
836 if overlay.redis.url != default_redis_url() { base.redis = overlay.redis.clone(); }
837 if overlay.cache.r#type != default_cache_type() { base.cache = overlay.cache.clone(); }
838 if overlay.router.prefix != String::new() { base.router.prefix = overlay.router.prefix.clone(); }
839 if overlay.router.not_found.message != default_not_found_msg() {
840 base.router.not_found.message = overlay.router.not_found.message.clone();
841 }
842 if !overlay.router.not_found.enabled {
843 base.router.not_found.enabled = false;
844 }
845 if overlay.upload.path != default_upload_path() { base.upload = overlay.upload.clone(); }
846 if overlay.download.path != default_download_path() { base.download = overlay.download.clone(); }
847 if overlay.template.path != default_template_path() { base.template = overlay.template.clone(); }
848 if overlay.static_files.path != default_static_path() { base.static_files = overlay.static_files.clone(); }
849
850 merge_middleware(&mut base.middleware, &overlay.middleware);
852
853 if !overlay.plugins.enabled.is_empty() {
855 base.plugins = overlay.plugins.clone();
856 }
857 for (k, v) in &overlay.custom { base.custom.insert(k.clone(), v.clone()); }
858}
859
860fn merge_middleware(base: &mut MiddlewareConfig, overlay: &MiddlewareConfig) {
861 let default_mw = MiddlewareConfig::default();
862 if overlay.request_id != default_mw.request_id { base.request_id = overlay.request_id; }
863 if overlay.request_log != default_mw.request_log { base.request_log = overlay.request_log; }
864 if overlay.request_log_config.log_duration != default_mw.request_log_config.log_duration {
865 base.request_log_config.log_duration = overlay.request_log_config.log_duration;
866 }
867 if !overlay.request_log_config.exclude_paths.is_empty() {
868 base.request_log_config.exclude_paths = overlay.request_log_config.exclude_paths.clone();
869 }
870 if overlay.auth.enabled != default_mw.auth.enabled { base.auth.enabled = overlay.auth.enabled; }
871 if overlay.auth.jwt_secret != default_mw.auth.jwt_secret { base.auth.jwt_secret = overlay.auth.jwt_secret.clone(); }
872 if overlay.auth.access_token_expire_secs != 0 { base.auth.access_token_expire_secs = overlay.auth.access_token_expire_secs; }
873 if overlay.auth.refresh_token_expire_secs != 0 { base.auth.refresh_token_expire_secs = overlay.auth.refresh_token_expire_secs; }
874 if !overlay.auth.ignore_paths.is_empty() { base.auth.ignore_paths = overlay.auth.ignore_paths.clone(); }
875 if overlay.cors.enabled != default_mw.cors.enabled { base.cors.enabled = overlay.cors.enabled; }
876 if !overlay.cors.allow_origins.is_empty() { base.cors.allow_origins = overlay.cors.allow_origins.clone(); }
877 if overlay.compression.enabled != default_mw.compression.enabled { base.compression.enabled = overlay.compression.enabled; }
878 if overlay.rate_limit.enabled != default_mw.rate_limit.enabled { base.rate_limit.enabled = overlay.rate_limit.enabled; }
879 if overlay.rate_limit.requests_per_window != 0 { base.rate_limit.requests_per_window = overlay.rate_limit.requests_per_window; }
880 if overlay.rate_limit.window_secs != 0 { base.rate_limit.window_secs = overlay.rate_limit.window_secs; }
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886
887 #[test]
888 fn test_default_config_serialization() {
889 let cfg = AppConfig::default();
890 let toml_str = toml::to_string_pretty(&cfg).unwrap();
891 assert!(toml_str.contains("listen = \"8023\""));
892 assert!(toml_str.contains("level = \"info\""));
893 }
894}