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}
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 // 10 MB
59}
60
61/// JWT authentication section.
62#[derive(Debug, Clone, Deserialize, Default)]
63pub struct JwtSection {
64    /// HMAC secret for signing/verifying JWT tokens.
65    #[serde(default, rename = "Secret")]
66    pub secret: String,
67}
68
69/// CORS (Cross-Origin Resource Sharing) section.
70#[derive(Debug, Clone, Deserialize)]
71pub struct CorsSection {
72    /// Allowed origins. Default: ["*"].
73    #[serde(default = "default_origins")]
74    pub origins: Vec<String>,
75    /// Allowed methods. Default: GET, POST, PUT, DELETE, PATCH, OPTIONS.
76    #[serde(default = "default_cors_methods")]
77    pub methods: Vec<String>,
78    /// Allowed headers. Default: Content-Type, Authorization.
79    #[serde(default = "default_cors_headers")]
80    pub headers: Vec<String>,
81    /// Allow credentials. Default: false.
82    #[serde(default)]
83    pub allow_credentials: bool,
84    /// Preflight cache max-age in seconds. Default: 86400.
85    #[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/// Per-IP rate limiting (token bucket).
125#[derive(Debug, Clone, Deserialize)]
126pub struct RateLimitSection {
127    /// Enable rate-limit middleware. Default: false (enable in Production appsettings).
128    #[serde(default, rename = "Enabled")]
129    pub enabled: bool,
130    /// Sustained requests per second per client IP.
131    #[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
132    pub requests_per_second: f64,
133    /// Burst capacity before throttling.
134    #[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
135    pub burst_size: u32,
136    /// Maximum distinct client IPs tracked (LRU eviction when exceeded).
137    #[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/// HTTP request metrics (`GET /metrics` Prometheus text when enabled).
165#[derive(Debug, Clone, Deserialize, Default)]
166pub struct MetricsSection {
167    #[serde(default, rename = "Enabled")]
168    pub enabled: bool,
169}
170
171/// TLS (Transport Layer Security) section.
172///
173/// TLS is activated automatically when the `App.Urls` array
174/// contains one or more `https://` entries. The certificate
175/// and key paths are read from this section.
176#[derive(Debug, Clone, Deserialize, Default)]
177pub struct TlsSection {
178    /// Path to TLS certificate PEM file.
179    #[serde(default, rename = "CertPath")]
180    pub cert_path: String,
181    /// Path to TLS private key PEM file.
182    #[serde(default, rename = "KeyPath")]
183    pub key_path: String,
184}
185
186/// Standard application options loaded from appsettings.json.
187///
188/// Bound automatically by the framework.  Access via `host.options()`
189/// or customize via `app.useOptions(|o| { ... })`.
190#[derive(Debug, Clone, Deserialize, Default)]
191pub struct AppOptions {
192    /// Application settings.
193    #[serde(default, rename = "App")]
194    pub app: AppSection,
195    /// JWT authentication settings.
196    #[serde(default, rename = "Jwt")]
197    pub jwt: JwtSection,
198    /// CORS settings.
199    #[serde(default, rename = "Cors")]
200    pub cors: CorsSection,
201    /// TLS settings.
202    #[serde(default, rename = "Tls")]
203    pub tls: TlsSection,
204    /// Rate limiting settings.
205    #[serde(default, rename = "RateLimit")]
206    pub rate_limit: RateLimitSection,
207    /// Prometheus-style metrics endpoint settings.
208    #[serde(default, rename = "Metrics")]
209    pub metrics: MetricsSection,
210}
211
212// ---------------------------------------------------------------------------
213// Config loading helpers
214// ---------------------------------------------------------------------------
215
216/// Load the merged appsettings JSON (base + environment overlay + env overrides).
217///
218/// 按运行模式自动合并对应的 `appsettings.{Mode}.json` overlay:
219/// - `Development` → `appsettings.Development.json`
220/// - `Production`  → `appsettings.Production.json`
221///
222/// Environment variables prefixed with `APP__` override the corresponding JSON values.
223/// For example, `APP__App__Address=0.0.0.0:8080` overrides `{"App": {"Address": "..."}}`.
224pub 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    // Well-known env vars (JWT_SECRET) — applied before APP__ so APP__Jwt__Secret wins.
233    apply_well_known_env_overrides(&mut base);
234
235    // Apply environment variable overrides (APP__Section__Key pattern)
236    apply_env_overrides(&mut base);
237
238    Some(base)
239}
240
241/// Apply conventional environment variables that map to config sections.
242///
243/// `JWT_SECRET` → `Jwt.Secret` (overridden by `APP__Jwt__Secret` if set).
244fn 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
252/// Apply environment variable overrides following the `APP__Section__Key` pattern.
253fn 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            // Split by double underscore to get path segments
257            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
266/// Set a value in a JSON object following the given path segments.
267fn 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            // Leaf: set the value, attempting to parse as JSON first
277            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            // Recurse into child
282            set_json_value(child, &segments[1..], value);
283        } else {
284            // Create intermediate objects as needed
285            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
292/// Bind a section of the config JSON to a deserializable type.
293pub 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
307/// Bind the entire config JSON to a type (for root-level deserialization).
308pub 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
312// ---------------------------------------------------------------------------
313// Internal helpers
314// ---------------------------------------------------------------------------
315
316fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
317    let path = path.as_ref();
318
319    /// Try to read and parse a JSON file at the given path.
320    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    // 1. 应用基准目录(exe 同级 / cwd / 上溯统一由 app_base 处理)。
326    //    部署与开发期均能定位到 appsettings.json 所在目录。
327    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    // 2. Try as-is (relative to current working directory) — 兜底
334    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}