Skip to main content

siderite_cli/
settings.rs

1//! The settings the command line needs, as plain values.
2//!
3//! [`CliSettings`] is a thin adapter: it carries only the listen address and
4//! the database URLs, so this crate does not depend on the configuration
5//! crate's `Settings` type. The application (or the configuration crate) maps
6//! its settings into it, for example from `server.addr` and
7//! `databases.<alias>.url`.
8
9use std::collections::BTreeMap;
10use std::fmt;
11
12/// Listen address used when neither a flag, `ADDR` nor the settings give one.
13pub const DEFAULT_ADDR: &str = "127.0.0.1:8000";
14
15/// Alias of the database that holds the application's models.
16pub const DEFAULT_DATABASE: &str = "default";
17
18/// Listen address and database URLs for the command line.
19///
20/// `Debug` prints the database aliases only: URLs may contain passwords.
21#[derive(Clone, Default, PartialEq, Eq)]
22pub struct CliSettings {
23    addr: Option<String>,
24    databases: BTreeMap<String, String>,
25}
26
27impl CliSettings {
28    /// Empty settings: no address (so [`DEFAULT_ADDR`] applies), no databases.
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Set the configured listen address (`server.addr`).
34    #[must_use]
35    pub fn addr(mut self, addr: impl Into<String>) -> Self {
36        self.addr = Some(addr.into());
37        self
38    }
39
40    /// Register the database `alias` at `url`.
41    #[must_use]
42    pub fn database(mut self, alias: impl Into<String>, url: impl Into<String>) -> Self {
43        self.databases.insert(alias.into(), url.into());
44        self
45    }
46
47    /// Replace all databases (`alias -> url`).
48    #[must_use]
49    pub fn databases(mut self, databases: BTreeMap<String, String>) -> Self {
50        self.databases = databases;
51        self
52    }
53
54    /// The configured listen address, if any.
55    pub fn configured_addr(&self) -> Option<&str> {
56        self.addr.as_deref()
57    }
58
59    /// Database URLs by alias.
60    pub fn database_urls(&self) -> &BTreeMap<String, String> {
61        &self.databases
62    }
63
64    /// URL of the database `alias`.
65    pub fn database_url(&self, alias: &str) -> Option<&str> {
66        self.databases.get(alias).map(String::as_str)
67    }
68
69    /// Resolve the listen address: `--addr` flag, then the `ADDR`
70    /// environment variable, then the configured address, then
71    /// [`DEFAULT_ADDR`]. Empty values count as unset.
72    pub fn resolve_addr(&self, flag: Option<&str>, env: Option<&str>) -> String {
73        [flag, env, self.addr.as_deref()]
74            .into_iter()
75            .flatten()
76            .find(|addr| !addr.trim().is_empty())
77            .unwrap_or(DEFAULT_ADDR)
78            .to_owned()
79    }
80}
81
82impl fmt::Debug for CliSettings {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.debug_struct("CliSettings")
85            .field("addr", &self.addr)
86            .field("databases", &self.databases.keys().collect::<Vec<_>>())
87            .finish()
88    }
89}
90
91impl From<&siderite_config::Settings> for CliSettings {
92    /// Take the server address and every database URL from loaded settings.
93    fn from(settings: &siderite_config::Settings) -> Self {
94        let databases = settings
95            .databases
96            .iter()
97            .map(|(alias, db)| (alias.clone(), db.url.expose().clone()))
98            .collect();
99        Self::new()
100            .addr(settings.server.addr.clone())
101            .databases(databases)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn converts_from_loaded_settings() {
111        let settings = siderite_config::ConfigBuilder::new()
112            .set("server.addr", "127.0.0.1:9100")
113            .set("databases.default.url", "sqlite::memory:")
114            .build()
115            .unwrap();
116        let cli = CliSettings::from(&settings);
117        assert_eq!(cli.configured_addr(), Some("127.0.0.1:9100"));
118        assert_eq!(cli.database_url("default"), Some("sqlite::memory:"));
119        assert!(!format!("{cli:?}").contains("sqlite::memory:"));
120    }
121
122    #[test]
123    fn addr_precedence_is_flag_env_settings_default() {
124        let settings = CliSettings::new().addr("0.0.0.0:9000");
125        assert_eq!(
126            settings.resolve_addr(Some("1.1.1.1:1"), Some("2.2.2.2:2")),
127            "1.1.1.1:1"
128        );
129        assert_eq!(settings.resolve_addr(None, Some("2.2.2.2:2")), "2.2.2.2:2");
130        assert_eq!(settings.resolve_addr(None, None), "0.0.0.0:9000");
131        assert_eq!(CliSettings::new().resolve_addr(None, None), DEFAULT_ADDR);
132    }
133
134    #[test]
135    fn empty_values_are_unset() {
136        let settings = CliSettings::new().addr("0.0.0.0:9000");
137        assert_eq!(settings.resolve_addr(Some(""), Some("  ")), "0.0.0.0:9000");
138        assert_eq!(
139            CliSettings::new().addr("").resolve_addr(None, None),
140            DEFAULT_ADDR
141        );
142    }
143
144    #[test]
145    fn debug_never_prints_urls() {
146        let settings = CliSettings::new().database("default", "postgres://user:hunter2@db/app");
147        let shown = format!("{settings:?}");
148        assert!(shown.contains("default"));
149        assert!(!shown.contains("hunter2"), "{shown}");
150        assert!(!shown.contains("postgres://"), "{shown}");
151    }
152
153    #[test]
154    fn database_lookup() {
155        let settings = CliSettings::new()
156            .database("default", "sqlite::memory:")
157            .database("replica", "sqlite::memory:");
158        assert_eq!(settings.database_url("default"), Some("sqlite::memory:"));
159        assert_eq!(settings.database_url("other"), None);
160        assert_eq!(settings.database_urls().len(), 2);
161    }
162}