Skip to main content

synapse/
config.rs

1//! Layered runtime config (env + file paths). Env takes precedence.
2
3use std::collections::HashMap;
4use std::time::Duration;
5
6#[derive(Debug, Clone)]
7pub struct Config {
8    pub addr: String,
9    pub metrics_addr: String,
10    pub routes_path: String,
11    pub pricing_path: String,
12    pub ledger_backends: Vec<LedgerBackend>,
13    pub default_tenant: String,
14    pub request_timeout: Duration,
15    pub stream_idle_timeout: Duration,
16    pub embed_default_input_per_mtok: f64,
17    /// Provider credentials/base-urls, read straight from the env map.
18    pub env: HashMap<String, String>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum LedgerBackend {
23    Sqlite,
24    Postgres,
25    Pubsub,
26    Sns,
27}
28
29impl LedgerBackend {
30    pub fn label(self) -> &'static str {
31        match self {
32            LedgerBackend::Sqlite => "sqlite",
33            LedgerBackend::Postgres => "postgres",
34            LedgerBackend::Pubsub => "pubsub",
35            LedgerBackend::Sns => "sns",
36        }
37    }
38}
39
40fn parse_ledger_backends(list: &str) -> anyhow::Result<Vec<LedgerBackend>> {
41    let mut out: Vec<LedgerBackend> = Vec::new();
42    for raw in list.split(',') {
43        let name = raw.trim().to_ascii_lowercase();
44        if name.is_empty() {
45            continue;
46        }
47        let b = match name.as_str() {
48            "sqlite" => LedgerBackend::Sqlite,
49            "postgres" => LedgerBackend::Postgres,
50            "pubsub" => LedgerBackend::Pubsub,
51            "sns" => LedgerBackend::Sns,
52            other => anyhow::bail!("unknown ledger backend '{other}' (sqlite|postgres|pubsub|sns)"),
53        };
54        if out.contains(&b) {
55            anyhow::bail!("duplicate ledger backend '{name}' in SYNAPSE_LEDGER_BACKENDS");
56        }
57        out.push(b);
58    }
59    if out.is_empty() {
60        anyhow::bail!("SYNAPSE_LEDGER_BACKENDS resolved to an empty backend list");
61    }
62    Ok(out)
63}
64
65impl Config {
66    pub fn from_env_map(env: &HashMap<String, String>) -> anyhow::Result<Self> {
67        let get = |k: &str| env.get(k).cloned().filter(|s| !s.trim().is_empty());
68        let get_or = |k: &str, d: &str| get(k).unwrap_or_else(|| d.to_string());
69        let backends_raw = get("SYNAPSE_LEDGER_BACKENDS")
70            .or_else(|| get("SYNAPSE_LEDGER_BACKEND"))
71            .unwrap_or_else(|| "sqlite".to_string());
72        let ledger_backends = parse_ledger_backends(&backends_raw)?;
73        Ok(Self {
74            addr: get_or("SYNAPSE_ADDR", "0.0.0.0:8080"),
75            metrics_addr: get_or("SYNAPSE_METRICS_ADDR", "0.0.0.0:9090"),
76            routes_path: get_or("SYNAPSE_ROUTES_PATH", "config/routes.toml"),
77            pricing_path: get_or("SYNAPSE_PRICING_PATH", "config/pricing.toml"),
78            ledger_backends,
79            default_tenant: get_or("SYNAPSE_DEFAULT_TENANT", "unattributed"),
80            request_timeout: Duration::from_secs(
81                get_or("SYNAPSE_REQUEST_TIMEOUT_SECS", "120")
82                    .parse()
83                    .map_err(|e| anyhow::anyhow!("SYNAPSE_REQUEST_TIMEOUT_SECS: {e}"))?,
84            ),
85            stream_idle_timeout: Duration::from_secs(
86                get_or("SYNAPSE_STREAM_IDLE_TIMEOUT_SECS", "60")
87                    .parse()
88                    .map_err(|e| anyhow::anyhow!("SYNAPSE_STREAM_IDLE_TIMEOUT_SECS: {e}"))?,
89            ),
90            embed_default_input_per_mtok: get_or(
91                "SYNAPSE_EMBED_DEFAULT_INPUT_PRICE_PER_MTOK",
92                "0.10",
93            )
94            .parse()
95            .unwrap_or(0.10),
96            env: env.clone(),
97        })
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    fn env(pairs: &[(&str, &str)]) -> HashMap<String, String> {
106        pairs
107            .iter()
108            .map(|(k, v)| (k.to_string(), v.to_string()))
109            .collect()
110    }
111
112    #[test]
113    fn defaults_apply_when_env_empty() {
114        let c = Config::from_env_map(&env(&[])).unwrap();
115        assert_eq!(c.addr, "0.0.0.0:8080");
116        assert_eq!(c.ledger_backends, vec![LedgerBackend::Sqlite]);
117        assert_eq!(c.default_tenant, "unattributed");
118    }
119
120    #[test]
121    fn env_overrides_and_validates_backend() {
122        let c = Config::from_env_map(&env(&[("SYNAPSE_LEDGER_BACKEND", "postgres")])).unwrap();
123        assert_eq!(c.ledger_backends, vec![LedgerBackend::Postgres]);
124        let err = Config::from_env_map(&env(&[("SYNAPSE_LEDGER_BACKEND", "mysql")])).unwrap_err();
125        assert!(err.to_string().contains("sqlite|postgres"));
126    }
127
128    #[test]
129    fn parses_stream_timeouts() {
130        let c = Config::from_env_map(&env(&[
131            ("SYNAPSE_REQUEST_TIMEOUT_SECS", "30"),
132            ("SYNAPSE_STREAM_IDLE_TIMEOUT_SECS", "45"),
133        ]))
134        .unwrap();
135        assert_eq!(c.request_timeout, std::time::Duration::from_secs(30));
136        assert_eq!(c.stream_idle_timeout, std::time::Duration::from_secs(45));
137    }
138
139    #[test]
140    fn parses_backend_list() {
141        let c =
142            Config::from_env_map(&env(&[("SYNAPSE_LEDGER_BACKENDS", "postgres, pubsub")])).unwrap();
143        assert_eq!(
144            c.ledger_backends,
145            vec![LedgerBackend::Postgres, LedgerBackend::Pubsub]
146        );
147    }
148
149    #[test]
150    fn singular_backend_is_back_compat() {
151        let c = Config::from_env_map(&env(&[("SYNAPSE_LEDGER_BACKEND", "sns")])).unwrap();
152        assert_eq!(c.ledger_backends, vec![LedgerBackend::Sns]);
153    }
154
155    #[test]
156    fn defaults_to_sqlite_list() {
157        let c = Config::from_env_map(&env(&[])).unwrap();
158        assert_eq!(c.ledger_backends, vec![LedgerBackend::Sqlite]);
159    }
160
161    #[test]
162    fn rejects_unknown_and_duplicate_backends() {
163        assert!(Config::from_env_map(&env(&[("SYNAPSE_LEDGER_BACKENDS", "mysql")])).is_err());
164        assert!(
165            Config::from_env_map(&env(&[("SYNAPSE_LEDGER_BACKENDS", "pubsub,pubsub")])).is_err()
166        );
167    }
168}