wagon_codegen/
filestructure.rs

1
2use std::fmt::Display;
3use std::io::Write;
4use std::path::Path;
5use std::fs::{File, create_dir};
6
7use proc_macro2::TokenStream;
8
9/// A struct which represents a (sub)-filestructure.
10#[derive(Debug, Eq, PartialEq)]
11pub struct FileStructure {
12	path: String,
13	kind: FileType,
14}
15
16/// The types of file this filestructure can represent.
17///
18/// Because [`TokenStream`]s are kinda funky and they just get converted to strings
19/// for writing to disk anyway, they are not supported. Just convert them to string instead.
20#[derive(Debug, Eq, PartialEq)]
21pub enum FileType {
22	/// Write a specific string to a file.
23	String(String),
24	/// Write binary data to a file.
25	Blob(Box<[u8]>),
26	/// This is a directory which holds multiple other files.
27	Dir(Vec<FileStructure>)
28}
29
30impl Default for FileStructure {
31    fn default() -> Self {
32        Self::new(String::new(), FileType::Dir(vec![]))
33    }
34}
35
36impl FileStructure {
37	/// Basic constructor.
38	#[must_use] 
39	pub const fn new(path: String, kind: FileType) -> Self {
40		Self { path, kind }
41	}
42
43	/// Constructs a dir.
44	#[must_use]
45	pub const fn new_dir(name: String) -> Self {
46		Self { path: name, kind: FileType::Dir(Vec::new()) }
47	}
48
49	/// Given a unix path, splits it into it's separate components.
50	///
51	/// Will also check whether the path contains a starting `/`, returning `true` if so and `false` otherwise.
52	///
53	/// # Example
54	/// ```ignore
55	/// use wagon_codegen::FileStructure;
56	/// let s1 = "/some/path";
57	/// let s2 = "some/path";
58	/// 
59	/// assert_eq!(FileStructure::split_path(s1), (vec!["some".to_string(), "path".to_string()], true));
60	/// assert_eq!(FileStructure::split_path(s2), (vec!["some".to_string(), "path".to_string()], false));
61	/// ```
62	fn split_path(path: &str) -> (Vec<String>, bool) {
63		let mut iter = path.split('/');
64		let mut ret = Vec::new();
65		let is_root = iter.next().map_or(true, |s| 
66			if s.is_empty() {
67				true
68			} else {
69				ret.push(s.to_string());
70				false
71			});
72		for sub in iter {
73			if !sub.is_empty() {
74				ret.push(sub.to_string());
75			}
76		}
77		(ret, is_root)
78	}
79
80	fn _from_path(mut components: Vec<String>, is_root: bool) -> Self {
81		components.pop().map_or_else(|| Self::new(String::new(), FileType::Dir(vec![])), |leaf_path| {
82			let mut curr = Self::new(leaf_path, FileType::Dir(vec![]));
83			while let Some(sub_dir) = components.pop() {
84				let new = Self::new(sub_dir, FileType::Dir(vec![curr]));
85				curr = new;
86			}
87			if is_root {
88				Self::new(String::new(), FileType::Dir(vec![curr]))
89			} else {
90				curr
91			}
92		})
93	}
94
95	/// Constructs a `FileStructure` completely from a unix path.
96	///
97	/// If the path contains a starting `/`, will return a structure with a root node. If not, the root will be the first name of the path.
98	///
99	/// # Example
100	/// ```
101	/// use wagon_codegen::{FileStructure, FileType};
102	/// let s = "/some/path";
103	/// let fs = FileStructure::from_path(s);
104	/// let expected = FileStructure::new("".to_string(), 
105	///     FileType::Dir(vec![
106	///         FileStructure::new("some".to_string(),
107	///             FileType::Dir(vec![
108	///                 FileStructure::new("path".to_string(),
109	///                     FileType::Dir(vec![])
110	///                 )
111	///             ])
112	///         )
113	///     ])
114	/// );
115	/// assert_eq!(fs, expected);
116	#[must_use]
117	pub fn from_path(path: &str) -> Self {
118		let (components, is_root) = Self::split_path(path);
119		Self::_from_path(components, is_root)
120	}
121
122	/// Given a full `FileStructure` and a starting path, attempts to write it all to disk.
123	///
124	/// Directories are handled recursively. Everything else is converted to bytes and
125	/// written to a specific file.
126	///
127	/// # Errors
128	/// Errors whenever we fail to either write to a file or create a dir. See the
129	/// documentation for [`File::create`], [`Write::write_all`] and [`create_dir`] for more
130	/// info.
131	pub fn write_to_disk(&self, root: &Path) -> std::io::Result<()> {
132		let path = root.join(&self.path);
133		match &self.kind {
134		    FileType::String(s) => File::create(path)?.write_all(s.as_bytes())?,
135		    FileType::Blob(b) => File::create(path)?.write_all(b)?,
136		    FileType::Dir(fs) => {
137		    	if !path.exists() {
138		    		create_dir(path.clone())?;
139		    	}
140		    	for f in fs {
141		    		f.write_to_disk(&path)?;
142		    	}
143		    },
144		}
145		Ok(())
146	}
147
148	/// Tries finding a file in the structure by its full path.
149	///
150	/// Path should be written unix style. 
151	#[must_use] 
152	pub fn get(&self, path: &str) -> Option<&Self> {
153		let (components, is_root) = Self::split_path(path);
154		let stop = components.len() - 1;
155		let mut iter = components.iter().enumerate();
156		if let Some((_, component)) = iter.next() { // Handle the first bit
157			if &self.path != component {
158				return None
159			}
160		} else if self.path.is_empty() && is_root {
161			return Some(self)
162		} else {
163			return None
164		};
165		let mut curr = self;
166		for (i, component) in iter {
167			if let FileType::Dir(files) = &curr.kind {
168        		if let Some(next) = files.iter().find(|file| &file.path == component) {
169                    curr = next;
170                } else {
171                	return None;
172                }
173        	} else if i < stop { // We've reached the end of the tree but not the end of the path
174     			return None;
175     		}
176		}
177		Some(curr)
178	}
179
180	/// Tries finding a file in the structure by its full path, mutably
181	///
182	/// Path should be written unix style. 
183	#[must_use] 
184	pub fn get_mut(&mut self, path: &str) -> Option<&mut Self> {
185		let (components, is_root) = Self::split_path(path);
186		let stop = components.len() - 1;
187		let mut iter = components.iter().enumerate();
188		if let Some((_, component)) = iter.next() { // Handle the first bit
189			if &self.path != component {
190				return None
191			}
192		} else if self.path.is_empty() && is_root {
193			return Some(self)
194		} else {
195			return None
196		};
197		let mut curr = self;
198		for (i, component) in iter {
199			if let FileType::Dir(ref mut files) = curr.kind {
200        		if let Some(next) = files.iter_mut().find(|file| &file.path == component) {
201                    curr = next;
202                } else {
203                	return None;
204                }
205        	} else if i < stop { // We've reached the end of the tree but not the end of the path
206     			return None;
207     		} else {
208     			break; // This isn't needed for logic, but it is needed for the borrow checker.
209     		}
210		}
211		Some(curr)
212	}
213
214	fn _insert_dir(&mut self, components: Vec<String>) -> Option<&mut Self> {
215		let mut iter = components.into_iter();
216		let mut curr = if let Some(component) = iter.next() {
217			if self.path.is_empty() {
218				if let FileType::Dir(ref mut files) = self.kind {
219	        		if let Some(index) = files.iter().position(|file| file.path == component) { // Use the index to avoid borrow checker
220	                    &mut files[index]
221	                } else {
222	                	let subdir = Self::new_dir(component);
223						files.push(subdir);
224						files.last_mut()?
225	                }
226	        	} else { // We've reached the end of the tree but not the end of the path and it's not a dir.
227	     			return None;
228	     		}
229			} else if self.path != component {
230				if let FileType::Dir(ref mut files) = self.kind { // Duplicate check because borrow checker complains.
231					let subdir = Self::new_dir(component);
232					files.push(subdir);
233					files.last_mut()?
234				} else {
235					return None;
236				}
237			} else {
238				self
239			}
240		} else {
241			return Some(self)
242		};
243		for component in iter {
244			if let FileType::Dir(ref mut files) = curr.kind {
245        		if let Some(index) = files.iter().position(|file| file.path == component) { // Use the index to avoid borrow checker
246                    curr = &mut files[index];
247                } else {
248                	let subdir = Self::new_dir(component); // Need to keep making dirs until we find the end.
249					files.push(subdir);
250					curr = files.last_mut()?;
251                }
252        	} else { // We've reached the end of the tree but not the end of the path and it's not a dir.
253     			return None;
254     		}
255		}
256		Some(curr)
257	}
258
259	fn insert_data(&mut self, path: &str, data: FileType) -> Option<&mut Self> {
260		let (mut components, _) = Self::split_path(path);
261		let filename = components.pop()?;
262		let dir = self._insert_dir(components)?;
263		dir.insert(Self::new(filename, data))
264	}
265
266	/// Inserts a directory into the `FileStructure`. Returns a `None` if we fail, returns a mutable reference to the bottom directory if we succeed.
267	///
268	/// Functions like `mkdir -p`, meaning that it will automatically create directories as needed until the full path has been added.
269	pub fn insert_dir(&mut self, path: &str) -> Option<&mut Self> {
270		let (components, _) = Self::split_path(path);
271		self._insert_dir(components)
272	}
273
274	/// Insert binary blob data at some path relative to the root `FileStructure`.
275	///
276	/// Will automatically create directories if needed.
277	pub fn insert_blob(&mut self, path: &str, blob: Box<[u8]>) -> Option<&mut Self> {
278		self.insert_data(path, FileType::Blob(blob))
279	}
280
281	/// Insert a [`String`] at some path relative to the root `FileStructure`.
282	///
283	/// Will automatically create directories if needed.
284	pub fn insert_string(&mut self, path: &str, data: String) -> Option<&mut Self> {
285		self.insert_data(path, FileType::String(data))
286	}
287
288	/// Insert a [`TokenStream`] at some path relative to the root `FileStructure`.
289	/// 
290	/// The [`TokenStream`] will be converted to a [`String`]. If the `pretty` flag is set, the `TokenStream` will be prettified first.
291	///
292	/// Will automatically create directories if needed.
293	///
294	/// # Errors
295	/// Returns a [`syn::parse::Error`] if the `pretty` flag is set and we fail to parse the [`TokenStream`].
296	pub fn insert_tokenstream(&mut self, path: &str, data: TokenStream, pretty: bool) -> syn::parse::Result<Option<&mut Self>> {
297		let data = if pretty {
298			let ast: syn::File = syn::parse2(data)?;
299			FileType::String(prettyplease::unparse(&ast))
300		} else {
301			FileType::String(data.to_string())
302		};
303		self.insert_data(path, data).map_or(Ok(None), |dir| Ok(Some(dir)))
304	}
305
306	/// Insert a `FileStructure` as a child to this `FileStructure`.
307	///
308	/// Returns a `None` if this `FileStructure` is not a directory. Returns a mutable reference to the child otherwise.
309	pub fn insert(&mut self, child: Self) -> Option<&mut Self> {
310		if let FileType::Dir(ref mut files) = self.kind {
311			files.push(child);
312			Some(files.last_mut()?)
313		} else {
314			None
315		}
316	}
317
318	/// Get the path for this node of the structure.
319	#[must_use] 
320	pub fn get_path(&self) -> &str {
321		&self.path
322	}
323
324	/// Get the "len" (file count) of the structure.
325	///
326	/// TODO: Precalculate this instead of exploring the whole tree.
327	#[must_use]
328	pub fn len(&self) -> usize {
329		match &self.kind {
330		    FileType::Dir(files) => {
331		    	let mut sum = 0;
332		    	for file in files {
333		    		sum += file.len();
334		    	}
335		    	sum
336		    },
337		    _ => 1
338		}
339	}
340
341	/// A filestructure is deemed empty if there are 0 and 0 subdirs in it.
342	#[must_use]
343	pub fn is_empty(&self) -> bool {
344		self.len() == 0
345	}
346
347	/// Convert the `FileStructure` into a usable iterator.
348	///
349	/// This will iterate over all the nodes in a DFS style.
350	#[must_use]
351	pub fn iter(&self) -> FileIterator {
352		FileIterator { stack: vec![self] }
353	}
354
355}
356
357pub struct FileIterator<'a> {
358	stack: Vec<&'a FileStructure>
359}
360
361pub struct OwnedFileIterator {
362	stack: Vec<FileStructure>
363}
364
365impl<'a> Iterator for FileIterator<'a> {
366    type Item = &'a FileStructure;
367
368    fn next(&mut self) -> Option<Self::Item> {
369        let cont = self.stack.pop()?;
370        if let FileType::Dir(subdirs) = &cont.kind {
371        	self.stack.extend(subdirs.iter());
372        }
373        Some(cont)
374    }
375}
376
377impl Iterator for OwnedFileIterator {
378    type Item = FileStructure;
379
380    fn next(&mut self) -> Option<Self::Item> {
381        let mut cont = self.stack.pop()?;
382        if let FileType::Dir(ref mut subdirs) = cont.kind {
383        	self.stack.extend(std::mem::take(subdirs));
384        }
385        Some(cont)
386    }
387}
388
389impl IntoIterator for FileStructure {
390    type Item = Self;
391
392    type IntoIter = OwnedFileIterator;
393
394    fn into_iter(self) -> Self::IntoIter {
395        OwnedFileIterator { stack: vec![self] }
396    }
397}
398
399impl<'a> IntoIterator for &'a FileStructure {
400	type Item = Self;
401
402	type IntoIter = FileIterator<'a>;
403
404	fn into_iter(self) -> Self::IntoIter {
405        self.iter()
406    }
407}
408
409impl Display for FileStructure {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        match &self.kind {
412            FileType::Dir(files) => {
413            	writeln!(f, "{}: [", self.path)?;
414            	for file in files {
415            		writeln!(f, "{file},")?;
416            	}
417            	writeln!(f, "]")
418            },
419            _ => write!(f, "{}", self.path)
420        }
421    }
422}
423
424#[cfg(test)]
425mod test {
426	use super::{FileStructure, FileType};
427
428	#[test]
429	fn test_get() {
430		let structure = FileStructure {
431		    path: "some".to_string(),
432		    kind: FileType::Dir(vec![
433		        FileStructure {
434		            path: "dir".to_string(),
435		            kind: FileType::Dir(vec![
436		            	FileStructure {
437		            		path: "file.txt".to_string(),
438		            		kind: FileType::Blob([].into()),
439		            	},
440		            	FileStructure {
441		            		path: "other.txt".to_string(),
442		            		kind: FileType::Blob([].into()),
443		            	}
444		            ]),
445		        },
446		        FileStructure {
447		        	path: "other".to_string(),
448		        	kind: crate::FileType::Dir(vec![
449		        		FileStructure {
450		        			path: "file.txt".to_string(),
451		        			kind: FileType::Blob([].into()),
452		        		}
453		        	]),
454		        }
455		    ]),
456		};
457		let target = FileStructure {
458    		path: "file.txt".to_string(),
459    		kind: FileType::Blob([].into()),
460    	};
461    	assert_eq!(structure.get("some/dir/file.txt"), Some(&target));
462    	assert_eq!(structure.get("some/dir/none.txt"), None);
463    	assert_eq!(structure.get("nothing"), None);
464	}
465
466	#[test]
467	fn test_insert() {
468		let mut structure = FileStructure::from_path("some");
469		structure.insert_dir("some/dir/");
470		structure.insert_dir("some/other");
471		structure.insert_blob("some/dir/file.txt", [].into()).unwrap();
472		structure.insert_blob("some/dir/other.txt", [].into()).unwrap();
473		structure.insert_blob("some/other/file.txt", [].into()).unwrap();
474		let expected = FileStructure {
475		    path: "some".to_string(),
476		    kind: FileType::Dir(vec![
477		        FileStructure {
478		            path: "dir".to_string(),
479		            kind: FileType::Dir(vec![
480		            	FileStructure {
481		            		path: "file.txt".to_string(),
482		            		kind: FileType::Blob([].into()),
483		            	},
484		            	FileStructure {
485		            		path: "other.txt".to_string(),
486		            		kind: FileType::Blob([].into()),
487		            	}
488		            ]),
489		        },
490		        FileStructure {
491		        	path: "other".to_string(),
492		        	kind: crate::FileType::Dir(vec![
493		        		FileStructure {
494		        			path: "file.txt".to_string(),
495		        			kind: FileType::Blob([].into()),
496		        		}
497		        	]),
498		        }
499		    ]),
500		};
501		assert_eq!(structure, expected);
502	}
503
504	#[test]
505	fn test_insert_empty() {
506		let mut structure = FileStructure::default();
507		structure.insert_dir("some/dir/");
508		structure.insert_dir("some/other");
509		structure.insert_blob("some/dir/file.txt", [].into()).unwrap();
510		structure.insert_blob("some/dir/other.txt", [].into()).unwrap();
511		structure.insert_blob("some/other/file.txt", [].into()).unwrap();
512		let expected = FileStructure {
513			path: String::new(),
514			kind: FileType::Dir(vec![
515				FileStructure {
516					path: "some".to_string(),
517				    kind: FileType::Dir(vec![
518				        FileStructure {
519				            path: "dir".to_string(),
520				            kind: FileType::Dir(vec![
521				            	FileStructure {
522				            		path: "file.txt".to_string(),
523				            		kind: FileType::Blob([].into()),
524				            	},
525				            	FileStructure {
526				            		path: "other.txt".to_string(),
527				            		kind: FileType::Blob([].into()),
528				            	}
529				            ]),
530				        },
531				        FileStructure {
532				        	path: "other".to_string(),
533				        	kind: crate::FileType::Dir(vec![
534				        		FileStructure {
535				        			path: "file.txt".to_string(),
536				        			kind: FileType::Blob([].into()),
537				        		}
538				        	]),
539				        }
540				    ]),
541				}
542			])
543		};
544		assert_eq!(structure, expected);
545	}
546}