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}
42
43impl Default for AppSection {
44 fn default() -> Self {
45 Self {
46 name: String::new(),
47 urls: default_urls(),
48 max_body_size: default_max_body_size(),
49 }
50 }
51}
52
53fn default_urls() -> Vec<String> {
54 vec!["http://0.0.0.0:5000".to_string()]
55}
56
57fn default_max_body_size() -> usize {
58 10 * 1024 * 1024 }
60
61#[derive(Debug, Clone, Deserialize, Default)]
63pub struct JwtSection {
64 #[serde(default, rename = "Secret")]
66 pub secret: String,
67}
68
69#[derive(Debug, Clone, Deserialize)]
71pub struct CorsSection {
72 #[serde(default = "default_origins")]
74 pub origins: Vec<String>,
75 #[serde(default = "default_cors_methods")]
77 pub methods: Vec<String>,
78 #[serde(default = "default_cors_headers")]
80 pub headers: Vec<String>,
81 #[serde(default)]
83 pub allow_credentials: bool,
84 #[serde(default = "default_max_age")]
86 pub max_age: u32,
87}
88
89impl Default for CorsSection {
90 fn default() -> Self {
91 Self {
92 origins: default_origins(),
93 methods: default_cors_methods(),
94 headers: default_cors_headers(),
95 allow_credentials: false,
96 max_age: default_max_age(),
97 }
98 }
99}
100
101fn default_origins() -> Vec<String> {
102 vec!["*".to_string()]
103}
104
105fn default_cors_methods() -> Vec<String> {
106 vec![
107 "GET".to_string(),
108 "POST".to_string(),
109 "PUT".to_string(),
110 "DELETE".to_string(),
111 "PATCH".to_string(),
112 "OPTIONS".to_string(),
113 ]
114}
115
116fn default_cors_headers() -> Vec<String> {
117 vec!["Content-Type".to_string(), "Authorization".to_string()]
118}
119
120fn default_max_age() -> u32 {
121 86400
122}
123
124#[derive(Debug, Clone, Deserialize)]
126pub struct RateLimitSection {
127 #[serde(default, rename = "Enabled")]
129 pub enabled: bool,
130 #[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
132 pub requests_per_second: f64,
133 #[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
135 pub burst_size: u32,
136 #[serde(default = "default_rate_limit_max_ips", rename = "MaxTrackedIps")]
138 pub max_tracked_ips: usize,
139}
140
141impl Default for RateLimitSection {
142 fn default() -> Self {
143 Self {
144 enabled: false,
145 requests_per_second: default_rate_limit_rps(),
146 burst_size: default_rate_limit_burst(),
147 max_tracked_ips: default_rate_limit_max_ips(),
148 }
149 }
150}
151
152fn default_rate_limit_rps() -> f64 {
153 100.0
154}
155
156fn default_rate_limit_burst() -> u32 {
157 200
158}
159
160fn default_rate_limit_max_ips() -> usize {
161 10_000
162}
163
164#[derive(Debug, Clone, Deserialize, Default)]
166pub struct MetricsSection {
167 #[serde(default, rename = "Enabled")]
168 pub enabled: bool,
169}
170
171#[derive(Debug, Clone, Deserialize, Default)]
177pub struct TlsSection {
178 #[serde(default, rename = "CertPath")]
180 pub cert_path: String,
181 #[serde(default, rename = "KeyPath")]
183 pub key_path: String,
184}
185
186#[derive(Debug, Clone, Deserialize, Default)]
191pub struct AppOptions {
192 #[serde(default, rename = "App")]
194 pub app: AppSection,
195 #[serde(default, rename = "Jwt")]
197 pub jwt: JwtSection,
198 #[serde(default, rename = "Cors")]
200 pub cors: CorsSection,
201 #[serde(default, rename = "Tls")]
203 pub tls: TlsSection,
204 #[serde(default, rename = "RateLimit")]
206 pub rate_limit: RateLimitSection,
207 #[serde(default, rename = "Metrics")]
209 pub metrics: MetricsSection,
210}
211
212pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
225 let mut base = read_json_file("appsettings.json")?;
226
227 let overlay_name = mode.overlay_filename();
228 if let Some(overlay) = read_json_file(overlay_name) {
229 merge_json(&mut base, overlay);
230 }
231
232 apply_well_known_env_overrides(&mut base);
234
235 apply_env_overrides(&mut base);
237
238 Some(base)
239}
240
241fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
245 if let Ok(secret) = std::env::var("JWT_SECRET") {
246 if !secret.is_empty() {
247 set_json_value(config, &["Jwt", "Secret"], &secret);
248 }
249 }
250}
251
252fn apply_env_overrides(config: &mut serde_json::Value) {
254 for (key, value) in std::env::vars() {
255 if let Some(path) = key.strip_prefix("APP__") {
256 let segments: Vec<&str> = path.split("__").collect();
258 if segments.is_empty() {
259 continue;
260 }
261 set_json_value(config, &segments, &value);
262 }
263 }
264}
265
266fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
268 if segments.is_empty() {
269 return;
270 }
271
272 let key = segments[0];
273
274 if let serde_json::Value::Object(map) = obj {
275 if segments.len() == 1 {
276 let parsed =
278 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
279 map.insert(key.to_string(), parsed);
280 } else if let Some(child) = map.get_mut(key) {
281 set_json_value(child, &segments[1..], value);
283 } else {
284 let mut child = serde_json::json!({});
286 set_json_value(&mut child, &segments[1..], value);
287 map.insert(key.to_string(), child);
288 }
289 }
290}
291
292pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
294 config: &serde_json::Value,
295 section: &str,
296) -> T {
297 if section.is_empty() || section == "." {
298 serde_json::from_value(config.clone()).unwrap_or_default()
299 } else {
300 config
301 .get(section)
302 .map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
303 .unwrap_or_default()
304 }
305}
306
307pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
309 serde_json::from_value(config.clone()).unwrap_or_default()
310}
311
312fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
317 let path = path.as_ref();
318
319 fn try_read(path: &Path) -> Option<serde_json::Value> {
321 let content = std::fs::read_to_string(path).ok()?;
322 serde_json::from_str(&content).ok()
323 }
324
325 let base = crate::paths::app_base();
328 let candidate = base.join(path);
329 if let Some(value) = try_read(&candidate) {
330 return Some(value);
331 }
332
333 if let Some(value) = try_read(path) {
335 return Some(value);
336 }
337
338 None
339}
340
341fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
342 match (base, overlay) {
343 (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
344 for (k, v) in b {
345 merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
346 }
347 }
348 (a, b) => *a = b,
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn config_env_jwt_and_app_override_precedence() {
358 std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
359 let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
360 apply_well_known_env_overrides(&mut config);
361 assert_eq!(
362 config["Jwt"]["Secret"],
363 "env-jwt-secret-that-is-long-enough-32"
364 );
365
366 std::env::set_var(
367 "APP__Jwt__Secret",
368 "from-app-override-secret-32chars-minimum",
369 );
370 apply_env_overrides(&mut config);
371 std::env::remove_var("JWT_SECRET");
372 std::env::remove_var("APP__Jwt__Secret");
373 assert_eq!(
374 config["Jwt"]["Secret"],
375 "from-app-override-secret-32chars-minimum"
376 );
377 }
378}