prns-config 0.3.4

Config discovery, parsing, and reconciliation for the Personal Reticulum daemon
Documentation
use std::fmt;
use std::path::{Path, PathBuf};

use prns_core::persistence::reticulum_directory;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveredConfig {
    pub dir: PathBuf,
    pub config: Option<PathBuf>,
}

impl DiscoveredConfig {
    pub fn is_empty(&self) -> bool {
        self.config.is_none()
    }
}

#[derive(Debug)]
pub enum DiscoveryError {
    HomeDirectoryUnavailable,
}

impl fmt::Display for DiscoveryError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::HomeDirectoryUnavailable => formatter.write_str(
                "could not determine the Reticulum config directory; pass --config explicitly",
            ),
        }
    }
}

impl std::error::Error for DiscoveryError {}

fn probe(dir: PathBuf, exists: &impl Fn(&Path) -> bool) -> DiscoveredConfig {
    let config = dir.join(reticulum_directory::CONFIG_FILE_NAME);
    DiscoveredConfig {
        config: exists(&config).then_some(config),
        dir,
    }
}

pub fn discover(override_dir: Option<&Path>) -> Result<DiscoveredConfig, DiscoveryError> {
    let dir = match override_dir {
        Some(dir) => dir.to_path_buf(),
        None => reticulum_directory::resolve().ok_or(DiscoveryError::HomeDirectoryUnavailable)?,
    };
    Ok(probe(dir, &|path: &Path| path.is_file()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;

    fn world(files: &[&str]) -> impl Fn(&Path) -> bool {
        let present: HashSet<PathBuf> = files.iter().map(PathBuf::from).collect();
        move |path: &Path| present.contains(path)
    }

    #[test]
    fn an_override_wins_outright_even_when_empty() {
        let dir = tempfile::tempdir().unwrap();
        let discovered = discover(Some(dir.path())).unwrap();
        assert_eq!(discovered.dir, dir.path());
        assert!(discovered.is_empty());
    }

    #[test]
    fn probe_ignores_the_retired_toml_filename() {
        let configs = probe(
            PathBuf::from("/home/op/.reticulum"),
            &world(&["/home/op/.reticulum/config.toml"]),
        );
        assert_eq!(configs.config, None);
        assert!(configs.is_empty());
    }

    #[test]
    fn probe_returns_only_the_extensionless_config() {
        let configs = probe(
            PathBuf::from("/etc/reticulum"),
            &world(&["/etc/reticulum/config", "/etc/reticulum/config.toml"]),
        );
        assert_eq!(configs.config, Some(PathBuf::from("/etc/reticulum/config")));
    }
}