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