use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct PeerConfig {
#[serde(default)]
pub peer: HashMap<String, PeerEntry>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PeerEntry {
pub path: PathBuf,
#[serde(default)]
pub rig: Option<String>,
}
#[derive(Debug, Error)]
pub enum PeerConfigError {
#[error("IO error reading {path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
#[error("TOML parse error in {path}: {source}")]
Parse {
path: String,
source: toml::de::Error,
},
}
impl PeerConfig {
pub fn load(qed_dir: &Path) -> Result<Self, PeerConfigError> {
let path = qed_dir.join("peers.toml");
if !path.exists() {
return Ok(Self::default());
}
let src = fs::read_to_string(&path).map_err(|e| PeerConfigError::Io {
path: path.display().to_string(),
source: e,
})?;
toml::from_str(&src).map_err(|e| PeerConfigError::Parse {
path: path.display().to_string(),
source: e,
})
}
pub fn get(&self, camp: &str) -> Option<&PeerEntry> {
self.peer.get(camp)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_file_yields_empty_config() {
let tmp = tempfile::tempdir().unwrap();
let cfg = PeerConfig::load(tmp.path()).unwrap();
assert!(cfg.peer.is_empty());
assert!(cfg.get("anyone").is_none());
}
#[test]
fn parses_local_and_remote_entries() {
let tmp = tempfile::tempdir().unwrap();
let src = r#"
[peer.mesofact]
path = "external/mesofact"
[peer.cheers]
path = "external/cheers"
[peer.bigbuild]
rig = "rig-tokyo-1"
path = "/srv/camps/bigbuild"
"#;
fs::write(tmp.path().join("peers.toml"), src).unwrap();
let cfg = PeerConfig::load(tmp.path()).unwrap();
assert_eq!(cfg.peer.len(), 3);
let meso = cfg.get("mesofact").unwrap();
assert_eq!(meso.path, PathBuf::from("external/mesofact"));
assert!(meso.rig.is_none());
let big = cfg.get("bigbuild").unwrap();
assert_eq!(big.rig.as_deref(), Some("rig-tokyo-1"));
assert_eq!(big.path, PathBuf::from("/srv/camps/bigbuild"));
}
#[test]
fn malformed_toml_surfaces_parse_error() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("peers.toml"), "not = valid = toml").unwrap();
let err = PeerConfig::load(tmp.path()).unwrap_err();
assert!(matches!(err, PeerConfigError::Parse { .. }), "got: {err:?}");
}
#[test]
fn entry_without_path_is_a_parse_error() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("peers.toml"),
"[peer.broken]\nrig = \"some-rig\"\n",
)
.unwrap();
let err = PeerConfig::load(tmp.path()).unwrap_err();
assert!(matches!(err, PeerConfigError::Parse { .. }), "got: {err:?}");
}
}