]>
Commit | Line | Data |
---|---|---|
1e2d00b6 RBR |
1 | mod file_strategies; |
2 | ||
3 | use file_strategies::file::Strategy as FileStrategy; | |
102a4884 RBR |
4 | use file_strategies::gemini::Strategy as GeminiStrategy; |
5 | use file_strategies::layout::Strategy as LayoutStrategy; | |
6 | ||
1e2d00b6 | 7 | use std::path::PathBuf; |
4fd89b80 | 8 | use std::fs::read_to_string; |
1e2d00b6 RBR |
9 | |
10 | pub struct FileHandler { | |
4fd89b80 RBR |
11 | pub strategies: Vec<Box<dyn FileHandlerStrategy>>, |
12 | pub layout: Option<String> | |
1e2d00b6 RBR |
13 | } |
14 | ||
15 | impl Default for FileHandler { | |
16 | fn default() -> FileHandler { | |
17 | FileHandler { | |
102a4884 RBR |
18 | strategies: vec![ |
19 | Box::new(GeminiStrategy{}), | |
20 | Box::new(LayoutStrategy{}), | |
21 | Box::new(FileStrategy{}), | |
4fd89b80 RBR |
22 | ], |
23 | layout: None | |
1e2d00b6 RBR |
24 | } |
25 | } | |
26 | } | |
27 | ||
28 | impl FileHandler { | |
29 | pub fn identify(&self, path: &PathBuf) -> FileType { | |
30 | for strategy in self.strategies.iter() { | |
31 | if strategy.is(&path) { | |
32 | return strategy.identify(); | |
33 | } | |
34 | } | |
35 | FileType::Unknown | |
36 | } | |
37 | ||
260e8ec6 | 38 | pub fn get_layout_or_panic(&mut self, files: &[File]) -> Result<(), &str> { |
4fd89b80 RBR |
39 | for file in files { |
40 | match file.file_type { | |
41 | FileType::Layout => { | |
42 | let layout_text = read_to_string(&file.path).unwrap(); | |
43 | self.layout = Some(layout_text); | |
48ea9080 | 44 | return Ok(()); |
4fd89b80 RBR |
45 | }, |
46 | _ => {} | |
47 | } | |
48 | } | |
48ea9080 | 49 | Err("No layout found. Please ensure there's a _layout.html file at the root") |
4fd89b80 RBR |
50 | } |
51 | ||
260e8ec6 RBR |
52 | pub fn handle_all(&self, source: &PathBuf, html_destination: &PathBuf, gemini_destination: &PathBuf, files: &[File]) { |
53 | files.iter().for_each(|file| { | |
dd0a540c | 54 | self.handle(source, html_destination, gemini_destination, file); |
260e8ec6 | 55 | }); |
4fd89b80 RBR |
56 | } |
57 | ||
dd0a540c | 58 | pub fn handle(&self, source: &PathBuf, html_destination: &PathBuf, gemini_destination: &PathBuf, file: &File) { |
260e8ec6 RBR |
59 | if let Some(strategy) = self.strategies |
60 | .iter() | |
61 | .find(|s| s.can_handle(&file.file_type)) | |
62 | { | |
63 | let layout = self.layout.as_ref() | |
64 | .expect("Layout should be initialized before handling files"); | |
dd0a540c RBR |
65 | strategy.handle_html(source, html_destination, file, layout); |
66 | strategy.handle_gemini(source, gemini_destination, file); | |
67 | return; | |
1e2d00b6 RBR |
68 | } |
69 | } | |
70 | } | |
71 | ||
72 | pub trait FileHandlerStrategy { | |
73 | fn is(&self, path: &PathBuf) -> bool; | |
74 | fn identify(&self) -> FileType; | |
4fd89b80 | 75 | fn can_handle(&self, file_type: &FileType) -> bool; |
260e8ec6 | 76 | fn handle_html(&self, source: &PathBuf, destination: &PathBuf, file: &File, layout: &str); |
dd0a540c | 77 | fn handle_gemini(&self, source: &PathBuf, destination: &PathBuf, file: &File); |
1e2d00b6 RBR |
78 | } |
79 | ||
260e8ec6 | 80 | #[derive(Debug, Clone, PartialEq)] |
1e2d00b6 RBR |
81 | pub enum FileType { |
82 | Gemini, | |
83 | File, | |
84 | Layout, | |
85 | Unknown, | |
86 | } | |
87 | ||
260e8ec6 | 88 | #[derive(PartialEq, Debug)] |
1e2d00b6 RBR |
89 | pub struct File { |
90 | pub path: PathBuf, | |
91 | pub file_type: FileType, | |
92 | } | |
260e8ec6 RBR |
93 | |
94 | ||
95 | #[cfg(test)] | |
96 | mod tests { | |
260e8ec6 RBR |
97 | use std::path::PathBuf; |
98 | ||
ad021007 RBR |
99 | use super::*; |
100 | ||
101 | use test_utilities::*; | |
102 | ||
103 | fn create_test_internal_file(path: &str, file_type: FileType) -> File { | |
260e8ec6 RBR |
104 | File { |
105 | path: PathBuf::from(path), | |
106 | file_type, | |
107 | } | |
108 | } | |
109 | ||
110 | #[test] | |
111 | fn test_identify_gemini_file() { | |
112 | let handler = FileHandler::default(); | |
113 | let path = PathBuf::from("test.gmi"); | |
114 | assert!(matches!(handler.identify(&path), FileType::Gemini)); | |
115 | } | |
116 | ||
117 | #[test] | |
118 | fn test_identify_layout_file() { | |
119 | let handler = FileHandler::default(); | |
120 | let path = PathBuf::from("_layout.html"); | |
121 | assert!(matches!(handler.identify(&path), FileType::Layout)); | |
122 | } | |
123 | ||
124 | #[test] | |
125 | fn test_identify_regular_file() { | |
126 | let handler = FileHandler::default(); | |
127 | let path = PathBuf::from("regular.html"); | |
128 | assert!(matches!(handler.identify(&path), FileType::File)); | |
129 | } | |
130 | ||
131 | #[test] | |
132 | fn test_identify_unknown_file() { | |
133 | let handler = FileHandler::default(); | |
ad021007 | 134 | let path = PathBuf::from("tests"); |
260e8ec6 RBR |
135 | assert!(matches!(handler.identify(&path), FileType::Unknown)); |
136 | } | |
137 | ||
138 | #[test] | |
139 | fn test_get_layout_success() { | |
ad021007 RBR |
140 | let test_dir = setup_test_dir(); |
141 | let layout_path = test_dir.join("_layout.html"); | |
142 | create_test_file(&layout_path, ""); | |
143 | ||
260e8ec6 RBR |
144 | let mut handler = FileHandler::default(); |
145 | let files = vec![ | |
ad021007 RBR |
146 | create_test_internal_file("test.gmi", FileType::Gemini), |
147 | create_test_internal_file(&layout_path.to_str().expect("Could not encode layout"), FileType::Layout), | |
148 | create_test_internal_file("regular.html", FileType::File), | |
260e8ec6 RBR |
149 | ]; |
150 | ||
151 | assert!(handler.get_layout_or_panic(&files).is_ok()); | |
152 | } | |
153 | ||
154 | #[test] | |
155 | fn test_get_layout_failure() { | |
156 | let mut handler = FileHandler::default(); | |
157 | let files = vec![ | |
ad021007 RBR |
158 | create_test_internal_file("test.gmi", FileType::Gemini), |
159 | create_test_internal_file("regular.html", FileType::File), | |
260e8ec6 RBR |
160 | ]; |
161 | ||
162 | assert!(handler.get_layout_or_panic(&files).is_err()); | |
163 | } | |
164 | ||
165 | // Mock strategy for testing | |
166 | struct MockStrategy { | |
167 | is_match: bool, | |
168 | file_type: FileType, | |
169 | } | |
170 | ||
171 | impl FileHandlerStrategy for MockStrategy { | |
172 | fn is(&self, _path: &PathBuf) -> bool { | |
173 | self.is_match | |
174 | } | |
175 | ||
176 | fn identify(&self) -> FileType { | |
177 | self.file_type.clone() | |
178 | } | |
179 | ||
180 | fn can_handle(&self, file_type: &FileType) -> bool { | |
181 | &self.file_type == file_type | |
182 | } | |
183 | ||
184 | fn handle_html(&self, _source: &PathBuf, _destination: &PathBuf, _file: &File, _layout: &str) {} | |
185 | fn handle_gemini(&self, _source: &PathBuf, _destination: &PathBuf, _file: &File) {} | |
186 | } | |
187 | ||
188 | #[test] | |
189 | fn test_custom_strategy() { | |
190 | let mock_strategy = MockStrategy { | |
191 | is_match: true, | |
192 | file_type: FileType::Gemini, | |
193 | }; | |
194 | ||
195 | let handler = FileHandler { | |
196 | strategies: vec![Box::new(mock_strategy)], | |
197 | layout: None, | |
198 | }; | |
199 | ||
200 | let path = PathBuf::from("test.whatever"); | |
201 | assert!(matches!(handler.identify(&path), FileType::Gemini)); | |
202 | } | |
203 | ||
204 | #[test] | |
205 | fn test_handle_all_empty_files() { | |
206 | let handler = FileHandler::default(); | |
207 | let files: Vec<File> = vec![]; | |
208 | ||
209 | // Should not panic with empty vector | |
210 | handler.handle_all( | |
211 | &PathBuf::from("source"), | |
212 | &PathBuf::from("tests/fixtures/output"), | |
213 | &PathBuf::from("tests/fixtures/output"), | |
214 | &files | |
215 | ); | |
216 | } | |
217 | ||
218 | #[test] | |
219 | fn test_handle_with_layout() { | |
220 | let mut handler = FileHandler::default(); | |
221 | handler.layout = Some("test layout".to_string()); | |
222 | ||
ad021007 | 223 | let file = create_test_internal_file("tests/fixtures/test1.gmi", FileType::Gemini); |
260e8ec6 RBR |
224 | |
225 | // Should not panic with valid layout | |
226 | handler.handle( | |
227 | &PathBuf::from(""), | |
228 | &PathBuf::from("tests/fixtures/output"), | |
229 | &PathBuf::from("tests/fixtures/output"), | |
230 | &file | |
231 | ); | |
232 | } | |
233 | ||
234 | #[test] | |
235 | #[should_panic(expected = "Layout should be initialized before handling files")] | |
236 | fn test_handle_without_layout() { | |
237 | let handler = FileHandler::default(); | |
ad021007 | 238 | let file = create_test_internal_file("test.gmi", FileType::Gemini); |
260e8ec6 RBR |
239 | |
240 | handler.handle( | |
241 | &PathBuf::from("source"), | |
242 | &PathBuf::from("tests/fixtures/output"), | |
243 | &PathBuf::from("tests/fixtures/output"), | |
244 | &file | |
245 | ); | |
246 | } | |
247 | ||
248 | #[test] | |
249 | fn test_slice_handling() { | |
ad021007 RBR |
250 | let test_dir = setup_test_dir(); |
251 | let layout_path = test_dir.join("_layout.html"); | |
252 | create_test_file(&layout_path, ""); | |
253 | ||
260e8ec6 RBR |
254 | let mut handler = FileHandler::default(); |
255 | let files = vec![ | |
ad021007 RBR |
256 | create_test_internal_file("tests/fixtures/test1.gmi", FileType::Gemini), |
257 | create_test_internal_file(&layout_path.to_str().expect("Could not encode layout"), FileType::Layout), | |
258 | create_test_internal_file("tests/fixtures/test2.gmi", FileType::Gemini), | |
259 | create_test_internal_file("tests/fixtures/test3.gmi", FileType::Gemini), | |
260e8ec6 RBR |
260 | ]; |
261 | ||
262 | let _ = handler.get_layout_or_panic(&files[1..]); | |
263 | ||
264 | // Test with slice | |
265 | handler.handle_all( | |
266 | &PathBuf::from(""), | |
267 | &PathBuf::from("tests/fixtures/output"), | |
268 | &PathBuf::from("tests/fixtures/output"), | |
269 | &files[1..] // Test with slice of last three elements | |
270 | ); | |
271 | } | |
272 | } |