1use std::fmt;
2use std::path::{Path, PathBuf};
3
4use prns_core::persistence::reticulum_directory;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct DiscoveredConfig {
8 pub dir: PathBuf,
9 pub config: Option<PathBuf>,
10}
11
12impl DiscoveredConfig {
13 pub fn is_empty(&self) -> bool {
14 self.config.is_none()
15 }
16}
17
18#[derive(Debug)]
19pub enum DiscoveryError {
20 HomeDirectoryUnavailable,
21}
22
23impl fmt::Display for DiscoveryError {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::HomeDirectoryUnavailable => formatter.write_str(
27 "could not determine the Reticulum config directory; pass --config explicitly",
28 ),
29 }
30 }
31}
32
33impl std::error::Error for DiscoveryError {}
34
35fn probe(dir: PathBuf, exists: &impl Fn(&Path) -> bool) -> DiscoveredConfig {
36 let config = dir.join(reticulum_directory::CONFIG_FILE_NAME);
37 DiscoveredConfig {
38 config: exists(&config).then_some(config),
39 dir,
40 }
41}
42
43pub fn discover(override_dir: Option<&Path>) -> Result<DiscoveredConfig, DiscoveryError> {
44 let dir = match override_dir {
45 Some(dir) => dir.to_path_buf(),
46 None => reticulum_directory::resolve().ok_or(DiscoveryError::HomeDirectoryUnavailable)?,
47 };
48 Ok(probe(dir, &|path: &Path| path.is_file()))
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use std::collections::HashSet;
55
56 fn world(files: &[&str]) -> impl Fn(&Path) -> bool {
57 let present: HashSet<PathBuf> = files.iter().map(PathBuf::from).collect();
58 move |path: &Path| present.contains(path)
59 }
60
61 #[test]
62 fn an_override_wins_outright_even_when_empty() {
63 let dir = tempfile::tempdir().unwrap();
64 let discovered = discover(Some(dir.path())).unwrap();
65 assert_eq!(discovered.dir, dir.path());
66 assert!(discovered.is_empty());
67 }
68
69 #[test]
70 fn probe_ignores_the_retired_toml_filename() {
71 let configs = probe(
72 PathBuf::from("/home/op/.reticulum"),
73 &world(&["/home/op/.reticulum/config.toml"]),
74 );
75 assert_eq!(configs.config, None);
76 assert!(configs.is_empty());
77 }
78
79 #[test]
80 fn probe_returns_only_the_extensionless_config() {
81 let configs = probe(
82 PathBuf::from("/etc/reticulum"),
83 &world(&["/etc/reticulum/config", "/etc/reticulum/config.toml"]),
84 );
85 assert_eq!(configs.config, Some(PathBuf::from("/etc/reticulum/config")));
86 }
87}