Skip to main content

kasl_server/
config.rs

1use std::net::SocketAddr;
2
3use anyhow::{Context, Result};
4
5/// Default bind address when `KASL_SERVER_ADDR` is not set.
6const DEFAULT_ADDR: &str = "0.0.0.0:8080";
7
8/// Runtime configuration, read from the environment.
9#[derive(Debug, Clone)]
10pub struct Config {
11    /// Address the HTTP server binds to (`KASL_SERVER_ADDR`).
12    pub addr: SocketAddr,
13    /// PostgreSQL connection string (`DATABASE_URL`).
14    pub database_url: String,
15    /// Agents to provision on startup (`KASL_AGENTS`), as `email:token` pairs.
16    /// The bootstrap way in until the admin UI issues tokens.
17    pub agents: String,
18}
19
20impl Config {
21    pub fn from_env() -> Result<Self> {
22        Self::from_lookup(|key| std::env::var(key).ok())
23    }
24
25    /// The environment is passed in as a lookup so tests can supply their own.
26    fn from_lookup(lookup: impl Fn(&str) -> Option<String>) -> Result<Self> {
27        let addr = lookup("KASL_SERVER_ADDR").unwrap_or_else(|| DEFAULT_ADDR.to_string());
28        let addr = addr
29            .parse()
30            .with_context(|| format!("KASL_SERVER_ADDR is not a valid socket address: {addr}"))?;
31        let database_url = lookup("DATABASE_URL").context("DATABASE_URL is not set (e.g. postgres://kasl:kasl@localhost:5432/kasl)")?;
32        let agents = lookup("KASL_AGENTS").unwrap_or_default();
33        Ok(Self { addr, database_url, agents })
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    fn env<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
42        move |key| pairs.iter().find(|(k, _)| *k == key).map(|(_, v)| v.to_string())
43    }
44
45    #[test]
46    fn defaults_the_bind_address() {
47        let config = Config::from_lookup(env(&[("DATABASE_URL", "postgres://localhost/kasl")])).expect("config should build with only DATABASE_URL set");
48        assert_eq!(config.addr, DEFAULT_ADDR.parse().unwrap());
49        assert_eq!(config.database_url, "postgres://localhost/kasl");
50    }
51
52    #[test]
53    fn reads_the_bind_address_override() {
54        let config = Config::from_lookup(env(&[("DATABASE_URL", "postgres://localhost/kasl"), ("KASL_SERVER_ADDR", "127.0.0.1:9090")]))
55            .expect("config should accept a valid override");
56        assert_eq!(config.addr, "127.0.0.1:9090".parse().unwrap());
57    }
58
59    #[test]
60    fn requires_database_url() {
61        let error = Config::from_lookup(env(&[])).unwrap_err();
62        assert!(error.to_string().contains("DATABASE_URL"));
63    }
64
65    #[test]
66    fn rejects_a_malformed_bind_address() {
67        let error = Config::from_lookup(env(&[("DATABASE_URL", "postgres://localhost/kasl"), ("KASL_SERVER_ADDR", "not-an-address")])).unwrap_err();
68        assert!(error.to_string().contains("KASL_SERVER_ADDR"));
69    }
70}