Skip to main content

webx_core/
mode.rs

1//! Application mode: Development vs Production.
2//!
3//! Controls framework behavior such as startup banner verbosity
4//! and built-in request logging.
5//!
6//! 环境识别遵循 ASP.NET Core 约定,但**默认值偏向安全**:
7//! 1. 环境变量 `APP_ENV`(不区分大小写)显式指定,取值 `Production`/`Prod` 或
8//!    `Development`/`Dev`;
9//! 2. 未设置或值非法时默认 `Production`,并输出**一次**警告 —— 安全开关不能
10//!    取决于“环境变量忘了写”;本机开发必须显式 `APP_ENV=Development`。
11//!
12//! 框架据此自动加载 `appsettings.{Environment}.json` overlay。
13
14use std::sync::Once;
15
16/// Controls how the framework behaves at startup and at runtime.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum AppMode {
19    /// Full startup banner, per-request console logging, verbose diagnostics.
20    ///
21    /// Opt-in only (`APP_ENV=Development`): local development must say so
22    /// explicitly.
23    Development,
24    /// Minimal one-line startup message, no framework-initiated console logging.
25    ///
26    /// The default: a forgotten `APP_ENV` keeps the production guards on instead
27    /// of silently weakening them.
28    #[default]
29    Production,
30}
31
32impl AppMode {
33    /// 环境模式对应的 overlay 文件名,如 `appsettings.Development.json`。
34    pub fn overlay_filename(&self) -> &'static str {
35        match self {
36            AppMode::Development => "appsettings.Development.json",
37            AppMode::Production => "appsettings.Production.json",
38        }
39    }
40
41    /// 从环境变量 `APP_ENV` 解析运行模式;未设置或值非法时返回 `Production`。
42    ///
43    /// 接受(不区分大小写):`Production` / `Prod` / `Development` / `Dev`。
44    /// 未设置或值非法时输出一次 `tracing::warn!`:安全默认值必须显式关掉,
45    /// 本机开发设置 `APP_ENV=Development` 即可。
46    pub fn from_env() -> Self {
47        Self::from_env_var("APP_ENV")
48    }
49
50    /// 从指定环境变量名解析运行模式。
51    pub fn from_env_var(var: &str) -> Self {
52        match std::env::var(var) {
53            Ok(raw) => match Self::parse(&raw) {
54                Some(mode) => mode,
55                None => {
56                    warn_invalid_once(var, &raw);
57                    AppMode::Production
58                }
59            },
60            Err(_) => {
61                warn_unset_once(var);
62                AppMode::Production
63            }
64        }
65    }
66
67    /// 解析环境变量取值;`Production`/`Prod`、`Development`/`Dev`(不区分大小写,
68    /// 允许首尾空白),其它取值返回 `None`。
69    fn parse(raw: &str) -> Option<AppMode> {
70        match raw.trim().to_lowercase().as_str() {
71            "production" | "prod" => Some(AppMode::Production),
72            "development" | "dev" => Some(AppMode::Development),
73            _ => None,
74        }
75    }
76}
77
78/// 未设置环境变量时的警告:每次进程只提示一次,避免每次 `from_env()` 刷屏。
79fn warn_unset_once(var: &str) {
80    static WARNED: Once = Once::new();
81    WARNED.call_once(|| {
82        tracing::warn!(
83            "{var} is not set; defaulting to Production. \
84             Set APP_ENV=Development for local development."
85        );
86    });
87}
88
89/// 环境变量取值非法时的警告:每次进程只提示一次(合法值不受影响)。
90fn warn_invalid_once(var: &str, raw: &str) {
91    static WARNED: Once = Once::new();
92    WARNED.call_once(|| {
93        tracing::warn!(
94            "Invalid {var}={raw:?}; expected Production/Prod or Development/Dev. \
95             Falling back to Production."
96        );
97    });
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn default_mode_is_production() {
106        // Safe default: a missing APP_ENV keeps the production guards armed.
107        assert_eq!(AppMode::default(), AppMode::Production);
108    }
109
110    #[test]
111    fn parse_accepts_known_values_case_insensitively() {
112        assert_eq!(AppMode::parse("Production"), Some(AppMode::Production));
113        assert_eq!(AppMode::parse("prod"), Some(AppMode::Production));
114        assert_eq!(AppMode::parse("  PROD  "), Some(AppMode::Production));
115        assert_eq!(AppMode::parse("Development"), Some(AppMode::Development));
116        assert_eq!(AppMode::parse("DEV"), Some(AppMode::Development));
117    }
118
119    #[test]
120    fn parse_rejects_unknown_values() {
121        assert_eq!(AppMode::parse("staging"), None);
122        assert_eq!(AppMode::parse(""), None);
123        assert_eq!(AppMode::parse("1"), None);
124    }
125
126    #[test]
127    fn from_env_var_unset_falls_back_to_production() {
128        // A variable name that is never set: no `set_var`, so no cross-test
129        // environment pollution (the warning is emitted once per process).
130        let mode = AppMode::from_env_var("WEBX_TEST_APP_ENV_THAT_IS_NEVER_SET");
131        assert_eq!(mode, AppMode::Production);
132    }
133
134    #[test]
135    fn from_env_var_invalid_falls_back_to_production() {
136        std::env::set_var("WEBX_TEST_APP_ENV_INVALID", "staging");
137        let mode = AppMode::from_env_var("WEBX_TEST_APP_ENV_INVALID");
138        std::env::remove_var("WEBX_TEST_APP_ENV_INVALID");
139        assert_eq!(mode, AppMode::Production);
140    }
141
142    #[test]
143    fn from_env_var_valid_value_is_honoured() {
144        std::env::set_var("WEBX_TEST_APP_ENV_PRODUCTION", "prod");
145        let mode = AppMode::from_env_var("WEBX_TEST_APP_ENV_PRODUCTION");
146        std::env::remove_var("WEBX_TEST_APP_ENV_PRODUCTION");
147        assert_eq!(mode, AppMode::Production);
148    }
149
150    #[test]
151    fn from_env_var_explicit_development_opts_out() {
152        std::env::set_var("WEBX_TEST_APP_ENV_DEVELOPMENT", "dev");
153        let mode = AppMode::from_env_var("WEBX_TEST_APP_ENV_DEVELOPMENT");
154        std::env::remove_var("WEBX_TEST_APP_ENV_DEVELOPMENT");
155        assert_eq!(mode, AppMode::Development);
156    }
157}