use crate::mode::AppMode;
use serde::Deserialize;
use std::path::Path;
pub trait IAppOptions: for<'de> Deserialize<'de> + Default + Send + Sync + 'static {}
impl<T> IAppOptions for T where T: for<'de> Deserialize<'de> + Default + Send + Sync + 'static {}
#[derive(Debug, Clone, Deserialize)]
pub struct AppSection {
#[serde(default, rename = "Name")]
pub name: String,
#[serde(default = "default_urls", rename = "Urls")]
pub urls: Vec<String>,
#[serde(default = "default_max_body_size", rename = "MaxBodySize")]
pub max_body_size: usize,
}
impl Default for AppSection {
fn default() -> Self {
Self {
name: String::new(),
urls: default_urls(),
max_body_size: default_max_body_size(),
}
}
}
fn default_urls() -> Vec<String> {
vec!["http://0.0.0.0:5000".to_string()]
}
fn default_max_body_size() -> usize {
10 * 1024 * 1024 }
#[derive(Debug, Clone, Deserialize, Default)]
pub struct JwtSection {
#[serde(default, rename = "Secret")]
pub secret: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CorsSection {
#[serde(default = "default_origins")]
pub origins: Vec<String>,
#[serde(default = "default_cors_methods")]
pub methods: Vec<String>,
#[serde(default = "default_cors_headers")]
pub headers: Vec<String>,
#[serde(default)]
pub allow_credentials: bool,
#[serde(default = "default_max_age")]
pub max_age: u32,
}
impl Default for CorsSection {
fn default() -> Self {
Self {
origins: default_origins(),
methods: default_cors_methods(),
headers: default_cors_headers(),
allow_credentials: false,
max_age: default_max_age(),
}
}
}
fn default_origins() -> Vec<String> {
vec!["*".to_string()]
}
fn default_cors_methods() -> Vec<String> {
vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
"PATCH".to_string(),
"OPTIONS".to_string(),
]
}
fn default_cors_headers() -> Vec<String> {
vec!["Content-Type".to_string(), "Authorization".to_string()]
}
fn default_max_age() -> u32 {
86400
}
#[derive(Debug, Clone, Deserialize)]
pub struct RateLimitSection {
#[serde(default, rename = "Enabled")]
pub enabled: bool,
#[serde(default = "default_rate_limit_rps", rename = "RequestsPerSecond")]
pub requests_per_second: f64,
#[serde(default = "default_rate_limit_burst", rename = "BurstSize")]
pub burst_size: u32,
#[serde(default = "default_rate_limit_max_ips", rename = "MaxTrackedIps")]
pub max_tracked_ips: usize,
}
impl Default for RateLimitSection {
fn default() -> Self {
Self {
enabled: false,
requests_per_second: default_rate_limit_rps(),
burst_size: default_rate_limit_burst(),
max_tracked_ips: default_rate_limit_max_ips(),
}
}
}
fn default_rate_limit_rps() -> f64 {
100.0
}
fn default_rate_limit_burst() -> u32 {
200
}
fn default_rate_limit_max_ips() -> usize {
10_000
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct MetricsSection {
#[serde(default, rename = "Enabled")]
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TlsSection {
#[serde(default, rename = "CertPath")]
pub cert_path: String,
#[serde(default, rename = "KeyPath")]
pub key_path: String,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct AppOptions {
#[serde(default, rename = "App")]
pub app: AppSection,
#[serde(default, rename = "Jwt")]
pub jwt: JwtSection,
#[serde(default, rename = "Cors")]
pub cors: CorsSection,
#[serde(default, rename = "Tls")]
pub tls: TlsSection,
#[serde(default, rename = "RateLimit")]
pub rate_limit: RateLimitSection,
#[serde(default, rename = "Metrics")]
pub metrics: MetricsSection,
}
pub fn load_appsettings(mode: AppMode) -> Option<serde_json::Value> {
let mut base = read_json_file("appsettings.json")?;
let overlay_name = mode.overlay_filename();
if let Some(overlay) = read_json_file(overlay_name) {
merge_json(&mut base, overlay);
}
apply_well_known_env_overrides(&mut base);
apply_env_overrides(&mut base);
Some(base)
}
fn apply_well_known_env_overrides(config: &mut serde_json::Value) {
if let Ok(secret) = std::env::var("JWT_SECRET") {
if !secret.is_empty() {
set_json_value(config, &["Jwt", "Secret"], &secret);
}
}
}
fn apply_env_overrides(config: &mut serde_json::Value) {
for (key, value) in std::env::vars() {
if let Some(path) = key.strip_prefix("APP__") {
let segments: Vec<&str> = path.split("__").collect();
if segments.is_empty() {
continue;
}
set_json_value(config, &segments, &value);
}
}
}
fn set_json_value(obj: &mut serde_json::Value, segments: &[&str], value: &str) {
if segments.is_empty() {
return;
}
let key = segments[0];
if let serde_json::Value::Object(map) = obj {
if segments.len() == 1 {
let parsed =
serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()));
map.insert(key.to_string(), parsed);
} else if let Some(child) = map.get_mut(key) {
set_json_value(child, &segments[1..], value);
} else {
let mut child = serde_json::json!({});
set_json_value(&mut child, &segments[1..], value);
map.insert(key.to_string(), child);
}
}
}
pub fn bind_config<T: for<'de> Deserialize<'de> + Default>(
config: &serde_json::Value,
section: &str,
) -> T {
if section.is_empty() || section == "." {
serde_json::from_value(config.clone()).unwrap_or_default()
} else {
config
.get(section)
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
.unwrap_or_default()
}
}
pub fn bind_root<T: for<'de> Deserialize<'de> + Default>(config: &serde_json::Value) -> T {
serde_json::from_value(config.clone()).unwrap_or_default()
}
fn read_json_file(path: impl AsRef<Path>) -> Option<serde_json::Value> {
let path = path.as_ref();
fn try_read(path: &Path) -> Option<serde_json::Value> {
let content = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
let base = crate::paths::app_base();
let candidate = base.join(path);
if let Some(value) = try_read(&candidate) {
return Some(value);
}
if let Some(value) = try_read(path) {
return Some(value);
}
None
}
fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
match (base, overlay) {
(serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
for (k, v) in b {
merge_json(a.entry(k).or_insert(serde_json::Value::Null), v);
}
}
(a, b) => *a = b,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_env_jwt_and_app_override_precedence() {
std::env::set_var("JWT_SECRET", "env-jwt-secret-that-is-long-enough-32");
let mut config = serde_json::json!({ "Jwt": { "Secret": "from-file" } });
apply_well_known_env_overrides(&mut config);
assert_eq!(
config["Jwt"]["Secret"],
"env-jwt-secret-that-is-long-enough-32"
);
std::env::set_var(
"APP__Jwt__Secret",
"from-app-override-secret-32chars-minimum",
);
apply_env_overrides(&mut config);
std::env::remove_var("JWT_SECRET");
std::env::remove_var("APP__Jwt__Secret");
assert_eq!(
config["Jwt"]["Secret"],
"from-app-override-secret-32chars-minimum"
);
}
}