use async_trait::async_trait;
use std::collections::HashMap;
use tokio::sync::RwLock;
use tracing::{debug, info};
use super::{
CustomCommand, CommandExecution, CommandError, CommandManager, CommandConfig,
CommandDiscovery, CommandParser,
};
pub struct CommandManagerImpl {
discovery: CommandDiscovery,
parser: CommandParser,
commands: RwLock<HashMap<String, CustomCommand>>,
config: CommandConfig,
}
impl CommandManagerImpl {
pub fn new(config: CommandConfig) -> Self {
let discovery = CommandDiscovery::new(config.clone());
let parser = CommandParser::new();
Self {
discovery,
parser,
commands: RwLock::new(HashMap::new()),
config,
}
}
pub fn with_default_config() -> Self {
Self::new(CommandConfig::default())
}
pub fn config(&self) -> &CommandConfig {
&self.config
}
fn validate_arguments(
&self,
command: &CustomCommand,
arguments: &HashMap<String, String>,
) -> Result<(), CommandError> {
for param in &command.parameters {
if param.required && !arguments.contains_key(¶m.name) {
return Err(CommandError::MissingParameter(param.name.clone()));
}
}
Ok(())
}
fn apply_defaults(
&self,
command: &CustomCommand,
mut arguments: HashMap<String, String>,
) -> HashMap<String, String> {
for param in &command.parameters {
if !arguments.contains_key(¶m.name) {
if let Some(default) = ¶m.default_value {
arguments.insert(param.name.clone(), default.clone());
}
}
}
arguments
}
pub fn get_discovery_stats(&self) -> super::discovery::DiscoveryStats {
self.discovery.get_discovery_stats()
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub async fn command_count(&self) -> usize {
let commands = self.commands.read().await;
commands.len()
}
pub async fn clear_cache(&self) {
let mut commands = self.commands.write().await;
commands.clear();
debug!("Command cache cleared");
}
pub async fn get_command_ids(&self) -> Vec<String> {
let commands = self.commands.read().await;
commands.keys().cloned().collect()
}
pub async fn has_command(&self, command_id: &str) -> bool {
let commands = self.commands.read().await;
commands.contains_key(command_id)
}
pub async fn get_commands_by_type(&self, command_type: super::CommandType) -> Vec<CustomCommand> {
let commands = self.commands.read().await;
commands
.values()
.filter(|cmd| cmd.command_type == command_type)
.cloned()
.collect()
}
pub async fn search_commands(&self, query: &str) -> Vec<CustomCommand> {
let commands = self.commands.read().await;
let query_lower = query.to_lowercase();
commands
.values()
.filter(|cmd| {
cmd.name.to_lowercase().contains(&query_lower) ||
cmd.description.as_ref()
.map_or(false, |desc| desc.to_lowercase().contains(&query_lower))
})
.cloned()
.collect()
}
}
#[async_trait]
impl CommandManager for CommandManagerImpl {
async fn discover_commands(&self) -> Result<Vec<CustomCommand>, CommandError> {
if !self.config.enabled {
return Ok(Vec::new());
}
info!("Discovering commands...");
let discovered = self.discovery.discover_all_commands()?;
let mut commands = self.commands.write().await;
commands.clear();
for command in &discovered {
commands.insert(command.id.clone(), command.clone());
}
info!("Cached {} commands", commands.len());
Ok(discovered)
}
async fn get_command(&self, command_id: &str) -> Result<Option<CustomCommand>, CommandError> {
let commands = self.commands.read().await;
Ok(commands.get(command_id).cloned())
}
async fn execute_command(
&self,
command_id: &str,
arguments: HashMap<String, String>,
) -> Result<CommandExecution, CommandError> {
debug!("Executing command: {} with {} arguments", command_id, arguments.len());
let command = {
let commands = self.commands.read().await;
commands.get(command_id)
.cloned()
.ok_or_else(|| CommandError::CommandNotFound(command_id.to_string()))?
};
self.validate_arguments(&command, &arguments)?;
let final_arguments = self.apply_defaults(&command, arguments);
let resolved_content = self.parser.substitute_parameters(&command.content, &final_arguments)?;
debug!("Command executed successfully: {}", command_id);
Ok(CommandExecution {
command,
arguments: final_arguments,
resolved_content,
})
}
async fn list_commands(&self) -> Result<Vec<CustomCommand>, CommandError> {
let commands = self.commands.read().await;
Ok(commands.values().cloned().collect())
}
async fn reload_commands(&self) -> Result<(), CommandError> {
info!("Reloading commands...");
self.discover_commands().await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::CommandType;
use std::fs;
use tempfile::TempDir;
fn create_test_command_file(dir: &std::path::Path, name: &str, content: &str) -> std::path::PathBuf {
let file_path = dir.join(format!("{}.md", name));
fs::write(&file_path, content).unwrap();
file_path
}
#[tokio::test]
async fn test_command_manager_basic() {
let temp_dir = TempDir::new().unwrap();
let commands_dir = temp_dir.path().join("commands");
fs::create_dir_all(&commands_dir).unwrap();
create_test_command_file(&commands_dir, "hello", r#"# Hello Command
Say hello to someone.
Hello $NAME!
"#);
let config = CommandConfig::new()
.add_user_dir(&commands_dir);
let manager = CommandManagerImpl::new(config);
let commands = manager.discover_commands().await.unwrap();
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].name, "Hello Command");
assert_eq!(manager.command_count().await, 1);
assert!(manager.has_command("user:hello").await);
assert!(!manager.has_command("user:nonexistent").await);
}
#[tokio::test]
async fn test_execute_command() {
let temp_dir = TempDir::new().unwrap();
let commands_dir = temp_dir.path().join("commands");
fs::create_dir_all(&commands_dir).unwrap();
create_test_command_file(&commands_dir, "greet", r#"# Greet Command
Greet someone with a custom message.
Hello $NAME, welcome to $PROJECT!
"#);
let config = CommandConfig::new()
.add_user_dir(&commands_dir);
let manager = CommandManagerImpl::new(config);
manager.discover_commands().await.unwrap();
let mut args = HashMap::new();
args.insert("NAME".to_string(), "Alice".to_string());
args.insert("PROJECT".to_string(), "MyApp".to_string());
let execution = manager.execute_command("user:greet", args).await.unwrap();
assert_eq!(execution.resolved_content, "Hello Alice, welcome to MyApp!");
assert_eq!(execution.arguments.get("NAME").unwrap(), "Alice");
assert_eq!(execution.arguments.get("PROJECT").unwrap(), "MyApp");
}
#[tokio::test]
async fn test_execute_command_missing_parameter() {
let temp_dir = TempDir::new().unwrap();
let commands_dir = temp_dir.path().join("commands");
fs::create_dir_all(&commands_dir).unwrap();
create_test_command_file(&commands_dir, "greet", r#"# Greet Command
Greet someone.
Hello $NAME!
"#);
let config = CommandConfig::new()
.add_user_dir(&commands_dir);
let manager = CommandManagerImpl::new(config);
manager.discover_commands().await.unwrap();
let args = HashMap::new();
let result = manager.execute_command("user:greet", args).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), CommandError::MissingParameter(_)));
}
#[tokio::test]
async fn test_search_commands() {
let temp_dir = TempDir::new().unwrap();
let commands_dir = temp_dir.path().join("commands");
fs::create_dir_all(&commands_dir).unwrap();
create_test_command_file(&commands_dir, "hello", r#"# Hello Command
Say hello to someone.
Hello $NAME!
"#);
create_test_command_file(&commands_dir, "deploy", r#"# Deploy Command
Deploy the application.
Deploying $APP...
"#);
let config = CommandConfig::new()
.add_user_dir(&commands_dir);
let manager = CommandManagerImpl::new(config);
manager.discover_commands().await.unwrap();
let results = manager.search_commands("hello").await;
assert_eq!(results.len(), 1);
assert_eq!(results[0].name, "Hello Command");
let results = manager.search_commands("deploy").await;
assert_eq!(results.len(), 1);
assert_eq!(results[0].name, "Deploy Command");
let results = manager.search_commands("nonexistent").await;
assert!(results.is_empty());
}
#[tokio::test]
async fn test_get_commands_by_type() {
let temp_dir = TempDir::new().unwrap();
let commands_dir = temp_dir.path().join("commands");
fs::create_dir_all(&commands_dir).unwrap();
create_test_command_file(&commands_dir, "user-cmd", "# User Command\nUser command");
let config = CommandConfig::new()
.add_user_dir(&commands_dir);
let manager = CommandManagerImpl::new(config);
manager.discover_commands().await.unwrap();
let user_commands = manager.get_commands_by_type(CommandType::User).await;
assert!(!user_commands.is_empty());
let project_commands = manager.get_commands_by_type(CommandType::Project).await;
assert!(project_commands.is_empty());
}
}