Skip to main content

rust_web_server/server_config/
mod.rs

1//! Typed server configuration.
2//!
3//! [`ServerConfig`] holds all per-instance configuration fields as typed Rust
4//! values. It replaces point-of-use `env::var("RWS_CONFIG_*")` calls with a
5//! struct that can be:
6//!
7//! - Built from environment variables once at startup: [`ServerConfig::from_env()`]
8//! - Constructed directly in tests without touching the environment:
9//!   [`ServerConfig::default()`] / struct update syntax
10//! - Passed to [`App::with_config`] to create a fully isolated application
11//!   instance — essential for parallel tests and embedded multi-tenant use
12//!
13//! # Test isolation example
14//!
15//! ```rust,ignore
16//! use rust_web_server::app::App;
17//! use rust_web_server::server_config::ServerConfig;
18//! use rust_web_server::test_client::TestClient;
19//!
20//! // No env writes, no lock needed.
21//! let app = App::with_config(ServerConfig {
22//!     cors_allow_all: false,
23//!     cors_allow_origins: "https://example.com".to_string(),
24//!     ..ServerConfig::default()
25//! });
26//! let client = TestClient::new(app);
27//! let res = client.get("/").send();
28//! ```
29
30#[cfg(test)]
31mod tests;
32
33use crate::entry_point::Config;
34
35/// Default `Content-Security-Policy` header value. Mirrors
36/// `Header::_CONTENT_SECURITY_POLICY_VALUE_DEFAULT` without creating a
37/// circular import (`server_config` ← `header` ← `cors` ← `server_config`).
38const CSP_DEFAULT: &str = "default-src 'self'";
39
40/// All runtime-configurable settings for one server instance.
41///
42/// Fields map 1-to-1 to `RWS_CONFIG_*` environment variable names documented
43/// in [`Config`]. Default values match the environment-variable defaults.
44///
45/// Construct via [`ServerConfig::from_env()`] at startup or
46/// [`ServerConfig::default()`] in tests.
47#[derive(Clone, Debug, PartialEq)]
48pub struct ServerConfig {
49    // ── CORS ─────────────────────────────────────────────────────────────────
50    /// `RWS_CONFIG_CORS_ALLOW_ALL` — when `true`, all cross-origin requests are
51    /// reflected back as allowed (echo the `Origin` header). Overrides all
52    /// other CORS fields. Default: `true`.
53    pub cors_allow_all: bool,
54    /// `RWS_CONFIG_CORS_ALLOW_ORIGINS` — comma-separated list of allowed
55    /// origins when `cors_allow_all` is `false`. Default: `""` (none).
56    pub cors_allow_origins: String,
57    /// `RWS_CONFIG_CORS_ALLOW_CREDENTIALS` — value for the
58    /// `Access-Control-Allow-Credentials` response header. Default: `""`.
59    pub cors_allow_credentials: String,
60    /// `RWS_CONFIG_CORS_ALLOW_METHODS` — value for
61    /// `Access-Control-Allow-Methods`. Default: `""`.
62    pub cors_allow_methods: String,
63    /// `RWS_CONFIG_CORS_ALLOW_HEADERS` — value for
64    /// `Access-Control-Allow-Headers`. Default: `""`.
65    pub cors_allow_headers: String,
66    /// `RWS_CONFIG_CORS_EXPOSE_HEADERS` — value for
67    /// `Access-Control-Expose-Headers`. Default: `""`.
68    pub cors_expose_headers: String,
69    /// `RWS_CONFIG_CORS_MAX_AGE` — value for `Access-Control-Max-Age`.
70    /// Default: `"86400"`.
71    pub cors_max_age: String,
72
73    // ── Security headers ──────────────────────────────────────────────────────
74    /// `RWS_CONFIG_CSP` — `Content-Security-Policy` header value. An empty
75    /// string suppresses the header entirely. Default: the framework default CSP.
76    pub csp: String,
77
78    // ── Server internals ──────────────────────────────────────────────────────
79    /// `RWS_CONFIG_LOG_FORMAT` — `"json"` or `"combined"`. Default: `"json"`.
80    pub log_format: String,
81    /// `RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES` — bytes allocated for
82    /// incoming request parsing. Default: `10000`.
83    pub request_allocation_size: i64,
84}
85
86impl Default for ServerConfig {
87    fn default() -> Self {
88        Self {
89            cors_allow_all: Config::RWS_CONFIG_CORS_ALLOW_ALL_DEFAULT_VALUE
90                .eq_ignore_ascii_case("true"),
91            cors_allow_origins: Config::RWS_CONFIG_CORS_ALLOW_ORIGINS_DEFAULT_VALUE.to_string(),
92            cors_allow_credentials: Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS_DEFAULT_VALUE
93                .to_string(),
94            cors_allow_methods: Config::RWS_CONFIG_CORS_ALLOW_METHODS_DEFAULT_VALUE.to_string(),
95            cors_allow_headers: Config::RWS_CONFIG_CORS_ALLOW_HEADERS_DEFAULT_VALUE.to_string(),
96            cors_expose_headers: Config::RWS_CONFIG_CORS_EXPOSE_HEADERS_DEFAULT_VALUE.to_string(),
97            cors_max_age: Config::RWS_CONFIG_CORS_MAX_AGE_DEFAULT_VALUE.to_string(),
98            csp: CSP_DEFAULT.to_string(),
99            log_format: Config::RWS_CONFIG_LOG_FORMAT_DEFAULT_VALUE.to_string(),
100            request_allocation_size: *Config::RWS_DEFAULT_REQUEST_ALLOCATION_SIZE_IN_BYTES,
101        }
102    }
103}
104
105impl ServerConfig {
106    /// Build a `ServerConfig` by reading all `RWS_CONFIG_*` environment
107    /// variables. Missing variables fall back to their default values.
108    ///
109    /// Call this once at startup (inside `App::new()`) rather than on every
110    /// request. For hot-reload, use `config_reload::current()` to get a
111    /// fresh snapshot after a `SIGHUP`/`POST /admin/config/reload`.
112    pub fn from_env() -> Self {
113        let read = |key: &str| std::env::var(key).unwrap_or_default();
114        Self {
115            cors_allow_all: read(Config::RWS_CONFIG_CORS_ALLOW_ALL)
116                .eq_ignore_ascii_case("true"),
117            cors_allow_origins: read(Config::RWS_CONFIG_CORS_ALLOW_ORIGINS),
118            cors_allow_credentials: read(Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS),
119            cors_allow_methods: read(Config::RWS_CONFIG_CORS_ALLOW_METHODS),
120            cors_allow_headers: read(Config::RWS_CONFIG_CORS_ALLOW_HEADERS),
121            cors_expose_headers: read(Config::RWS_CONFIG_CORS_EXPOSE_HEADERS),
122            cors_max_age: read(Config::RWS_CONFIG_CORS_MAX_AGE),
123            csp: std::env::var("RWS_CONFIG_CSP")
124                .unwrap_or_else(|_| CSP_DEFAULT.to_string()),
125            log_format: read(Config::RWS_CONFIG_LOG_FORMAT),
126            request_allocation_size: std::env::var(
127                Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES,
128            )
129            .ok()
130            .and_then(|v| v.parse().ok())
131            .unwrap_or(*Config::RWS_DEFAULT_REQUEST_ALLOCATION_SIZE_IN_BYTES),
132        }
133    }
134}