1use std::sync::Once;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum AppMode {
19 Development,
24 #[default]
29 Production,
30}
31
32impl AppMode {
33 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 pub fn from_env() -> Self {
47 Self::from_env_var("APP_ENV")
48 }
49
50 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 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
78fn 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
89fn 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 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 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}