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 tracked_render: None,
75 context_hash: None,
76 }])
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn matches_identity_extension() {
86 let pp = IdentityPreprocessor::new();
87 assert!(pp.matches_extension("config.toml.identity"));
88 assert!(pp.matches_extension("aliases.sh.identity"));
89 assert!(!pp.matches_extension("config.toml"));
90 assert!(!pp.matches_extension("identity"));
91 assert!(!pp.matches_extension("config.identity.bak"));
92 }
93
94 #[test]
95 fn stripped_name_removes_extension() {
96 let pp = IdentityPreprocessor::new();
97 assert_eq!(pp.stripped_name("config.toml.identity"), "config.toml");
98 assert_eq!(pp.stripped_name("aliases.sh.identity"), "aliases.sh");
99 assert_eq!(pp.stripped_name("simple.identity"), "simple");
100 }
101
102 #[test]
103 fn custom_extension() {
104 let pp = IdentityPreprocessor::with_extension("test");
105 assert!(pp.matches_extension("config.toml.test"));
106 assert!(!pp.matches_extension("config.toml.identity"));
107 assert_eq!(pp.stripped_name("config.toml.test"), "config.toml");
108 }
109
110 #[test]
111 fn expand_returns_content_unchanged() {
112 let env = crate::testing::TempEnvironment::builder()
113 .pack("app")
114 .file("config.toml.identity", "host = localhost\nport = 5432")
115 .done()
116 .build();
117
118 let pp = IdentityPreprocessor::new();
119 let source = env.dotfiles_root.join("app/config.toml.identity");
120 let result = pp.expand(&source, env.fs.as_ref()).unwrap();
121
122 assert_eq!(result.len(), 1);
123 assert_eq!(result[0].relative_path, PathBuf::from("config.toml"));
124 assert_eq!(
125 String::from_utf8_lossy(&result[0].content),
126 "host = localhost\nport = 5432"
127 );
128 assert!(!result[0].is_dir);
129 }
130
131 #[test]
132 fn trait_properties() {
133 let pp = IdentityPreprocessor::new();
134 assert_eq!(pp.name(), "identity");
135 assert_eq!(pp.transform_type(), TransformType::Generative);
136 }
137
138 #[test]
139 fn expand_empty_file() {
140 let env = crate::testing::TempEnvironment::builder()
141 .pack("app")
142 .file("empty.conf.identity", "")
143 .done()
144 .build();
145
146 let pp = IdentityPreprocessor::new();
147 let source = env.dotfiles_root.join("app/empty.conf.identity");
148 let result = pp.expand(&source, env.fs.as_ref()).unwrap();
149
150 assert_eq!(result.len(), 1);
151 assert_eq!(result[0].relative_path, PathBuf::from("empty.conf"));
152 assert!(result[0].content.is_empty());
153 }
154
155 #[test]
156 fn expand_binary_content() {
157 let env = crate::testing::TempEnvironment::builder()
158 .pack("app")
159 .file("data.bin.identity", "")
160 .done()
161 .build();
162
163 let source = env.dotfiles_root.join("app/data.bin.identity");
165 let binary = vec![0u8, 1, 2, 255, 128, 64];
166 env.fs.write_file(&source, &binary).unwrap();
167
168 let pp = IdentityPreprocessor::new();
169 let result = pp.expand(&source, env.fs.as_ref()).unwrap();
170
171 assert_eq!(result.len(), 1);
172 assert_eq!(result[0].content, binary);
173 }
174
175 #[test]
176 fn expand_missing_file_returns_error() {
177 let env = crate::testing::TempEnvironment::builder().build();
178
179 let pp = IdentityPreprocessor::new();
180 let source = env.dotfiles_root.join("nonexistent.identity");
181 let err = pp.expand(&source, env.fs.as_ref());
182
183 assert!(err.is_err(), "expanding a missing file should fail");
184 }
185
186 #[test]
187 fn double_extension_only_strips_last() {
188 let pp = IdentityPreprocessor::new();
189 assert_eq!(pp.stripped_name("file.identity.identity"), "file.identity");
191 assert!(pp.matches_extension("file.identity.identity"));
192 }
193
194 #[test]
195 fn expand_is_idempotent() {
196 let env = crate::testing::TempEnvironment::builder()
197 .pack("app")
198 .file("config.toml.identity", "data")
199 .done()
200 .build();
201
202 let pp = IdentityPreprocessor::new();
203 let source = env.dotfiles_root.join("app/config.toml.identity");
204
205 let result1 = pp.expand(&source, env.fs.as_ref()).unwrap();
206 let result2 = pp.expand(&source, env.fs.as_ref()).unwrap();
207
208 assert_eq!(result1.len(), result2.len());
209 assert_eq!(result1[0].relative_path, result2[0].relative_path);
210 assert_eq!(result1[0].content, result2[0].content);
211 }
212}