1use crate::mode::AppMode;
9use serde::Deserialize;
10use std::path::Path;
11
12pub trait IAppOptions: for<'de> Deserialize<'de> + Default + Send + Sync + 'static {}
21
22impl<T> IAppOptions for T where T: for<'de> Deserialize<'de> + Default + Send + Sync + 'static {}
23
24#[derive(Debug, Clone, Deserialize)]
30pub struct AppSection {
31 #[serde(default, rename = "Name")]
33 pub name: String,
34 #[serde(default = "default_urls", rename = "Urls")]
37 pub urls: Vec<String>,
38 #[serde(default = "default_max_body_size", rename = "MaxBodySize")]
40 pub max_body_size: usize,
41 #[serde(default = "default_max_connections", rename = "MaxConnections")]
43 pub max_connections: usize,
44}
45
46impl Default for AppSection {
47 fn default() -> Self {
48 Self {
49 name: String::new(),
50 urls: default_urls(),
51 max_body_size: default_max_body_size(),
52 max_connections: default_max_connections(),
53 }
54 }
55}
56
57fn default_urls() -> Vec<String> {
58 vec!["http://0.0.0.0:5000".to_string()]
59}
60
61fn default_max_body_size() -> usize {
62 10 * 1024 * 1024 }
64
65fn default_max_connections() -> usize {
66 10_000
67}
68
69#[derive(Debug, Clone, Deserialize, Default)]
71pub struct JwtSection {
72 #[serde(default, rename = "Secret")]
74 pub secret: String,
75}
76
77#[derive(Debug, Clone, Deserialize)]
79pub struct CorsSection {
80 #[serde(default = "default_origins")]
82 pub origins: Vec<String>,
83 #[serde(default = "default_cors_methods")]
85 pub methods: Vec<String>,
86 #[serde(default = "default_cors_headers")]
88 pub headers: Vec<String>,
89 #[serde(default)]
91 pub expose_headers: Vec<String>,
92 #[serde(default)]
94 pub allow_credentials: bool,
95 #[serde(default = "default_max_age")]
97 pub max_age: u32,
98}
99
100impl Default for CorsSection {
101 fn default() -> Self {
102 Self {
103 origins: default_origins(),
104 methods: default_cors_methods(),
105 headers: default_cors_headers(),
106 expose_headers: Vec::new(),
107 allow_credentials: false,
108 max_age: default_max_age(),
109 }
110 }
111}
112
113fn default_origins() -> Vec<String> {
114 vec!["*".to_string()]
115}
116
117fn default_cors_methods() -> Vec<String> {
118 vec![
119 "GET".to_string(),
120 "POST".to_string(),
121 "PUT".to_string(),
122 "DELETE".to_string(),
123 "PATCH".to_string(),
124 "OPTIONS".to_string(),
125 ]
126}
127
128fn default_cors_headers() -> Vec<String> {
129 vec!["Content-Type".to_string(), "Authorization".to_string()]
130}
131
132fn default_max_age() -> u32 {
133 86400
134}
135
136#[derive(Debug, Clone, Deserialize)]
138pub struct RateLimitSection {
139 #[serde(default, rename = "Enabled")]
141 pub enabled: bool,
142 #[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
144 pub requests_per_second: f64,
145 #[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
147 pub burst_size: u32,
148 #[serde(default = "default_rate_limit_max_ips", rename = "MaxTrackedIps")]
150 pub max_tracked_ips: usize,
151 #[serde(default, rename = "TrustProxy")]
154 pub trust_proxy: bool,
155}
156
157impl Default for RateLimitSection {
158 fn default() -> Self {
159 Self {
160 enabled: false,
161 requests_per_second: default_rate_limit_rps(),
162 burst_size: default_rate_limit_burst(),
163 max_tracked_ips: default_rate_limit_max_ips(),
164 trust_proxy: false,
165 }
166 }
167}
168
169fn default_rate_limit_rps() -> f64 {
170 100.0
171}
172
173fn default_rate_limit_burst() -> u32 {
174 200
175}
176
177fn default_rate_limit_max_ips() -> usize {
178 10_000
179}
180
181#[derive(Debug, Clone, Deserialize, Default)]
183pub struct MetricsSection {
184 #[serde(default, rename = "Enabled")]
185 pub enabled: bool,
186}
187
188#[derive(Debug, Clone, Deserialize, Default)]
194pub struct TlsSection {
195 #[serde(default, rename = "CertPath")]
197 pub cert_path: String,
198 #[serde(default, rename = "KeyPath")]
200 pub key_path: String,
201}
202
203#[derive(Debug, Clone, Deserialize, Default)]
208pub struct AppOptions {
209 #[serde(default, rename = "App")]
211 pub app: AppSection,
212 #[serde(default, rename = "Jwt")]
214 pub jwt: JwtSection,
215 #[serde(default, rename = "Cors")]
217 pub cors: CorsSection,
218 #[serde(default, rename = "Tls")]
220 pub tls: TlsSection,
221 #[serde(default, rename = "RateLimit")]
223 pub rate_limit: RateLimitSection,
224 #[serde(default, rename = "Metrics")]
226 pub metrics: MetricsSection,
227}
228
229pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
242 let mut base = read_json_file("appsettings.json")?;
243
244 let overlay_name = mode.overlay_filename();
245 if let Some(overlay) = read_json_file(overlay_name) {
246 merge_json(&mut base, overlay);
247 }
248
249 apply_well_known_env_overrides(&mut base);
251
252 apply_env_overrides(&mut base);
254
255 Some(base)
256}
257
258fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
262 if let Ok(secret) = std::env::var("JWT_SECRET") {
263 if !secret.is_empty() {
264 set_json_value(config, &["Jwt", "Secret"], &secret);
265 }
266 }
267}
268
269fn apply_env_overrides(config: &mut serde_json::Value) {
271 for (key, value) in std::env::vars() {
272 if let Some(path) = key.strip_prefix("APP__") {
273 let segments: Vec<&str> = path.split("__").collect();
275 if segments.is_empty() {
276 continue;
277 }
278 set_json_value(config, &segments, &value);
279 }
280 }
281}
282
283fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
285 if segments.is_empty() {
286 return;
287 }
288
289 let key = segments[0];
290
291 if let serde_json::Value::Object(map) = obj {
292 if segments.len() == 1 {
293 let parsed =
295 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
296 map.insert(key.to_string(), parsed);
297 } else if let Some(child) = map.get_mut(key) {
298 set_json_value(child, &segments[1..], value);
300 } else {
301 let mut child = serde_json::json!({});
303 set_json_value(&mut child, &segments[1..], value);
304 map.insert(key.to_string(), child);
305 }
306 }
307}
308
309pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
311 config: &serde_json::Value,
312 section: &str,
313) -> T {
314 if section.is_empty() || section == "." {
315 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
316 tracing::warn!("[Config] Failed to bind root config to {}: {}, using defaults", std::any::type_name::<T>(), e);
317 T::default()
318 })
319 } else {
320 config
321 .get(section)
322 .map(|v| serde_json::from_value(v.clone()).unwrap_or_else(|e| {
323 tracing::warn!("[Config] Failed to bind section '{}' to {}: {}, using defaults", section, std::any::type_name::<T>(), e);
324 T::default()
325 }))
326 .unwrap_or_default()
327 }
328}
329
330pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
332 serde_json::from_value(config.clone()).unwrap_or_else(|e| {
333 tracing::warn!("[Config] Failed to bind root config to {}: {}, using defaults", std::any::type_name::<T>(), e);
334 T::default()
335 })
336}
337
338fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
343 let path = path.as_ref();
344
345 fn try_read(path: &Path) -> Option<serde_json::Value> {
347 let content = std::fs::read_to_string(path).ok()?;
348 serde_json::from_str(&content).ok()
349 }
350
351 let base = crate::paths::app_base();
354 let candidate = base.join(path);
355 if let Some(value) = try_read(&candidate) {
356 return Some(value);
357 }
358
359 if let Some(value) = try_read(path) {
361 return Some(value);
362 }
363
364 None
365}
366
367fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
368 match (base, overlay) {
369 (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
370 for (k, v) in b {
371 merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
372 }
373 }
374 (a, b) => *a = b,
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn config_env_jwt_and_app_override_precedence() {
384 std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
385 let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
386 apply_well_known_env_overrides(&mut config);
387 assert_eq!(
388 config["Jwt"]["Secret"],
389 "env-jwt-secret-that-is-long-enough-32"
390 );
391
392 std::env::set_var(
393 "APP__Jwt__Secret",
394 "from-app-override-secret-32chars-minimum",
395 );
396 apply_env_overrides(&mut config);
397 std::env::remove_var("JWT_SECRET");
398 std::env::remove_var("APP__Jwt__Secret");
399 assert_eq!(
400 config["Jwt"]["Secret"],
401 "from-app-override-secret-32chars-minimum"
402 );
403 }
404}