use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use super::cache::PluginCache;
use super::config_manager::PluginConfigManager;
use super::fetcher::PluginFetcher;
use super::manifest::PluginManifest;
use super::registry::PluginRegistry;
use super::{log_plugin_operation, PluginError, PluginSource, Result};
#[derive(Debug, Clone)]
pub struct LoadedConfig {
pub plugin_name: String,
pub language: String,
pub tool: String,
pub config_path: PathBuf,
}
#[derive(Debug)]
pub struct PluginLoader {
cache: PluginCache,
fetcher: PluginFetcher,
registry: PluginRegistry,
verbose: bool,
}
impl PluginLoader {
pub fn new() -> Result<Self> {
Ok(Self {
cache: PluginCache::new()?,
fetcher: PluginFetcher::new(),
registry: PluginRegistry::new(),
verbose: false,
})
}
pub fn with_verbose(verbose: bool) -> Result<Self> {
Ok(Self {
cache: PluginCache::new()?,
fetcher: PluginFetcher::with_verbose(verbose),
registry: PluginRegistry::new(),
verbose,
})
}
pub fn with_components(cache: PluginCache, registry: PluginRegistry, verbose: bool) -> Self {
Self {
cache,
fetcher: PluginFetcher::with_verbose(verbose),
registry,
verbose,
}
}
pub fn load_configs(
&self,
sources: &[PluginSource],
force_update: bool,
) -> Result<Vec<LoadedConfig>> {
let mut all_configs: HashMap<(String, String), LoadedConfig> = HashMap::new();
for source in sources {
if !source.enabled {
log_plugin_operation(
"skip",
&format!("Plugin '{}' is disabled", source.name),
self.verbose,
);
continue;
}
match self.load_plugin_configs(source, force_update) {
Ok(configs) => {
for config in configs {
let key = (config.language.clone(), config.tool.clone());
log_plugin_operation(
"load",
&format!(
"Loaded {}/{} from {}",
config.language, config.tool, config.plugin_name
),
self.verbose,
);
all_configs.insert(key, config);
}
}
Err(e) => {
log_plugin_operation(
"error",
&format!("Failed to load plugin '{}': {}", source.name, e),
true, );
if let Ok(configs) = self.load_from_cache_only(source) {
log_plugin_operation(
"fallback",
&format!("Using cached version of '{}'", source.name),
self.verbose,
);
for config in configs {
let key = (config.language.clone(), config.tool.clone());
all_configs.insert(key, config);
}
}
}
}
}
Ok(all_configs.into_values().collect())
}
fn resolve_alias(&self, source: &PluginSource) -> Result<PluginSource> {
if source.url.is_some() {
return Ok(source.clone());
}
if let Ok(manager) = PluginConfigManager::project() {
if let Ok(Some((url, ref_))) = manager.get_plugin_by_alias(&source.name) {
log_plugin_operation(
"resolve",
&format!("Resolved alias '{}' from project config", source.name),
self.verbose,
);
return Ok(PluginSource {
name: source.name.clone(),
url: Some(url),
git_ref: ref_.or_else(|| source.git_ref.clone()),
enabled: source.enabled,
});
}
}
if let Ok(manager) = PluginConfigManager::global() {
if let Ok(Some((url, ref_))) = manager.get_plugin_by_alias(&source.name) {
log_plugin_operation(
"resolve",
&format!("Resolved alias '{}' from global config", source.name),
self.verbose,
);
return Ok(PluginSource {
name: source.name.clone(),
url: Some(url),
git_ref: ref_.or_else(|| source.git_ref.clone()),
enabled: source.enabled,
});
}
}
Ok(source.clone())
}
fn load_plugin_configs(
&self,
source: &PluginSource,
force_update: bool,
) -> Result<Vec<LoadedConfig>> {
let resolved_from_alias = self.resolve_alias(source)?;
let resolved_source = self.registry.resolve(&resolved_from_alias)?;
let cached = self
.fetcher
.fetch(&resolved_source, &self.cache, force_update)?;
let manifest = PluginManifest::load(&cached.cache_path)?;
self.extract_configs(&manifest, &cached.cache_path)
}
fn load_from_cache_only(&self, source: &PluginSource) -> Result<Vec<LoadedConfig>> {
let resolved_source = self.registry.resolve(source)?;
let (cache_path, manifest) = self.cache.load_cached_plugin(&resolved_source)?;
self.extract_configs(&manifest, &cache_path)
}
fn extract_configs(
&self,
manifest: &PluginManifest,
plugin_path: &Path,
) -> Result<Vec<LoadedConfig>> {
let mut configs = Vec::new();
for (language, tools) in &manifest.configs {
for (tool, config_rel_path) in tools {
let config_path = plugin_path.join(config_rel_path);
if !config_path.exists() {
return Err(PluginError::ConfigNotFound { path: config_path });
}
configs.push(LoadedConfig {
plugin_name: manifest.plugin.name.clone(),
language: language.clone(),
tool: tool.clone(),
config_path,
});
}
}
Ok(configs)
}
pub fn get_config_content(
&self,
sources: &[PluginSource],
language: &str,
tool: &str,
) -> Result<Option<String>> {
let configs = self.load_configs(sources, false)?;
for config in configs {
if config.language == language && config.tool == tool {
let content = fs::read_to_string(&config.config_path)?;
return Ok(Some(content));
}
}
Ok(None)
}
pub fn get_config_path(
&self,
sources: &[PluginSource],
language: &str,
tool: &str,
) -> Result<Option<PathBuf>> {
let configs = self.load_configs(sources, false)?;
for config in configs {
if config.language == language && config.tool == tool {
return Ok(Some(config.config_path));
}
}
Ok(None)
}
pub fn cache(&self) -> &PluginCache {
&self.cache
}
pub fn registry(&self) -> &PluginRegistry {
&self.registry
}
}
impl Default for PluginLoader {
fn default() -> Self {
Self::new().expect("Failed to create plugin loader")
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_plugin(dir: &Path, name: &str) -> PathBuf {
let plugin_dir = dir.join(name);
fs::create_dir_all(&plugin_dir).unwrap();
let manifest = format!(
r#"
[plugin]
name = "{}"
version = "1.0.0"
[configs.rust]
clippy = "rust/clippy.toml"
"#,
name
);
fs::write(plugin_dir.join("linthis-plugin.toml"), manifest).unwrap();
fs::create_dir_all(plugin_dir.join("rust")).unwrap();
fs::write(plugin_dir.join("rust/clippy.toml"), "# clippy config").unwrap();
plugin_dir
}
#[test]
fn test_extract_configs() {
let temp_dir = TempDir::new().unwrap();
let plugin_path = create_test_plugin(temp_dir.path(), "test-plugin");
let manifest = PluginManifest::load(&plugin_path).unwrap();
let cache = PluginCache::with_dir(temp_dir.path().to_path_buf());
let loader = PluginLoader::with_components(cache, PluginRegistry::new(), false);
let configs = loader.extract_configs(&manifest, &plugin_path).unwrap();
assert_eq!(configs.len(), 1);
assert_eq!(configs[0].language, "rust");
assert_eq!(configs[0].tool, "clippy");
}
}