//! What a machine that has never been configured looks like. use std::collections::BTreeMap; use std::path::PathBuf; use crate::config::{Config, Profile, Service, ServiceKind}; /// The profile a directory nobody has bound resolves to. pub const DEFAULT_PROFILE: &str = "default"; /// Directory this project keeps its own files in, under each XDG base. const PROJECT_DIR: &str = "didbot"; /// Name of the configuration file itself. const CONFIG_FILE: &str = "stack.toml"; /// The personal data server the default profile names, and so the directory /// its store lands in. const PDS_SERVICE: &str = "pds"; /// The interface a service listens on unless it says otherwise. /// /// Loopback, and an address rather than a name: `localhost` resolves to `::1` /// under systemd-resolved and to `127.0.0.1` elsewhere, and a default that /// means different things on different machines is not a default. pub(crate) fn host() -> String { "127.0.0.1".to_owned() } /// An XDG base directory: the variable if it names one, else the fallback /// under `$HOME`, else this directory. /// /// The current directory last because these paths are read on machines that /// have neither variable — a container, a cron job — and returning a relative /// path there beats returning one rooted at `/`. fn xdg_base(variable: &str, fallback: &str) -> PathBuf { std::env::var_os(variable) .map(PathBuf::from) .filter(|path| !path.as_os_str().is_empty()) .or_else(|| { std::env::var_os("HOME") .map(PathBuf::from) .filter(|path| !path.as_os_str().is_empty()) .map(|home| home.join(fallback)) }) .unwrap_or_else(|| PathBuf::from(".")) } /// `$XDG_CONFIG_HOME/didbot/stack.toml`. /// /// Configuration rather than state: it is hand-editable, it is worth keeping, /// and losing it changes what this machine does rather than merely costing it /// some accounts. That is the opposite of [`default_data_dir`], which lives /// under the state directory for the opposite reasons. pub fn default_config_path() -> PathBuf { xdg_base("XDG_CONFIG_HOME", ".config") .join(PROJECT_DIR) .join(CONFIG_FILE) } /// `$XDG_STATE_HOME/didbot/`, where that server keeps its store. /// /// State rather than configuration: nothing here is hand-editable, and losing /// it costs this machine its local accounts and nothing else. That is what /// the state directory is for, and it is why this is the default — a restart /// that keeps the accounts a session is holding is worth more than a clean /// slate, which `DIDBOT_PDS_DATA=` still asks for by name. /// /// Per user rather than per checkout, because the agents on this machine /// address one server on one port; a second checkout serving the same port /// is refused by `pds.lock` before it can write. Keyed by service name so /// that a machine running two of them keeps two stores: one log has one /// writer, and the second opener is refused rather than interleaved. pub fn default_data_dir(service: &str) -> PathBuf { xdg_base("XDG_STATE_HOME", ".local/state") .join(PROJECT_DIR) .join(service) } /// One of everything, on the ports and the state directory `scripts/dev-*.sh` /// use. /// /// These are duplicated from those scripts on purpose, and the duplication is /// checked: the tests below read the scripts and compare. A default that /// drifted from what the scripts actually bind would send every hook on an /// unconfigured machine to a port with nothing on it, and one that drifted /// from where they keep state would have `didbot-setup show` describe a store /// nothing is writing. pub(crate) fn builtin() -> Config { let mut services = BTreeMap::new(); services.insert( PDS_SERVICE.to_owned(), Service { kind: ServiceKind::Pds, port: 3000, host: host(), zone: Some("agents.localhost".to_owned()), names: Some("mineral+creature".to_owned()), pds: None, index: None, query: None, data: Some(default_data_dir(PDS_SERVICE)), }, ); let mut profiles = BTreeMap::new(); profiles.insert( DEFAULT_PROFILE.to_owned(), Profile { pds: PDS_SERVICE.to_owned(), // The record host, the index, the query service and the // canvas are vibescrobble.com's processes now. `ServiceKind` // still names them, so a developer running both can declare them // in their own profile; what is built in is what this repository // can start. index: None, query: None, web: None, }, ); Config { services, profiles, bind: BTreeMap::new(), debug: false, } } #[cfg(test)] mod tests { use super::*; /// The one duplication in this crate, checked rather than trusted. #[test] fn the_builtin_ports_match_the_development_scripts() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(|crates| crates.parent()) .expect("the workspace root is two levels up") .join("scripts"); let config = builtin(); let (script, variable, service) = ("dev-pds.sh", "DIDBOT_PDS_PORT", "pds"); let source = std::fs::read_to_string(root.join(script)) .unwrap_or_else(|err| panic!("read {script}: {err}")); let expected = config.services[service].port; let needle = format!("${{{variable}:-{expected}}}"); assert!( source.contains(&needle), "{script} does not default {variable} to {expected}; \ the builtin configuration and the script disagree" ); } /// The other duplication, checked the same way. /// /// `-` rather than `:-` is the whole escape hatch: an unset variable takes /// the default, and one explicitly emptied asks for a store in memory. #[test] fn the_builtin_data_directory_matches_the_development_script() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(|crates| crates.parent()) .expect("the workspace root is two levels up") .join("scripts"); let source = std::fs::read_to_string(root.join("dev-pds.sh")) .unwrap_or_else(|err| panic!("read dev-pds.sh: {err}")); let service = &builtin().profiles[DEFAULT_PROFILE].pds; let needle = format!( "${{DIDBOT_PDS_DATA-${{XDG_STATE_HOME:-$HOME/.local/state}}/{PROJECT_DIR}/{service}}}" ); assert!( source.contains(&needle), "dev-pds.sh does not default DIDBOT_PDS_DATA to {needle}; \ the builtin configuration and the script disagree" ); } }