1use std::time::Duration;
32
33use serde::{Deserialize, Serialize};
34
35#[derive(Debug, thiserror::Error)]
37#[non_exhaustive]
38pub enum ConfigError {
39 #[error("invalid value {value:?} for `{key}`: {reason}")]
41 InvalidValue {
42 key: String,
44 value: String,
46 reason: String,
48 },
49 #[error("failed to read configuration file `{path}`: {source}")]
51 Read {
52 path: String,
54 #[source]
56 source: std::io::Error,
57 },
58 #[error("failed to parse configuration file `{path}`: {message}")]
60 Parse {
61 path: String,
63 message: String,
65 },
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(default, deny_unknown_fields)]
89pub struct ServerConfig {
90 pub host: String,
92 pub port: u16,
94 pub workers: Option<usize>,
96 #[serde(with = "secs::option")]
99 pub request_timeout: Option<Duration>,
100 pub max_body_size: usize,
104 pub max_connections: Option<usize>,
106 pub max_concurrent_requests: Option<usize>,
108 #[serde(with = "secs")]
111 pub shutdown_timeout: Duration,
112 #[serde(with = "secs")]
117 pub header_read_timeout: Duration,
118 pub tcp_nodelay: bool,
120 pub connect_info: bool,
124}
125
126pub const DEFAULT_MAX_BODY_SIZE: usize = 2 * 1024 * 1024;
128
129impl Default for ServerConfig {
130 fn default() -> Self {
131 Self {
132 host: "127.0.0.1".to_owned(),
133 port: 3000,
134 workers: None,
135 request_timeout: None,
136 max_body_size: DEFAULT_MAX_BODY_SIZE,
137 max_connections: None,
138 max_concurrent_requests: None,
139 shutdown_timeout: Duration::from_secs(30),
140 header_read_timeout: Duration::from_secs(30),
141 tcp_nodelay: true,
142 connect_info: true,
143 }
144 }
145}
146
147impl ServerConfig {
148 pub fn new() -> Self {
150 Self::default()
151 }
152
153 #[must_use]
155 pub fn host(mut self, host: impl Into<String>) -> Self {
156 self.host = host.into();
157 self
158 }
159
160 #[must_use]
162 pub fn port(mut self, port: u16) -> Self {
163 self.port = port;
164 self
165 }
166
167 #[must_use]
169 pub fn workers(mut self, workers: usize) -> Self {
170 self.workers = Some(workers);
171 self
172 }
173
174 #[must_use]
176 pub fn request_timeout(mut self, timeout: Duration) -> Self {
177 self.request_timeout = Some(timeout);
178 self
179 }
180
181 #[must_use]
183 pub fn max_body_size(mut self, bytes: usize) -> Self {
184 self.max_body_size = bytes;
185 self
186 }
187
188 #[must_use]
190 pub fn max_connections(mut self, max: usize) -> Self {
191 self.max_connections = Some(max);
192 self
193 }
194
195 #[must_use]
197 pub fn max_concurrent_requests(mut self, max: usize) -> Self {
198 self.max_concurrent_requests = Some(max);
199 self
200 }
201
202 #[must_use]
204 pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
205 self.shutdown_timeout = timeout;
206 self
207 }
208
209 #[must_use]
211 pub fn header_read_timeout(mut self, timeout: Duration) -> Self {
212 self.header_read_timeout = timeout;
213 self
214 }
215
216 #[must_use]
218 pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
219 self.tcp_nodelay = enabled;
220 self
221 }
222
223 #[must_use]
225 pub fn connect_info(mut self, enabled: bool) -> Self {
226 self.connect_info = enabled;
227 self
228 }
229
230 pub fn address(&self) -> String {
232 if self.host.contains(':') && !self.host.starts_with('[') {
233 format!("[{}]:{}", self.host, self.port)
234 } else {
235 format!("{}:{}", self.host, self.port)
236 }
237 }
238
239 pub fn from_env() -> Result<Self, ConfigError> {
242 Self::default().apply_env()
243 }
244
245 pub fn apply_env(self) -> Result<Self, ConfigError> {
248 self.apply_env_vars(std::env::vars())
249 }
250
251 pub fn apply_env_vars<I, K, V>(mut self, vars: I) -> Result<Self, ConfigError>
270 where
271 I: IntoIterator<Item = (K, V)>,
272 K: AsRef<str>,
273 V: AsRef<str>,
274 {
275 for (key, value) in vars {
276 let (key, value) = (key.as_ref(), value.as_ref().trim());
277 let Some(name) = key.strip_prefix("KERNIQ_") else {
278 continue;
279 };
280 match name {
281 "HOST" => self.host = value.to_owned(),
282 "PORT" => self.port = parse(key, value)?,
283 "WORKERS" => self.workers = Some(parse(key, value)?),
284 "REQUEST_TIMEOUT" => self.request_timeout = Some(parse_secs(key, value)?),
285 "MAX_BODY_SIZE" => self.max_body_size = parse(key, value)?,
286 "MAX_CONNECTIONS" => self.max_connections = Some(parse(key, value)?),
287 "MAX_CONCURRENT_REQUESTS" => {
288 self.max_concurrent_requests = Some(parse(key, value)?)
289 }
290 "SHUTDOWN_TIMEOUT" => self.shutdown_timeout = parse_secs(key, value)?,
291 "HEADER_READ_TIMEOUT" => self.header_read_timeout = parse_secs(key, value)?,
292 "TCP_NODELAY" => self.tcp_nodelay = parse_bool(key, value)?,
293 "CONNECT_INFO" => self.connect_info = parse_bool(key, value)?,
294 _ => {}
295 }
296 }
297 self.validate()?;
298 Ok(self)
299 }
300
301 #[cfg(feature = "config-file")]
311 pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, ConfigError> {
312 let path = path.as_ref();
313 let display = path.display().to_string();
314 let text = std::fs::read_to_string(path).map_err(|source| ConfigError::Read {
315 path: display.clone(),
316 source,
317 })?;
318 Self::from_toml_str(&text).map_err(|err| match err {
319 ConfigError::Parse { message, .. } => ConfigError::Parse {
320 path: display,
321 message,
322 },
323 other => other,
324 })
325 }
326
327 #[cfg(feature = "config-file")]
329 pub fn from_toml_str(text: &str) -> Result<Self, ConfigError> {
330 let config: Self = toml::from_str(text).map_err(|err| ConfigError::Parse {
331 path: "<string>".to_owned(),
332 message: err.message().to_owned(),
333 })?;
334 config.validate()?;
335 Ok(config)
336 }
337
338 pub fn validate(&self) -> Result<(), ConfigError> {
340 fn non_zero(key: &str, value: Option<usize>) -> Result<(), ConfigError> {
341 match value {
342 Some(0) => Err(ConfigError::InvalidValue {
343 key: key.to_owned(),
344 value: "0".to_owned(),
345 reason: "must be greater than zero".to_owned(),
346 }),
347 _ => Ok(()),
348 }
349 }
350 if self.host.trim().is_empty() {
351 return Err(ConfigError::InvalidValue {
352 key: "host".to_owned(),
353 value: self.host.clone(),
354 reason: "must not be empty".to_owned(),
355 });
356 }
357 non_zero("workers", self.workers)?;
358 non_zero("max_connections", self.max_connections)?;
359 non_zero("max_concurrent_requests", self.max_concurrent_requests)?;
360 if self.request_timeout == Some(Duration::ZERO) {
361 return Err(ConfigError::InvalidValue {
362 key: "request_timeout".to_owned(),
363 value: "0".to_owned(),
364 reason: "must be greater than zero".to_owned(),
365 });
366 }
367 Ok(())
368 }
369}
370
371fn parse<T>(key: &str, value: &str) -> Result<T, ConfigError>
372where
373 T: std::str::FromStr,
374 T::Err: std::fmt::Display,
375{
376 value
377 .parse()
378 .map_err(|err: T::Err| ConfigError::InvalidValue {
379 key: key.to_owned(),
380 value: value.to_owned(),
381 reason: err.to_string(),
382 })
383}
384
385fn parse_secs(key: &str, value: &str) -> Result<Duration, ConfigError> {
386 let secs: f64 = parse(key, value)?;
387 Duration::try_from_secs_f64(secs).map_err(|err| ConfigError::InvalidValue {
388 key: key.to_owned(),
389 value: value.to_owned(),
390 reason: err.to_string(),
391 })
392}
393
394fn parse_bool(key: &str, value: &str) -> Result<bool, ConfigError> {
395 match value.to_ascii_lowercase().as_str() {
396 "1" | "true" | "yes" | "on" => Ok(true),
397 "0" | "false" | "no" | "off" => Ok(false),
398 _ => Err(ConfigError::InvalidValue {
399 key: key.to_owned(),
400 value: value.to_owned(),
401 reason: "expected a boolean (true/false/1/0)".to_owned(),
402 }),
403 }
404}
405
406mod secs {
408 use std::time::Duration;
409
410 use serde::{Deserialize, Deserializer, Serializer};
411
412 pub(super) fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
413 s.serialize_f64(d.as_secs_f64())
414 }
415
416 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
417 let secs = f64::deserialize(d)?;
418 Duration::try_from_secs_f64(secs).map_err(serde::de::Error::custom)
419 }
420
421 pub(super) mod option {
422 use super::*;
423
424 pub(crate) fn serialize<S: Serializer>(
425 d: &Option<Duration>,
426 s: S,
427 ) -> Result<S::Ok, S::Error> {
428 match d {
429 Some(d) => s.serialize_some(&d.as_secs_f64()),
430 None => s.serialize_none(),
431 }
432 }
433
434 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
435 d: D,
436 ) -> Result<Option<Duration>, D::Error> {
437 Option::<f64>::deserialize(d)?
438 .map(|secs| Duration::try_from_secs_f64(secs).map_err(serde::de::Error::custom))
439 .transpose()
440 }
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn builder_sets_fields() {
450 let c = ServerConfig::new()
451 .host("0.0.0.0")
452 .port(8080)
453 .workers(4)
454 .request_timeout(Duration::from_secs(30))
455 .max_body_size(1024)
456 .max_connections(10)
457 .max_concurrent_requests(5)
458 .shutdown_timeout(Duration::from_secs(5));
459 assert_eq!(c.address(), "0.0.0.0:8080");
460 assert_eq!(c.workers, Some(4));
461 assert_eq!(c.request_timeout, Some(Duration::from_secs(30)));
462 assert_eq!(c.max_body_size, 1024);
463 assert_eq!(c.max_connections, Some(10));
464 assert_eq!(c.max_concurrent_requests, Some(5));
465 assert_eq!(c.shutdown_timeout, Duration::from_secs(5));
466 c.validate().unwrap();
467 }
468
469 #[test]
470 fn ipv6_address_is_bracketed() {
471 assert_eq!(
472 ServerConfig::new().host("::1").port(80).address(),
473 "[::1]:80"
474 );
475 }
476
477 #[test]
478 fn env_overrides() {
479 let c = ServerConfig::default()
480 .apply_env_vars([
481 ("KERNIQ_HOST", "0.0.0.0"),
482 ("KERNIQ_PORT", "9000"),
483 ("KERNIQ_REQUEST_TIMEOUT", "2.5"),
484 ("KERNIQ_TCP_NODELAY", "false"),
485 ("KERNIQ_MAX_CONCURRENT_REQUESTS", "64"),
486 ("PATH", "/usr/bin"),
487 ("KERNIQ_UNKNOWN", "ignored"),
488 ])
489 .unwrap();
490 assert_eq!(c.address(), "0.0.0.0:9000");
491 assert_eq!(c.request_timeout, Some(Duration::from_millis(2500)));
492 assert!(!c.tcp_nodelay);
493 assert_eq!(c.max_concurrent_requests, Some(64));
494 }
495
496 #[test]
497 fn env_rejects_bad_values() {
498 let err = ServerConfig::default()
499 .apply_env_vars([("KERNIQ_PORT", "seventy")])
500 .unwrap_err();
501 assert!(err.to_string().contains("KERNIQ_PORT"));
502 let err = ServerConfig::default()
503 .apply_env_vars([("KERNIQ_WORKERS", "0")])
504 .unwrap_err();
505 assert!(err.to_string().contains("workers"));
506 }
507
508 #[cfg(feature = "config-file")]
509 #[test]
510 fn toml_parsing() {
511 let c = ServerConfig::from_toml_str(
512 "host = \"0.0.0.0\"\nport = 8080\nrequest_timeout = 30\nmax_connections = 100\n",
513 )
514 .unwrap();
515 assert_eq!(c.port, 8080);
516 assert_eq!(c.request_timeout, Some(Duration::from_secs(30)));
517 assert_eq!(c.max_connections, Some(100));
518 assert_eq!(c.shutdown_timeout, Duration::from_secs(30));
519
520 let err = ServerConfig::from_toml_str("prot = 1").unwrap_err();
521 assert!(matches!(err, ConfigError::Parse { .. }));
522 }
523}