Skip to main content

rust_webx_core/
config.rs

1//! Built-in configuration support: appsettings.json + AppOptions pattern.
2//!
3//! The framework automatically loads `appsettings.json` (merged with
4//! `appsettings.Development.json` in dev mode) and binds it to the
5//! built-in `AppOptions` struct.  Users customize options via
6//! `HostBuilder::configure(|app| app.useOptions(|o| { ... }))`.
7
8use crate::mode::AppMode;
9use serde::Deserialize;
10use std::path::Path;
11
12// ---------------------------------------------------------------------------
13// IAppOptions trait (for user-defined option types)
14// ---------------------------------------------------------------------------
15
16/// Application options — binds to a section of appsettings.json.
17///
18/// Users define their own structs implementing this trait,
19/// then call `AppConfig::bind()` to bind values.
20pub 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// ---------------------------------------------------------------------------
25// Built-in option types (matching standard appsettings.json layout)
26// ---------------------------------------------------------------------------
27
28/// Top-level application section.
29#[derive(Debug, Clone, Deserialize)]
30pub struct AppSection {
31    /// Application display name.
32    #[serde(default, rename = "Name")]
33    pub name: String,
34    /// Listen addresses as full URLs (e.g., "http://0.0.0.0:5000").
35    /// ASP.NET Core compatible. Default: ["http://0.0.0.0:5000"].
36    #[serde(default = "default_urls", rename = "Urls")]
37    pub urls: Vec<String>,
38    /// Maximum request body size in bytes. Default: 10 MB.
39    #[serde(default = "default_max_body_size", rename = "MaxBodySize")]
40    pub max_body_size: usize,
41    /// Maximum concurrent connections. Default: 10000.
42    #[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 // 10 MB
63}
64
65fn default_max_connections() -> usize {
66    10_000
67}
68
69/// JWT authentication section.
70#[derive(Debug, Clone, Deserialize, Default)]
71pub struct JwtSection {
72    /// HMAC secret for signing/verifying JWT tokens.
73    #[serde(default, rename = "Secret")]
74    pub secret: String,
75}
76
77/// CORS (Cross-Origin Resource Sharing) section.
78#[derive(Debug, Clone, Deserialize)]
79pub struct CorsSection {
80    /// Allowed origins. Default: ["*"].
81    #[serde(default = "default_origins")]
82    pub origins: Vec<String>,
83    /// Allowed methods. Default: GET, POST, PUT, DELETE, PATCH, OPTIONS.
84    #[serde(default = "default_cors_methods")]
85    pub methods: Vec<String>,
86    /// Allowed headers. Default: Content-Type, Authorization.
87    #[serde(default = "default_cors_headers")]
88    pub headers: Vec<String>,
89    /// Response headers exposed to the client. Default: empty.
90    #[serde(default)]
91    pub expose_headers: Vec<String>,
92    /// Allow credentials. Default: false.
93    #[serde(default)]
94    pub allow_credentials: bool,
95    /// Preflight cache max-age in seconds. Default: 86400.
96    #[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/// Per-IP rate limiting (token bucket).
137#[derive(Debug, Clone, Deserialize)]
138pub struct RateLimitSection {
139    /// Enable rate-limit middleware. Default: false (enable in Production appsettings).
140    #[serde(default, rename = "Enabled")]
141    pub enabled: bool,
142    /// Sustained requests per second per client IP.
143    #[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
144    pub requests_per_second: f64,
145    /// Burst capacity before throttling.
146    #[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
147    pub burst_size: u32,
148    /// Maximum distinct client IPs tracked (LRU eviction when exceeded).
149    #[serde(default = "default_rate_limit_max_ips", rename = "MaxTrackedIps")]
150    pub max_tracked_ips: usize,
151    /// Trust X-Forwarded-For / X-Real-IP headers for client IP extraction.
152    /// Only enable when behind a trusted reverse proxy. Default: false.
153    #[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/// HTTP request metrics (`GET /metrics` Prometheus text when enabled).
182#[derive(Debug, Clone, Deserialize, Default)]
183pub struct MetricsSection {
184    #[serde(default, rename = "Enabled")]
185    pub enabled: bool,
186}
187
188/// TLS (Transport Layer Security) section.
189///
190/// TLS is activated automatically when the `App.Urls` array
191/// contains one or more `https://` entries. The certificate
192/// and key paths are read from this section.
193#[derive(Debug, Clone, Deserialize, Default)]
194pub struct TlsSection {
195    /// Path to TLS certificate PEM file.
196    #[serde(default, rename = "CertPath")]
197    pub cert_path: String,
198    /// Path to TLS private key PEM file.
199    #[serde(default, rename = "KeyPath")]
200    pub key_path: String,
201}
202
203/// Standard application options loaded from appsettings.json.
204///
205/// Bound automatically by the framework.  Access via `host.options()`
206/// or customize via `app.useOptions(|o| { ... })`.
207#[derive(Debug, Clone, Deserialize, Default)]
208pub struct AppOptions {
209    /// Application settings.
210    #[serde(default, rename = "App")]
211    pub app: AppSection,
212    /// JWT authentication settings.
213    #[serde(default, rename = "Jwt")]
214    pub jwt: JwtSection,
215    /// CORS settings.
216    #[serde(default, rename = "Cors")]
217    pub cors: CorsSection,
218    /// TLS settings.
219    #[serde(default, rename = "Tls")]
220    pub tls: TlsSection,
221    /// Rate limiting settings.
222    #[serde(default, rename = "RateLimit")]
223    pub rate_limit: RateLimitSection,
224    /// Prometheus-style metrics endpoint settings.
225    #[serde(default, rename = "Metrics")]
226    pub metrics: MetricsSection,
227}
228
229// ---------------------------------------------------------------------------
230// Config loading helpers
231// ---------------------------------------------------------------------------
232
233/// Load the merged appsettings JSON (base + environment overlay + env overrides).
234///
235/// 按运行模式自动合并对应的 `appsettings.{Mode}.json` overlay:
236/// - `Development` → `appsettings.Development.json`
237/// - `Production`  → `appsettings.Production.json`
238///
239/// Environment variables prefixed with `APP__` override the corresponding JSON values.
240/// For example, `APP__App__Address=0.0.0.0:8080` overrides `{"App": {"Address": "..."}}`.
241pub 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    // Well-known env vars (JWT_SECRET) — applied before APP__ so APP__Jwt__Secret wins.
250    apply_well_known_env_overrides(&mut base);
251
252    // Apply environment variable overrides (APP__Section__Key pattern)
253    apply_env_overrides(&mut base);
254
255    Some(base)
256}
257
258/// Apply conventional environment variables that map to config sections.
259///
260/// `JWT_SECRET` → `Jwt.Secret` (overridden by `APP__Jwt__Secret` if set).
261fn 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
269/// Apply environment variable overrides following the `APP__Section__Key` pattern.
270fn 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            // Split by double underscore to get path segments
274            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
283/// Set a value in a JSON object following the given path segments.
284fn 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            // Leaf: set the value, attempting to parse as JSON first
294            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            // Recurse into child
299            set_json_value(child, &segments[1..], value);
300        } else {
301            // Create intermediate objects as needed
302            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
309/// Bind a section of the config JSON to a deserializable type.
310pub 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
330/// Bind the entire config JSON to a type (for root-level deserialization).
331pub 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
338// ---------------------------------------------------------------------------
339// Internal helpers
340// ---------------------------------------------------------------------------
341
342fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
343    let path = path.as_ref();
344
345    /// Try to read and parse a JSON file at the given path.
346    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    // 1. 应用基准目录(exe 同级 / cwd / 上溯统一由 app_base 处理)。
352    //    部署与开发期均能定位到 appsettings.json 所在目录。
353    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    // 2. Try as-is (relative to current working directory) — 兜底
360    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}