use std::error::Error; use std::fmt::{Display, Formatter}; use std::path::PathBuf; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct FileNameInfo { pub name: String, pub version: Option, pub extension: Option, } impl FileNameInfo { pub fn to_full_file_name(&self) -> String { self.to_string() } } impl TryFrom for FileNameInfo { type Error = FileInfoError; fn try_from(file: PathBuf) -> Result { let file_name = file.file_name().ok_or(FileInfoError::NotAFile)?; let file_name = file_name .to_str() .ok_or(FileInfoError::InvalidCharactersInFileName)?; let (file_name_without_version, extension) = match file_name.rsplit_once('.').filter(|(_, ending)| { ending.parse::().is_err() //there are usually no file extensions which are just a number, but rather versions }) { Some((name, ending)) => (name, Some(ending.to_string())), None => (file_name, None), }; let (name, version) = match file_name_without_version.split_once('-') { Some((name, version)) => (name, Some(version.to_string())), None => (file_name_without_version, None), }; Ok(Self { name: name.to_string(), version, extension, }) } } impl Display for FileNameInfo { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}{}{}", self.name, self .version .as_ref() .map(|v| format!("-{v}")) .unwrap_or_default(), self .extension .as_ref() .map(|extension| format!(".{extension}")) .unwrap_or_default() ) } } #[derive(Debug)] pub enum FileInfoError { NotAFile, InvalidCharactersInFileName, } impl Display for FileInfoError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { FileInfoError::NotAFile => write!(f, "Path doesn't point to a file"), FileInfoError::InvalidCharactersInFileName => write!(f, "Invalid characters in file name"), } } } impl Error for FileInfoError {} #[derive(Debug, Clone)] pub struct FileMatcher { name: String, extension: Option, } impl FileMatcher { pub fn from(name: S) -> Self where S: ToString, { Self { name: name.to_string(), extension: None, } } pub fn and_extension(self, extension: S) -> Self where S: ToString, { Self { extension: Some(extension.to_string()), ..self } } pub fn matches(&self, file_name: &str) -> bool { file_name.starts_with(&self.name) && self .extension .as_ref() .is_none_or(|extension| file_name.ends_with(extension)) } } #[cfg(test)] mod test_file_name_info { use crate::file::FileNameInfo; use std::path::PathBuf; #[test] fn test_from_plugin() { assert_eq!( FileNameInfo { name: "TestPlugin".to_string(), version: Some("1.0.0".to_string()), extension: Some("jar".to_string()), }, FileNameInfo::try_from(PathBuf::from("test-ressources/files/TestPlugin-1.0.0.jar")) .expect("valid file") ); } #[test] fn test_from_unversioned() { assert_eq!( FileNameInfo { name: "unversioned".to_string(), version: None, extension: Some("jar".to_string()), }, FileNameInfo::try_from(PathBuf::from("test-ressources/files/unversioned.jar")) .expect("valid file") ); } #[test] fn test_from_versioned_bin() { assert_eq!( FileNameInfo { name: "bin".to_string(), version: Some("7.3".to_string()), extension: None, }, FileNameInfo::try_from(PathBuf::from("test-ressources/files/bin-7.3")).expect("valid file") ); } #[test] fn test_from_unversioned_bin() { assert_eq!( FileNameInfo { name: "test".to_string(), version: None, extension: None, }, FileNameInfo::try_from(PathBuf::from("test-ressources/files/test")).expect("valid file") ); } } #[cfg(test)] mod test_file_matcher { use crate::file::FileMatcher; #[test] fn test_match_with_extension() { let matcher = FileMatcher::from("test").and_extension(".jar"); assert!(matcher.matches("test.jar")); assert!(matcher.matches("test-1.3.0.jar")); assert!(!matcher.matches("test")); } #[test] fn test_match_without_extension() { let matcher = FileMatcher::from("test"); assert!(matcher.matches("test.jar")); assert!(matcher.matches("test-1.3.0.jar")); assert!(matcher.matches("test")); } }