use std::collections::HashMap;
use std::path::Path;
use super::{FileExtension, FileName, LanguageId, PatternTarget};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileKey {
Extension(FileExtension),
Name(FileName),
Unmappable,
}
impl FileKey {
#[must_use]
pub fn of(path: &Path) -> Self {
let by_name = || {
path.file_name()
.and_then(|name| name.to_str())
.and_then(|name| FileName::new(name).ok())
.map_or(Self::Unmappable, Self::Name)
};
path.extension().map_or_else(by_name, |extension| {
extension
.to_str()
.and_then(|extension| FileExtension::new(extension).ok())
.map_or(Self::Unmappable, Self::Extension)
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LanguageMap {
by_extension: HashMap<FileExtension, LanguageId>,
by_name: HashMap<FileName, LanguageId>,
}
impl LanguageMap {
pub fn insert(&mut self, target: PatternTarget, language: LanguageId) {
match target {
PatternTarget::Extension(extension) => {
self.by_extension.insert(extension, language);
}
PatternTarget::Name(name) => {
self.by_name.insert(name, language);
}
}
}
#[must_use]
pub fn detect(&self, path: &Path) -> LanguageId {
match FileKey::of(path) {
FileKey::Extension(extension) => self.by_extension.get(&extension),
FileKey::Name(name) => self.by_name.get(&name),
FileKey::Unmappable => None,
}
.cloned()
.unwrap_or(LanguageId::PLAINTEXT)
}
pub fn languages(&self) -> impl Iterator<Item = &LanguageId> {
self.by_extension.values().chain(self.by_name.values())
}
}
impl From<HashMap<FileExtension, LanguageId>> for LanguageMap {
fn from(by_extension: HashMap<FileExtension, LanguageId>) -> Self {
Self {
by_extension,
by_name: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn map() -> LanguageMap {
let mut map = LanguageMap::from(HashMap::from([
(
FileExtension::from_static("rs"),
LanguageId::from_static("rust"),
),
(
FileExtension::from_static("R"),
LanguageId::from_static("r"),
),
]));
map.insert(
PatternTarget::Name(FileName::from_static("Dockerfile")),
LanguageId::from_static("dockerfile"),
);
map
}
#[test]
fn test_detect_by_extension_and_name() {
let map = map();
assert_eq!(map.detect(Path::new("src/main.rs")), "rust");
assert_eq!(map.detect(Path::new("a/b/Dockerfile")), "dockerfile");
}
#[test]
fn test_detect_is_case_sensitive() {
let map = map();
assert_eq!(map.detect(Path::new("x.r")), LanguageId::PLAINTEXT);
assert_eq!(map.detect(Path::new("x.R")), "r");
assert_eq!(map.detect(Path::new("dockerfile")), LanguageId::PLAINTEXT);
}
#[test]
fn test_detect_falls_back_to_plaintext() {
let map = map();
for unmapped in ["x.unknown", "Makefile", ".gitignore", "x.", "x.r\u{e9}"] {
assert_eq!(map.detect(Path::new(unmapped)), LanguageId::PLAINTEXT);
}
}
#[test]
fn test_a_file_with_an_extension_never_matches_a_name() {
let map = map();
assert_eq!(
map.detect(Path::new("Dockerfile.bak")),
LanguageId::PLAINTEXT
);
}
#[test]
fn test_file_key_of() {
assert_eq!(
FileKey::of(Path::new("a.tar.gz")),
FileKey::Extension(FileExtension::from_static("gz"))
);
assert_eq!(
FileKey::of(Path::new("Makefile")),
FileKey::Name(FileName::from_static("Makefile"))
);
for unmappable in [".gitignore", "x.", "r\u{e9}", "a.\u{e9}", "", ".."] {
assert_eq!(FileKey::of(Path::new(unmappable)), FileKey::Unmappable);
}
}
#[test]
fn test_languages_lists_both_maps() {
let map = map();
let languages: Vec<&LanguageId> = map.languages().collect();
assert_eq!(languages.len(), 3);
assert!(languages.contains(&&LanguageId::from_static("dockerfile")));
}
}