Skip to main content

saorsa_agent/extension/
command_registry.rs

1//! Command registration system for extensions.
2
3use crate::error::{Result, SaorsaAgentError};
4use std::collections::HashMap;
5use std::sync::Arc;
6
7/// Type alias for command handler functions.
8pub type CommandHandler = Arc<dyn Fn(&[&str]) -> Result<String> + Send + Sync>;
9
10/// Command definition registered by an extension.
11pub struct CommandDefinition {
12    /// Unique command name.
13    pub name: String,
14    /// Human-readable description.
15    pub description: String,
16    /// Usage string (e.g., "command \[options\] \<args\>").
17    pub usage: String,
18    /// Command handler function.
19    pub handler: CommandHandler,
20}
21
22impl CommandDefinition {
23    /// Creates a new command definition.
24    pub fn new(name: String, description: String, usage: String, handler: CommandHandler) -> Self {
25        Self {
26            name,
27            description,
28            usage,
29            handler,
30        }
31    }
32}
33
34/// Registry for extension-provided commands.
35pub struct CommandRegistry {
36    commands: HashMap<String, CommandDefinition>,
37}
38
39impl CommandRegistry {
40    /// Creates a new empty command registry.
41    pub fn new() -> Self {
42        Self {
43            commands: HashMap::new(),
44        }
45    }
46
47    /// Registers a command.
48    ///
49    /// Returns an error if a command with the same name is already registered.
50    pub fn register_command(&mut self, def: CommandDefinition) -> Result<()> {
51        if self.commands.contains_key(&def.name) {
52            return Err(SaorsaAgentError::Extension(format!(
53                "command '{}' is already registered",
54                def.name
55            )));
56        }
57        self.commands.insert(def.name.clone(), def);
58        Ok(())
59    }
60
61    /// Unregisters a command by name.
62    ///
63    /// Returns an error if the command is not found.
64    pub fn unregister_command(&mut self, name: &str) -> Result<()> {
65        self.commands
66            .remove(name)
67            .ok_or_else(|| SaorsaAgentError::Extension(format!("command '{}' not found", name)))?;
68        Ok(())
69    }
70
71    /// Gets a command definition by name.
72    pub fn get_command(&self, name: &str) -> Option<&CommandDefinition> {
73        self.commands.get(name)
74    }
75
76    /// Lists all registered commands.
77    pub fn list_commands(&self) -> Vec<&CommandDefinition> {
78        self.commands.values().collect()
79    }
80
81    /// Executes a command by name with the given arguments.
82    ///
83    /// Returns an error if the command is not found or execution fails.
84    pub fn execute_command(&self, name: &str, args: &[&str]) -> Result<String> {
85        let def = self
86            .commands
87            .get(name)
88            .ok_or_else(|| SaorsaAgentError::Extension(format!("command '{}' not found", name)))?;
89        (def.handler)(args)
90    }
91}
92
93impl Default for CommandRegistry {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn echo_handler(args: &[&str]) -> Result<String> {
104        Ok(format!("echo: {}", args.join(" ")))
105    }
106
107    #[test]
108    fn register_command() {
109        let mut registry = CommandRegistry::new();
110        let def = CommandDefinition::new(
111            "echo".to_string(),
112            "Echo arguments".to_string(),
113            "echo <text>".to_string(),
114            Arc::new(echo_handler),
115        );
116        let result = registry.register_command(def);
117        assert!(result.is_ok());
118        assert!(registry.get_command("echo").is_some());
119    }
120
121    #[test]
122    fn duplicate_command_fails() {
123        let mut registry = CommandRegistry::new();
124        let def1 = CommandDefinition::new(
125            "echo".to_string(),
126            "Echo 1".to_string(),
127            "echo".to_string(),
128            Arc::new(echo_handler),
129        );
130        let def2 = CommandDefinition::new(
131            "echo".to_string(),
132            "Echo 2".to_string(),
133            "echo".to_string(),
134            Arc::new(echo_handler),
135        );
136        assert!(registry.register_command(def1).is_ok());
137        let result = registry.register_command(def2);
138        assert!(result.is_err());
139        match result {
140            Err(SaorsaAgentError::Extension(msg)) => {
141                assert!(msg.contains("already registered"));
142            }
143            _ => unreachable!(),
144        }
145    }
146
147    #[test]
148    fn unregister_command() {
149        let mut registry = CommandRegistry::new();
150        let def = CommandDefinition::new(
151            "echo".to_string(),
152            "Echo".to_string(),
153            "echo".to_string(),
154            Arc::new(echo_handler),
155        );
156        assert!(registry.register_command(def).is_ok());
157        assert!(registry.unregister_command("echo").is_ok());
158        assert!(registry.get_command("echo").is_none());
159    }
160
161    #[test]
162    fn unregister_nonexistent_fails() {
163        let mut registry = CommandRegistry::new();
164        let result = registry.unregister_command("nonexistent");
165        assert!(result.is_err());
166        match result {
167            Err(SaorsaAgentError::Extension(msg)) => {
168                assert!(msg.contains("not found"));
169            }
170            _ => unreachable!(),
171        }
172    }
173
174    #[test]
175    fn list_commands() {
176        let mut registry = CommandRegistry::new();
177        let def1 = CommandDefinition::new(
178            "echo".to_string(),
179            "Echo".to_string(),
180            "echo".to_string(),
181            Arc::new(echo_handler),
182        );
183        let def2 = CommandDefinition::new(
184            "test".to_string(),
185            "Test".to_string(),
186            "test".to_string(),
187            Arc::new(echo_handler),
188        );
189        assert!(registry.register_command(def1).is_ok());
190        assert!(registry.register_command(def2).is_ok());
191        let list = registry.list_commands();
192        assert_eq!(list.len(), 2);
193    }
194
195    #[test]
196    fn execute_command() {
197        let mut registry = CommandRegistry::new();
198        let def = CommandDefinition::new(
199            "echo".to_string(),
200            "Echo".to_string(),
201            "echo".to_string(),
202            Arc::new(echo_handler),
203        );
204        assert!(registry.register_command(def).is_ok());
205        let result = registry.execute_command("echo", &["hello", "world"]);
206        assert!(result.is_ok());
207        let output = result.ok().unwrap_or_default();
208        assert_eq!(output, "echo: hello world");
209    }
210
211    #[test]
212    fn execute_nonexistent_fails() {
213        let registry = CommandRegistry::new();
214        let result = registry.execute_command("nonexistent", &[]);
215        assert!(result.is_err());
216        match result {
217            Err(SaorsaAgentError::Extension(msg)) => {
218                assert!(msg.contains("not found"));
219            }
220            _ => unreachable!(),
221        }
222    }
223}