use std::collections::HashMap;
use std::path::Path;
use anyhow::Result;
use super::manifest::{LangSpec, Manifest};
#[derive(Debug, Clone)]
pub struct GrammarRegistry {
manifest: Manifest,
by_ext: HashMap<String, String>,
}
impl GrammarRegistry {
pub fn new(manifest: Manifest) -> Self {
let mut by_ext: HashMap<String, String> = HashMap::new();
for (name, spec) in manifest.iter() {
for ext in &spec.extensions {
let key = ext.to_ascii_lowercase();
by_ext.entry(key).or_insert_with(|| name.to_string());
}
}
Self { manifest, by_ext }
}
pub fn embedded() -> Result<Self> {
let s = include_str!("../../bonsai.toml");
Ok(Self::new(Manifest::from_toml_str(s)?))
}
pub fn by_name(&self, name: &str) -> Option<&LangSpec> {
self.manifest.get(name)
}
pub fn detect_for_path(&self, path: &Path) -> Option<&LangSpec> {
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
let name = self.by_ext.get(&ext)?;
self.manifest.get(name)
}
pub fn name_for_path(&self, path: &Path) -> Option<&str> {
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
self.by_ext.get(&ext).map(|s| s.as_str())
}
pub fn manifest(&self) -> &Manifest {
&self.manifest
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn embedded() -> GrammarRegistry {
GrammarRegistry::embedded().expect("embedded manifest must build")
}
#[test]
fn rust_path_resolves() {
let r = embedded();
let spec = r.detect_for_path(&PathBuf::from("src/main.rs")).unwrap();
assert!(spec.git_url.contains("rust"));
}
#[test]
fn python_path_resolves() {
let r = embedded();
assert_eq!(
r.name_for_path(&PathBuf::from("foo/bar.py")),
Some("python")
);
}
#[test]
fn c_extension_picks_c_over_cpp() {
let r = embedded();
assert_eq!(r.name_for_path(&PathBuf::from("foo.c")), Some("c"));
}
#[test]
fn cpp_specific_extensions_still_route_to_cpp() {
let r = embedded();
assert_eq!(r.name_for_path(&PathBuf::from("foo.cpp")), Some("cpp"));
assert_eq!(r.name_for_path(&PathBuf::from("foo.h")), Some("cpp"));
}
#[test]
fn case_insensitive_extension() {
let r = embedded();
assert_eq!(
r.name_for_path(&PathBuf::from("README.MD")),
Some("markdown")
);
}
#[test]
fn unknown_extension_returns_none() {
let r = embedded();
assert!(r.detect_for_path(&PathBuf::from("foo.zzznope")).is_none());
}
#[test]
fn extensionless_returns_none() {
let r = embedded();
assert!(r.detect_for_path(&PathBuf::from("Makefile")).is_none());
}
#[test]
fn by_name_direct_lookup() {
let r = embedded();
assert!(r.by_name("rust").is_some());
assert!(r.by_name("definitely-not-a-language").is_none());
}
#[test]
fn handcrafted_alphabetical_precedence() {
let toml = r#"
[language.aaa]
git_url = "https://example/aaa"
git_rev = "1"
extensions = ["x"]
c_files = ["src/parser.c"]
query_dir = "queries"
[language.bbb]
git_url = "https://example/bbb"
git_rev = "2"
extensions = ["x"]
c_files = ["src/parser.c"]
query_dir = "queries"
"#;
let r = GrammarRegistry::new(Manifest::from_toml_str(toml).unwrap());
assert_eq!(r.name_for_path(&PathBuf::from("foo.x")), Some("aaa"));
}
}