1use serde::de::DeserializeOwned;
18use serde::Deserialize;
19use std::collections::HashMap;
20use std::path::Path;
21use thiserror::Error;
22
23#[derive(Debug, Error)]
25pub enum ConfigError {
26 #[error("配置文件读取失败: {path} — {source}")]
28 FileRead {
29 path: String,
31 #[source]
33 source: std::io::Error,
34 },
35 #[error("配置文件解析失败: {path} — {source}")]
37 Parse {
38 path: String,
40 #[source]
42 source: serde_yml::Error,
43 },
44}
45
46#[derive(Debug, Clone, Deserialize, Default)]
48pub struct AppConfig {
49 #[serde(default)]
51 pub app: AppSection,
52 #[serde(default)]
54 pub database: DatabaseSection,
55 #[serde(default)]
57 pub cache: CacheSection,
58 #[serde(default)]
60 pub addons: AddonsSection,
61 #[serde(default)]
63 pub log: LogSection,
64 #[serde(default)]
66 pub server: ServerSection,
67}
68
69#[derive(Debug, Clone, Deserialize)]
71pub struct AppSection {
72 #[serde(default)]
74 pub app_host: String,
75 #[serde(default)]
77 pub app_namespace: String,
78 #[serde(default = "default_true")]
80 pub with_route: bool,
81 #[serde(default = "default_true")]
83 pub with_event: bool,
84 #[serde(default = "default_default_app")]
86 pub default_app: String,
87 #[serde(default = "default_timezone")]
89 pub default_timezone: String,
90 #[serde(default = "default_true")]
92 pub auto_multi_app: bool,
93 #[serde(default = "default_app_map")]
95 pub app_map: HashMap<String, String>,
96 #[serde(default = "default_deny_app_list")]
98 pub deny_app_list: Vec<String>,
99}
100
101impl Default for AppSection {
102 fn default() -> Self {
103 Self {
104 app_host: String::new(),
105 app_namespace: String::new(),
106 with_route: true,
107 with_event: true,
108 default_app: default_default_app(),
109 default_timezone: default_timezone(),
110 auto_multi_app: true,
111 app_map: default_app_map(),
112 deny_app_list: default_deny_app_list(),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Deserialize)]
119pub struct DatabaseSection {
120 #[serde(default = "default_mysql")]
122 pub default: String,
123 #[serde(default = "default_true")]
125 pub auto_timestamp: bool,
126 #[serde(default = "default_datetime_format")]
128 pub datetime_format: String,
129 #[serde(default)]
131 pub connections: HashMap<String, DatabaseConnection>,
132}
133
134impl Default for DatabaseSection {
135 fn default() -> Self {
136 Self {
137 default: default_mysql(),
138 auto_timestamp: true,
139 datetime_format: default_datetime_format(),
140 connections: HashMap::new(),
141 }
142 }
143}
144
145#[derive(Debug, Clone, Deserialize)]
147pub struct DatabaseConnection {
148 #[serde(default = "default_mysql")]
150 pub r#type: String,
151 #[serde(default)]
153 pub hostname: String,
154 #[serde(default)]
156 pub database: String,
157 #[serde(default)]
159 pub username: String,
160 #[serde(default, skip_serializing)]
165 pub password: String,
166 #[serde(default = "default_port_8802")]
168 pub hostport: u16,
169 #[serde(default = "default_charset_utf8mb4")]
171 pub charset: String,
172 #[serde(default)]
174 pub prefix: String,
175 #[serde(default)]
177 pub deploy: u8,
178 #[serde(default)]
180 pub rw_separate: bool,
181 #[serde(default = "default_true")]
183 pub fields_strict: bool,
184 #[serde(default = "default_true")]
186 pub break_reconnect: bool,
187}
188
189#[derive(Debug, Clone, Deserialize, Default)]
191pub struct CacheSection {
192 #[serde(default = "default_cache_memory")]
194 pub default: String,
195 #[serde(default)]
197 pub stores: HashMap<String, CacheStore>,
198}
199
200#[derive(Debug, Clone, Deserialize, Default)]
202pub struct CacheStore {
203 #[serde(default)]
205 pub r#type: String,
206 #[serde(default)]
208 pub capacity: usize,
209 #[serde(default)]
211 pub levels: Vec<String>,
212}
213
214#[derive(Debug, Clone, Deserialize, Default)]
216pub struct AddonsSection {
217 #[serde(default = "default_addons_path")]
219 pub addons_path: String,
220 #[serde(default)]
222 pub priority: AddonsPriority,
223}
224
225#[derive(Debug, Clone, Deserialize, Default)]
227pub struct AddonsPriority {
228 #[serde(default)]
230 pub p0: Vec<String>,
231 #[serde(default)]
233 pub p1: Vec<String>,
234 #[serde(default)]
236 pub p2: Vec<String>,
237}
238
239#[derive(Debug, Clone, Deserialize, Default)]
241pub struct LogSection {
242 #[serde(default = "default_log_file")]
244 pub default: String,
245 #[serde(default)]
247 pub channels: HashMap<String, LogChannel>,
248}
249
250#[derive(Debug, Clone, Deserialize, Default)]
252pub struct LogChannel {
253 #[serde(default)]
255 pub r#type: String,
256 #[serde(default)]
258 pub path: String,
259 #[serde(default = "default_log_level")]
261 pub level: String,
262 #[serde(default)]
264 pub max_files: u32,
265 #[serde(default)]
267 pub format: String,
268}
269
270fn default_true() -> bool {
275 true
276}
277
278fn default_default_app() -> String {
279 "index".to_string()
280}
281
282fn default_timezone() -> String {
283 "Asia/Shanghai".to_string()
284}
285
286fn default_app_map() -> HashMap<String, String> {
287 let mut map = HashMap::new();
288 map.insert("oapc".to_string(), "oapc".to_string());
289 map.insert("admin".to_string(), "admin".to_string());
290 map.insert("api".to_string(), "api".to_string());
291 map.insert("farm".to_string(), "farm".to_string());
292 map.insert("oapi".to_string(), "oapi".to_string());
293 map.insert("cashier".to_string(), "cashier".to_string());
294 map.insert("scene".to_string(), "scene".to_string());
295 map
296}
297
298fn default_deny_app_list() -> Vec<String> {
299 vec!["common".to_string()]
300}
301
302#[derive(Debug, Clone, Deserialize)]
307pub struct ServerSection {
308 #[serde(default = "default_server_host")]
310 pub host: String,
311 #[serde(default = "default_server_port")]
313 pub port: u16,
314}
315
316impl Default for ServerSection {
317 fn default() -> Self {
318 Self {
319 host: default_server_host(),
320 port: default_server_port(),
321 }
322 }
323}
324
325fn default_server_host() -> String {
326 "0.0.0.0".to_string()
327}
328
329fn default_server_port() -> u16 {
330 8080
331}
332
333fn default_mysql() -> String {
334 "mysql".to_string()
335}
336
337fn default_datetime_format() -> String {
338 "Y-m-d H:i:s".to_string()
339}
340
341fn default_port_8802() -> u16 {
342 8802
343}
344
345fn default_charset_utf8mb4() -> String {
346 "utf8mb4".to_string()
347}
348
349fn default_cache_memory() -> String {
350 "memory".to_string()
351}
352
353fn default_addons_path() -> String {
354 "addons".to_string()
355}
356
357fn default_log_file() -> String {
358 "file".to_string()
359}
360
361fn default_log_level() -> String {
362 "info".to_string()
363}
364
365impl AppConfig {
370 #[tracing::instrument(skip_all)]
383 pub fn load_from_dir(config_dir: impl AsRef<Path>) -> Result<Self, ConfigError> {
384 let dir = config_dir.as_ref();
385
386 let mut config = AppConfig {
388 app: load_section(&dir.join("app.yml"), AppSection::default())?,
389 database: load_section(&dir.join("database.yml"), DatabaseSection::default())?,
390 cache: load_section(&dir.join("cache.yml"), CacheSection::default())?,
391 addons: load_section(&dir.join("addons.yml"), AddonsSection::default())?,
392 log: load_section(&dir.join("log.yml"), LogSection::default())?,
393 server: load_section(&dir.join("server.yml"), ServerSection::default())?,
394 };
395
396 config.apply_env_overrides();
398
399 Ok(config)
400 }
401
402 #[tracing::instrument(skip(self))]
410 pub fn apply_env_overrides(&mut self) {
411 for (conn_name, conn) in &mut self.database.connections {
413 let prefix = format!("SZ_DB_{}", conn_name.to_uppercase());
414
415 let env_key = format!("{}_PASSWORD", prefix);
417 if let Ok(password) = std::env::var(&env_key) {
418 if !password.is_empty() {
419 conn.password = password;
420 }
421 }
422
423 let env_key = format!("{}_HOSTNAME", prefix);
425 if let Ok(hostname) = std::env::var(&env_key) {
426 if !hostname.is_empty() {
427 conn.hostname = hostname;
428 }
429 }
430
431 let env_key = format!("{}_HOSTPORT", prefix);
433 if let Ok(hostport_str) = std::env::var(&env_key) {
434 if !hostport_str.is_empty() {
435 if let Ok(hostport) = hostport_str.parse() {
436 conn.hostport = hostport;
437 }
438 }
439 }
440 }
441 }
442
443 pub fn default_connection(&self) -> Option<&DatabaseConnection> {
445 self.database.connections.get(&self.database.default)
446 }
447}
448
449fn load_section<T: DeserializeOwned + Default>(path: &Path, default: T) -> Result<T, ConfigError> {
451 if !path.exists() {
452 return Ok(default);
453 }
454 let content = std::fs::read_to_string(path).map_err(|e| ConfigError::FileRead {
455 path: path.display().to_string(),
456 source: e,
457 })?;
458 serde_yml::from_str(&content).map_err(|e| ConfigError::Parse {
459 path: path.display().to_string(),
460 source: e,
461 })
462}
463
464#[cfg(test)]
469mod tests {
470 use super::*;
471
472 static ENV_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
475
476 #[test]
478 fn test_default_config() {
479 let config = AppConfig::default();
480 assert!(config.app.auto_multi_app);
481 assert!(config.app.with_route);
482 assert_eq!(config.app.default_app, "index");
483 assert_eq!(config.app.default_timezone, "Asia/Shanghai");
484 assert_eq!(config.app.app_map.len(), 7);
485 assert!(config.app.app_map.contains_key("oapc"));
486 assert_eq!(config.app.deny_app_list, vec!["common"]);
487
488 assert_eq!(config.database.default, "mysql");
489 assert!(config.database.auto_timestamp);
490 assert_eq!(config.database.datetime_format, "Y-m-d H:i:s");
491
492 assert_eq!(config.server.host, "0.0.0.0");
494 assert_eq!(config.server.port, 8080);
495 }
496
497 #[test]
499 fn test_load_from_yaml_string() {
500 let yaml = r#"
501app_host: "https://example.com"
502default_app: "api"
503auto_multi_app: true
504app_map:
505 oapc: oapc
506 admin: admin
507"#;
508 let app: AppSection = serde_yml::from_str(yaml).unwrap();
509 assert_eq!(app.app_host, "https://example.com");
510 assert_eq!(app.default_app, "api");
511 assert!(app.auto_multi_app);
512 assert_eq!(app.app_map.len(), 2);
513 }
514
515 #[test]
517 fn test_load_from_dir() {
518 let config_dir = std::env::current_dir().ok().and_then(|d| {
520 let mut current = d.clone();
523 for _ in 0..5 {
524 if current.join("config").exists() {
525 return Some(current.join("config"));
526 }
527 if let Some(parent) = current.parent() {
528 current = parent.to_path_buf();
529 } else {
530 break;
531 }
532 }
533 None
534 });
535
536 if let Some(config_dir) = config_dir {
537 let config = AppConfig::load_from_dir(&config_dir).unwrap();
538 assert_eq!(config.app.default_app, "index");
540 assert!(config.app.auto_multi_app);
541 assert_eq!(config.app.app_map.len(), 7);
542 assert_eq!(config.app.deny_app_list, vec!["common"]);
543
544 assert_eq!(config.database.default, "mysql");
546 assert_eq!(config.database.connections.len(), 5);
547 assert!(config.database.connections.contains_key("mysql"));
548 assert!(config.database.connections.contains_key("njszjt"));
549 assert!(config.database.connections.contains_key("ljclz"));
550 assert!(config.database.connections.contains_key("food"));
551 assert!(config.database.connections.contains_key("oceanbase"));
552
553 let mysql = config.database.connections.get("mysql").unwrap();
555 assert_eq!(mysql.hostname, "localhost");
556 assert_eq!(mysql.hostport, 8802);
557 assert_eq!(mysql.charset, "utf8mb4");
558 assert_eq!(mysql.prefix, "sz_");
559
560 let ljclz = config.database.connections.get("ljclz").unwrap();
562 assert_eq!(ljclz.charset, "utf8");
563 assert_eq!(ljclz.prefix, "ims_");
564
565 let oceanbase = config.database.connections.get("oceanbase").unwrap();
567 assert_eq!(oceanbase.hostport, 2881);
568 assert_eq!(oceanbase.hostname, "localhost");
569
570 assert_eq!(config.cache.default, "memory");
572 assert!(config.cache.stores.contains_key("memory"));
573
574 assert_eq!(config.addons.addons_path, "addons");
576 assert_eq!(config.addons.priority.p0.len(), 3);
577
578 assert_eq!(config.log.default, "file");
580 assert!(config.log.channels.contains_key("file"));
581
582 assert_eq!(config.server.host, "0.0.0.0");
584 assert_eq!(config.server.port, 8080);
585 }
586 }
587
588 #[test]
590 fn test_load_missing_file_uses_default() {
591 let temp_dir = std::env::temp_dir().join("sz_rust_config_test_missing");
592 let _ = std::fs::create_dir_all(&temp_dir);
593 let config = AppConfig::load_from_dir(&temp_dir).unwrap();
595 assert!(config.app.auto_multi_app);
596 assert_eq!(config.database.default, "mysql");
597 let _ = std::fs::remove_dir_all(&temp_dir);
598 }
599
600 #[test]
602 fn test_env_override_password() {
603 let _env_guard = ENV_TEST_LOCK.lock().unwrap();
605 std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
607
608 let mut config = AppConfig::default();
609 config.database.connections.insert(
610 "mysql".to_string(),
611 DatabaseConnection {
612 r#type: "mysql".to_string(),
613 hostname: "localhost".to_string(),
614 database: "test".to_string(),
615 username: "root".to_string(),
616 password: String::new(),
617 hostport: 3306,
618 charset: "utf8mb4".to_string(),
619 prefix: "sz_".to_string(),
620 deploy: 0,
621 rw_separate: false,
622 fields_strict: true,
623 break_reconnect: true,
624 },
625 );
626
627 std::env::set_var("SZ_DB_MYSQL_PASSWORD", "secret123");
629
630 config.apply_env_overrides();
632
633 assert_eq!(config.database.connections["mysql"].password, "secret123");
635
636 std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
638 }
639
640 #[test]
642 fn test_env_override_empty_ignored() {
643 let _env_guard = ENV_TEST_LOCK.lock().unwrap();
645 std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
647
648 let mut config = AppConfig::default();
649 config.database.connections.insert(
650 "mysql".to_string(),
651 DatabaseConnection {
652 r#type: "mysql".to_string(),
653 hostname: "localhost".to_string(),
654 database: "test".to_string(),
655 username: "root".to_string(),
656 password: "existing".to_string(),
657 hostport: 3306,
658 charset: "utf8mb4".to_string(),
659 prefix: "sz_".to_string(),
660 deploy: 0,
661 rw_separate: false,
662 fields_strict: true,
663 break_reconnect: true,
664 },
665 );
666
667 std::env::set_var("SZ_DB_MYSQL_PASSWORD", "");
669
670 config.apply_env_overrides();
671
672 assert_eq!(config.database.connections["mysql"].password, "existing");
674
675 std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
676 }
677
678 #[test]
680 fn test_default_connection() {
681 let mut config = AppConfig::default();
682 config.database.default = "mysql".to_string();
683 config.database.connections.insert(
684 "mysql".to_string(),
685 DatabaseConnection {
686 r#type: "mysql".to_string(),
687 hostname: "localhost".to_string(),
688 database: "test".to_string(),
689 username: "root".to_string(),
690 password: String::new(),
691 hostport: 3306,
692 charset: "utf8mb4".to_string(),
693 prefix: "sz_".to_string(),
694 deploy: 0,
695 rw_separate: false,
696 fields_strict: true,
697 break_reconnect: true,
698 },
699 );
700
701 let conn = config.default_connection();
702 assert!(conn.is_some());
703 assert_eq!(conn.unwrap().hostname, "localhost");
704 }
705
706 #[test]
708 fn test_default_connection_missing() {
709 let config = AppConfig::default();
710 assert!(config.default_connection().is_none());
711 }
712
713 #[test]
718 fn test_env_override_hostname() {
719 let _env_guard = ENV_TEST_LOCK.lock().unwrap();
720 std::env::remove_var("SZ_DB_MYSQL_HOSTNAME");
721
722 let mut config = AppConfig::default();
723 config.database.connections.insert(
724 "mysql".to_string(),
725 DatabaseConnection {
726 r#type: "mysql".to_string(),
727 hostname: "localhost".to_string(),
728 database: "test".to_string(),
729 username: "root".to_string(),
730 password: String::new(),
731 hostport: 3306,
732 charset: "utf8mb4".to_string(),
733 prefix: "sz_".to_string(),
734 deploy: 0,
735 rw_separate: false,
736 fields_strict: true,
737 break_reconnect: true,
738 },
739 );
740
741 std::env::set_var("SZ_DB_MYSQL_HOSTNAME", "10.0.0.5");
742 config.apply_env_overrides();
743
744 assert_eq!(config.database.connections["mysql"].hostname, "10.0.0.5");
745
746 std::env::remove_var("SZ_DB_MYSQL_HOSTNAME");
747 }
748
749 #[test]
751 fn test_env_override_hostport() {
752 let _env_guard = ENV_TEST_LOCK.lock().unwrap();
753 std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
754
755 let mut config = AppConfig::default();
756 config.database.connections.insert(
757 "mysql".to_string(),
758 DatabaseConnection {
759 r#type: "mysql".to_string(),
760 hostname: "localhost".to_string(),
761 database: "test".to_string(),
762 username: "root".to_string(),
763 password: String::new(),
764 hostport: 3306,
765 charset: "utf8mb4".to_string(),
766 prefix: "sz_".to_string(),
767 deploy: 0,
768 rw_separate: false,
769 fields_strict: true,
770 break_reconnect: true,
771 },
772 );
773
774 std::env::set_var("SZ_DB_MYSQL_HOSTPORT", "8802");
775 config.apply_env_overrides();
776
777 assert_eq!(config.database.connections["mysql"].hostport, 8802);
778
779 std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
780 }
781
782 #[test]
784 fn test_env_override_hostport_invalid_ignored() {
785 let _env_guard = ENV_TEST_LOCK.lock().unwrap();
786 std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
787
788 let mut config = AppConfig::default();
789 config.database.connections.insert(
790 "mysql".to_string(),
791 DatabaseConnection {
792 r#type: "mysql".to_string(),
793 hostname: "localhost".to_string(),
794 database: "test".to_string(),
795 username: "root".to_string(),
796 password: String::new(),
797 hostport: 3306,
798 charset: "utf8mb4".to_string(),
799 prefix: "sz_".to_string(),
800 deploy: 0,
801 rw_separate: false,
802 fields_strict: true,
803 break_reconnect: true,
804 },
805 );
806
807 std::env::set_var("SZ_DB_MYSQL_HOSTPORT", "not-a-number");
808 config.apply_env_overrides();
809
810 assert_eq!(config.database.connections["mysql"].hostport, 3306);
812
813 std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
814 }
815
816 #[test]
818 fn test_parse_error() {
819 let bad_yaml = "default: mysql\n bad: : : indent";
820 let result: Result<DatabaseSection, _> = serde_yml::from_str(bad_yaml);
821 let _ = result;
824 }
825}