use std::collections::HashMap;
use std::path::Path;
use std::sync::OnceLock;
use crate::lang::traits::{BoxedLanguage, Language};
use crate::lang::{c, cpp, go, java, python, rust_lang, typescript};
static REGISTRY: OnceLock<LanguageRegistry> = OnceLock::new();
pub struct LanguageRegistry {
by_name: HashMap<&'static str, BoxedLanguage>,
by_ext: HashMap<&'static str, &'static str>,
aliases: HashMap<&'static str, &'static str>,
}
impl LanguageRegistry {
pub fn global() -> &'static Self {
REGISTRY.get_or_init(Self::new)
}
fn new() -> Self {
let mut registry = Self {
by_name: HashMap::new(),
by_ext: HashMap::new(),
aliases: HashMap::new(),
};
registry.register(Box::new(python::Python));
registry.register(Box::new(typescript::TypeScript::tsx()));
registry.register(Box::new(typescript::TypeScript::new()));
registry.register(Box::new(go::Go));
registry.register(Box::new(rust_lang::Rust));
registry.register(Box::new(java::Java));
registry.register(Box::new(c::C));
registry.register(Box::new(cpp::Cpp));
registry.register_alias("javascript", "typescript");
registry.register_alias("js", "typescript");
registry.register_alias("ts", "typescript");
registry.register_alias("jsx", "tsx");
registry
}
fn register_alias(&mut self, alias: &'static str, target: &'static str) {
self.aliases.insert(alias, target);
}
fn register(&mut self, lang: BoxedLanguage) {
let name = lang.name();
for ext in lang.extensions() {
self.by_ext.insert(*ext, name);
}
self.by_name.insert(name, lang);
}
pub fn get_by_name(&self, name: &str) -> Option<&dyn Language> {
let canonical_name = self.aliases.get(name).copied().unwrap_or(name);
self.by_name.get(canonical_name).map(|b| b.as_ref())
}
pub fn get_by_extension(&self, ext: &str) -> Option<&dyn Language> {
self.by_ext.get(ext).and_then(|name| self.get_by_name(name))
}
pub fn detect_language(&self, path: &Path) -> Option<&dyn Language> {
path.extension()
.and_then(|e| e.to_str())
.map(|ext| format!(".{}", ext))
.and_then(|ext| self.get_by_extension(&ext))
}
#[allow(dead_code)]
pub fn supported_languages(&self) -> Vec<&'static str> {
self.by_name.keys().copied().collect()
}
#[allow(dead_code)]
pub fn supported_languages_with_aliases(&self) -> Vec<&'static str> {
let mut names: Vec<&'static str> = self.by_name.keys().copied().collect();
names.extend(self.aliases.keys().copied());
names.sort_unstable();
names
}
#[allow(dead_code)]
pub fn is_supported(&self, name: &str) -> bool {
self.by_name.contains_key(name) || self.aliases.contains_key(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_by_name_canonical() {
let registry = LanguageRegistry::global();
assert!(registry.get_by_name("python").is_some());
assert!(registry.get_by_name("typescript").is_some());
assert!(registry.get_by_name("tsx").is_some());
assert!(registry.get_by_name("go").is_some());
assert!(registry.get_by_name("rust").is_some());
}
#[test]
fn test_get_by_name_javascript_alias() {
let registry = LanguageRegistry::global();
let js_lang = registry.get_by_name("javascript");
assert!(js_lang.is_some(), "javascript alias should be supported");
let ts_lang = registry.get_by_name("typescript");
assert!(ts_lang.is_some());
assert_eq!(js_lang.unwrap().name(), ts_lang.unwrap().name());
}
#[test]
fn test_get_by_name_shorthand_aliases() {
let registry = LanguageRegistry::global();
assert!(registry.get_by_name("js").is_some());
assert!(registry.get_by_name("ts").is_some());
assert!(registry.get_by_name("jsx").is_some());
assert_eq!(registry.get_by_name("js").unwrap().name(), "typescript");
assert_eq!(registry.get_by_name("ts").unwrap().name(), "typescript");
assert_eq!(registry.get_by_name("jsx").unwrap().name(), "tsx");
}
#[test]
fn test_is_supported_includes_aliases() {
let registry = LanguageRegistry::global();
assert!(registry.is_supported("python"));
assert!(registry.is_supported("typescript"));
assert!(registry.is_supported("tsx"));
assert!(registry.is_supported("javascript"));
assert!(registry.is_supported("js"));
assert!(registry.is_supported("ts"));
assert!(registry.is_supported("jsx"));
assert!(!registry.is_supported("brainfuck"));
assert!(!registry.is_supported("cobol"));
}
#[test]
fn test_supported_languages_with_aliases() {
let registry = LanguageRegistry::global();
let all_names = registry.supported_languages_with_aliases();
assert!(all_names.contains(&"python"));
assert!(all_names.contains(&"typescript"));
assert!(all_names.contains(&"tsx"));
assert!(all_names.contains(&"javascript"));
assert!(all_names.contains(&"js"));
assert!(all_names.contains(&"ts"));
assert!(all_names.contains(&"jsx"));
}
#[test]
fn test_supported_languages_excludes_aliases() {
let registry = LanguageRegistry::global();
let canonical_names = registry.supported_languages();
assert!(canonical_names.contains(&"python"));
assert!(canonical_names.contains(&"typescript"));
assert!(canonical_names.contains(&"tsx"));
assert!(!canonical_names.contains(&"javascript"));
assert!(!canonical_names.contains(&"js"));
assert!(!canonical_names.contains(&"ts"));
assert!(!canonical_names.contains(&"jsx"));
}
#[test]
fn test_get_by_extension_js() {
let registry = LanguageRegistry::global();
let js_lang = registry.get_by_extension(".js");
assert!(js_lang.is_some());
assert_eq!(js_lang.unwrap().name(), "typescript");
let jsx_lang = registry.get_by_extension(".jsx");
assert!(jsx_lang.is_some());
assert_eq!(jsx_lang.unwrap().name(), "tsx");
}
}