1use camino::Utf8Path;
20use serde::Deserialize;
21
22use crate::{Error, Result};
23
24#[derive(Debug, Clone)]
25pub enum MarkerSpec {
26 PassThrough,
28 Override { links: Vec<MarkerLink> },
31}
32
33#[derive(Debug, Clone, Deserialize)]
34pub struct MarkerLink {
35 pub dst: String,
36 #[serde(default)]
37 pub when: Option<String>,
38}
39
40#[derive(Deserialize)]
41struct MarkerFile {
42 #[serde(default)]
43 link: Vec<MarkerLink>,
44}
45
46pub fn read_spec(dir: &Utf8Path, marker_filename: &str) -> Result<Option<MarkerSpec>> {
54 let path = dir.join(marker_filename);
55 let raw = match std::fs::read_to_string(&path) {
56 Ok(s) => s,
57 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
58 Err(e) => return Err(Error::Io(e)),
59 };
60 if raw.trim().is_empty() {
61 return Ok(Some(MarkerSpec::PassThrough));
62 }
63 let parsed: MarkerFile =
64 toml::from_str(&raw).map_err(|e| Error::Config(format!("parse {path}: {e}")))?;
65 if parsed.link.is_empty() {
66 return Ok(Some(MarkerSpec::PassThrough));
67 }
68 Ok(Some(MarkerSpec::Override { links: parsed.link }))
69}
70
71pub fn is_marker_dir(dir: &Utf8Path, marker_filename: &str) -> bool {
74 dir.join(marker_filename).is_file()
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use camino::Utf8PathBuf;
81 use tempfile::TempDir;
82
83 fn root(tmp: &TempDir) -> Utf8PathBuf {
84 Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap()
85 }
86
87 #[test]
88 fn no_marker_returns_none() {
89 let tmp = TempDir::new().unwrap();
90 assert!(read_spec(&root(&tmp), ".yuilink").unwrap().is_none());
91 }
92
93 #[test]
94 fn empty_marker_is_passthrough() {
95 let tmp = TempDir::new().unwrap();
96 std::fs::write(tmp.path().join(".yuilink"), "").unwrap();
97 assert!(matches!(
98 read_spec(&root(&tmp), ".yuilink").unwrap(),
99 Some(MarkerSpec::PassThrough)
100 ));
101 }
102
103 #[test]
104 fn whitespace_only_marker_is_passthrough() {
105 let tmp = TempDir::new().unwrap();
106 std::fs::write(tmp.path().join(".yuilink"), " \n\n").unwrap();
107 assert!(matches!(
108 read_spec(&root(&tmp), ".yuilink").unwrap(),
109 Some(MarkerSpec::PassThrough)
110 ));
111 }
112
113 #[test]
114 fn marker_with_links_is_override() {
115 let tmp = TempDir::new().unwrap();
116 std::fs::write(
117 tmp.path().join(".yuilink"),
118 r#"
119[[link]]
120dst = "/a"
121
122[[link]]
123dst = "/b"
124when = "{{ yui.os == 'windows' }}"
125"#,
126 )
127 .unwrap();
128 let spec = read_spec(&root(&tmp), ".yuilink").unwrap().unwrap();
129 match spec {
130 MarkerSpec::Override { links } => {
131 assert_eq!(links.len(), 2);
132 assert_eq!(links[0].dst, "/a");
133 assert!(links[0].when.is_none());
134 assert_eq!(links[1].dst, "/b");
135 assert_eq!(links[1].when.as_deref(), Some("{{ yui.os == 'windows' }}"));
136 }
137 _ => panic!("expected Override"),
138 }
139 }
140
141 #[test]
142 fn marker_with_invalid_toml_errors() {
143 let tmp = TempDir::new().unwrap();
144 std::fs::write(tmp.path().join(".yuilink"), "this is not toml ===").unwrap();
145 let err = read_spec(&root(&tmp), ".yuilink").unwrap_err();
146 assert!(format!("{err}").contains("parse"));
147 }
148
149 #[test]
150 fn empty_link_array_is_passthrough() {
151 let tmp = TempDir::new().unwrap();
153 std::fs::write(tmp.path().join(".yuilink"), "# no links\n").unwrap();
154 assert!(matches!(
155 read_spec(&root(&tmp), ".yuilink").unwrap(),
156 Some(MarkerSpec::PassThrough)
157 ));
158 }
159}