use anyhow::{Context, Result};
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize)]
pub struct Manifest {
pub link: Link,
#[serde(default)]
pub render: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Deserialize)]
pub struct Link {
#[serde(default)]
pub common: Vec<String>,
#[serde(default)]
pub darwin: Vec<String>,
#[serde(default)]
pub linux: Vec<String>,
#[serde(default)]
pub ignore: Vec<String>,
}
impl Manifest {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("failed to read manifest: {}", path.display()))?;
toml::from_str(&text)
.with_context(|| format!("failed to parse manifest: {}", path.display()))
}
pub fn targets(&self) -> Vec<&str> {
let mut out: Vec<&str> = self.link.common.iter().map(String::as_str).collect();
let os_specific = if cfg!(target_os = "macos") {
&self.link.darwin
} else {
&self.link.linux
};
out.extend(os_specific.iter().map(String::as_str));
out
}
pub fn is_ignored(&self, rel: &Path) -> bool {
self.link.ignore.iter().any(|pat| {
if let Some(ext) = pat.strip_prefix("*.") {
rel.extension().is_some_and(|e| e == ext)
} else {
rel.starts_with(pat)
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn manifest(ignore: &[&str]) -> Manifest {
Manifest {
link: Link {
common: vec![],
darwin: vec![],
linux: vec![],
ignore: ignore.iter().map(|s| s.to_string()).collect(),
},
render: Default::default(),
}
}
#[test]
fn ignores_by_extension_glob() {
let m = manifest(&["*.tmpl"]);
assert!(m.is_ignored(Path::new(".config/starship.toml.tmpl")));
assert!(!m.is_ignored(Path::new(".config/starship.toml")));
}
#[test]
fn ignores_by_path_prefix() {
let m = manifest(&[".config/secret"]);
assert!(m.is_ignored(Path::new(".config/secret/token")));
assert!(m.is_ignored(Path::new(".config/secret")));
assert!(!m.is_ignored(Path::new(".config/public/token")));
}
#[test]
fn empty_ignore_matches_nothing() {
let m = manifest(&[]);
assert!(!m.is_ignored(Path::new(".config/anything")));
}
}