dodot_lib/preprocessing/
identity.rs1use std::path::{Path, PathBuf};
8
9use crate::fs::Fs;
10use crate::preprocessing::{ExpandedFile, Preprocessor, TransformType};
11use crate::Result;
12
13pub struct IdentityPreprocessor {
18 extension: String,
19}
20
21impl IdentityPreprocessor {
22 pub fn new() -> Self {
24 Self {
25 extension: "identity".to_string(),
26 }
27 }
28
29 pub fn with_extension(ext: &str) -> Self {
31 Self {
32 extension: ext.to_string(),
33 }
34 }
35}
36
37impl Default for IdentityPreprocessor {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl Preprocessor for IdentityPreprocessor {
44 fn name(&self) -> &str {
45 "identity"
46 }
47
48 fn transform_type(&self) -> TransformType {
49 TransformType::Generative
50 }
51
52 fn matches_extension(&self, filename: &str) -> bool {
53 let suffix = format!(".{}", self.extension);
54 filename.ends_with(&suffix)
55 }
56
57 fn stripped_name(&self, filename: &str) -> String {
58 let suffix = format!(".{}", self.extension);
59 filename
60 .strip_suffix(&suffix)
61 .unwrap_or(filename)
62 .to_string()
63 }
64
65 fn expand(&self, source: &Path, fs: &dyn Fs) -> Result<Vec<ExpandedFile>> {
66 let content = fs.read_file(source)?;
67 let stripped =
68 self.stripped_name(&source.file_name().unwrap_or_default().to_string_lossy());
69
70 Ok(vec![ExpandedFile {
71 relative_path: PathBuf::from(stripped),
72 content,
73 is_dir: false,
74 }])
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn matches_identity_extension() {
84 let pp = IdentityPreprocessor::new();
85 assert!(pp.matches_extension("config.toml.identity"));
86 assert!(pp.matches_extension("aliases.sh.identity"));
87 assert!(!pp.matches_extension("config.toml"));
88 assert!(!pp.matches_extension("identity"));
89 assert!(!pp.matches_extension("config.identity.bak"));
90 }
91
92 #[test]
93 fn stripped_name_removes_extension() {
94 let pp = IdentityPreprocessor::new();
95 assert_eq!(pp.stripped_name("config.toml.identity"), "config.toml");
96 assert_eq!(pp.stripped_name("aliases.sh.identity"), "aliases.sh");
97 assert_eq!(pp.stripped_name("simple.identity"), "simple");
98 }
99
100 #[test]
101 fn custom_extension() {
102 let pp = IdentityPreprocessor::with_extension("test");
103 assert!(pp.matches_extension("config.toml.test"));
104 assert!(!pp.matches_extension("config.toml.identity"));
105 assert_eq!(pp.stripped_name("config.toml.test"), "config.toml");
106 }
107
108 #[test]
109 fn expand_returns_content_unchanged() {
110 let env = crate::testing::TempEnvironment::builder()
111 .pack("app")
112 .file("config.toml.identity", "host = localhost\nport = 5432")
113 .done()
114 .build();
115
116 let pp = IdentityPreprocessor::new();
117 let source = env.dotfiles_root.join("app/config.toml.identity");
118 let result = pp.expand(&source, env.fs.as_ref()).unwrap();
119
120 assert_eq!(result.len(), 1);
121 assert_eq!(result[0].relative_path, PathBuf::from("config.toml"));
122 assert_eq!(
123 String::from_utf8_lossy(&result[0].content),
124 "host = localhost\nport = 5432"
125 );
126 assert!(!result[0].is_dir);
127 }
128
129 #[test]
130 fn trait_properties() {
131 let pp = IdentityPreprocessor::new();
132 assert_eq!(pp.name(), "identity");
133 assert_eq!(pp.transform_type(), TransformType::Generative);
134 }
135
136 #[test]
137 fn expand_empty_file() {
138 let env = crate::testing::TempEnvironment::builder()
139 .pack("app")
140 .file("empty.conf.identity", "")
141 .done()
142 .build();
143
144 let pp = IdentityPreprocessor::new();
145 let source = env.dotfiles_root.join("app/empty.conf.identity");
146 let result = pp.expand(&source, env.fs.as_ref()).unwrap();
147
148 assert_eq!(result.len(), 1);
149 assert_eq!(result[0].relative_path, PathBuf::from("empty.conf"));
150 assert!(result[0].content.is_empty());
151 }
152
153 #[test]
154 fn expand_binary_content() {
155 let env = crate::testing::TempEnvironment::builder()
156 .pack("app")
157 .file("data.bin.identity", "")
158 .done()
159 .build();
160
161 let source = env.dotfiles_root.join("app/data.bin.identity");
163 let binary = vec![0u8, 1, 2, 255, 128, 64];
164 env.fs.write_file(&source, &binary).unwrap();
165
166 let pp = IdentityPreprocessor::new();
167 let result = pp.expand(&source, env.fs.as_ref()).unwrap();
168
169 assert_eq!(result.len(), 1);
170 assert_eq!(result[0].content, binary);
171 }
172
173 #[test]
174 fn expand_missing_file_returns_error() {
175 let env = crate::testing::TempEnvironment::builder().build();
176
177 let pp = IdentityPreprocessor::new();
178 let source = env.dotfiles_root.join("nonexistent.identity");
179 let err = pp.expand(&source, env.fs.as_ref());
180
181 assert!(err.is_err(), "expanding a missing file should fail");
182 }
183
184 #[test]
185 fn double_extension_only_strips_last() {
186 let pp = IdentityPreprocessor::new();
187 assert_eq!(pp.stripped_name("file.identity.identity"), "file.identity");
189 assert!(pp.matches_extension("file.identity.identity"));
190 }
191
192 #[test]
193 fn expand_is_idempotent() {
194 let env = crate::testing::TempEnvironment::builder()
195 .pack("app")
196 .file("config.toml.identity", "data")
197 .done()
198 .build();
199
200 let pp = IdentityPreprocessor::new();
201 let source = env.dotfiles_root.join("app/config.toml.identity");
202
203 let result1 = pp.expand(&source, env.fs.as_ref()).unwrap();
204 let result2 = pp.expand(&source, env.fs.as_ref()).unwrap();
205
206 assert_eq!(result1.len(), result2.len());
207 assert_eq!(result1[0].relative_path, result2[0].relative_path);
208 assert_eq!(result1[0].content, result2[0].content);
209 }
210}