ricecoder_storage/markdown_config/integration/
commands.rs

1//! Integration with ricecoder-commands for markdown-based command configuration
2
3use crate::markdown_config::error::MarkdownConfigResult;
4use crate::markdown_config::loader::{ConfigFile, ConfigFileType, ConfigurationLoader};
5use crate::markdown_config::types::CommandConfig;
6use std::path::PathBuf;
7use std::sync::Arc;
8use tracing::{debug, info, warn};
9
10/// Type alias for registration results: (success_count, error_count, errors)
11pub type RegistrationResult = (usize, usize, Vec<(String, String)>);
12
13/// Trait for registering command configurations
14///
15/// This trait allows ricecoder-storage to register command configurations without
16/// directly depending on ricecoder-commands, avoiding circular dependencies.
17pub trait CommandRegistrar: Send + Sync {
18    /// Register a command configuration
19    fn register_command(&mut self, command: CommandConfig) -> Result<(), String>;
20}
21
22/// Integration layer for command configuration with ricecoder-commands
23///
24/// This struct provides methods to discover, load, and register command configurations
25/// from markdown files with the ricecoder-commands subsystem.
26pub struct CommandConfigIntegration {
27    loader: Arc<ConfigurationLoader>,
28}
29
30impl CommandConfigIntegration {
31    /// Create a new command configuration integration
32    pub fn new(loader: Arc<ConfigurationLoader>) -> Self {
33        Self { loader }
34    }
35
36    /// Discover command configuration files in the given paths
37    ///
38    /// # Arguments
39    /// * `paths` - Directories to search for command markdown files
40    ///
41    /// # Returns
42    /// A vector of discovered command configuration files
43    pub fn discover_command_configs(&self, paths: &[PathBuf]) -> MarkdownConfigResult<Vec<ConfigFile>> {
44        let all_files = self.loader.discover(paths)?;
45
46        // Filter to only command configuration files
47        let command_files: Vec<ConfigFile> = all_files
48            .into_iter()
49            .filter(|f| f.config_type == ConfigFileType::Command)
50            .collect();
51
52        debug!("Discovered {} command configuration files", command_files.len());
53        Ok(command_files)
54    }
55
56    /// Load command configurations from markdown files
57    ///
58    /// # Arguments
59    /// * `paths` - Directories to search for command markdown files
60    ///
61    /// # Returns
62    /// A tuple of (loaded_commands, errors)
63    pub async fn load_command_configs(
64        &self,
65        paths: &[PathBuf],
66    ) -> MarkdownConfigResult<(Vec<CommandConfig>, Vec<(PathBuf, String)>)> {
67        let files = self.discover_command_configs(paths)?;
68
69        let mut commands = Vec::new();
70        let mut errors = Vec::new();
71
72        for file in files {
73            match self.loader.load(&file).await {
74                Ok(config) => {
75                    match config {
76                        crate::markdown_config::loader::LoadedConfig::Command(command) => {
77                            debug!("Loaded command configuration: {}", command.name);
78                            commands.push(command);
79                        }
80                        _ => {
81                            warn!("Expected command configuration but got different type from {}", file.path.display());
82                            errors.push((
83                                file.path,
84                                "Expected command configuration but got different type".to_string(),
85                            ));
86                        }
87                    }
88                }
89                Err(e) => {
90                    let error_msg = e.to_string();
91                    warn!("Failed to load command configuration from {}: {}", file.path.display(), error_msg);
92                    errors.push((file.path, error_msg));
93                }
94            }
95        }
96
97        info!("Loaded {} command configurations", commands.len());
98        Ok((commands, errors))
99    }
100
101    /// Register command configurations with a registrar
102    ///
103    /// This method registers command configurations using a generic registrar trait,
104    /// allowing integration with any command registry implementation.
105    ///
106    /// # Arguments
107    /// * `commands` - Command configurations to register
108    /// * `registrar` - The command registrar to register with
109    ///
110    /// # Returns
111    /// A tuple of (successful_count, error_count, errors)
112    pub fn register_commands(
113        &self,
114        commands: Vec<CommandConfig>,
115        registrar: &mut dyn CommandRegistrar,
116    ) -> MarkdownConfigResult<RegistrationResult> {
117        let mut success_count = 0;
118        let mut error_count = 0;
119        let mut errors = Vec::new();
120
121        for command in commands {
122            // Validate command configuration
123            if let Err(e) = command.validate() {
124                error_count += 1;
125                let error_msg = format!("Invalid command configuration: {}", e);
126                warn!("Failed to register command '{}': {}", command.name, error_msg);
127                errors.push((command.name.clone(), error_msg));
128                continue;
129            }
130
131            debug!("Registering command: {}", command.name);
132
133            // Register the command using the registrar
134            match registrar.register_command(command.clone()) {
135                Ok(_) => {
136                    success_count += 1;
137                    info!("Registered command: {}", command.name);
138                }
139                Err(e) => {
140                    error_count += 1;
141                    warn!("Failed to register command '{}': {}", command.name, e);
142                    errors.push((command.name.clone(), e));
143                }
144            }
145        }
146
147        debug!(
148            "Command registration complete: {} successful, {} failed",
149            success_count, error_count
150        );
151
152        Ok((success_count, error_count, errors))
153    }
154
155    /// Load and register command configurations in one operation
156    ///
157    /// # Arguments
158    /// * `paths` - Directories to search for command markdown files
159    /// * `registrar` - The command registrar to register with
160    ///
161    /// # Returns
162    /// A tuple of (successful_count, error_count, errors)
163    pub async fn load_and_register_commands(
164        &self,
165        paths: &[PathBuf],
166        registrar: &mut dyn CommandRegistrar,
167    ) -> MarkdownConfigResult<(usize, usize, Vec<(String, String)>)> {
168        let (commands, load_errors) = self.load_command_configs(paths).await?;
169
170        let (success, errors, mut reg_errors) = self.register_commands(commands, registrar)?;
171
172        // Combine load and registration errors
173        for (path, msg) in load_errors {
174            reg_errors.push((path.display().to_string(), msg));
175        }
176
177        Ok((success, errors, reg_errors))
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::markdown_config::registry::ConfigRegistry;
185    use std::fs;
186    use tempfile::TempDir;
187
188    fn create_test_command_file(dir: &PathBuf, name: &str, content: &str) -> PathBuf {
189        let path = dir.join(format!("{}.command.md", name));
190        fs::write(&path, content).unwrap();
191        path
192    }
193
194    #[test]
195    fn test_discover_command_configs() {
196        let temp_dir = TempDir::new().unwrap();
197        let dir_path = temp_dir.path().to_path_buf();
198
199        // Create test command files
200        create_test_command_file(&dir_path, "cmd1", "---\nname: cmd1\n---\nTest");
201        create_test_command_file(&dir_path, "cmd2", "---\nname: cmd2\n---\nTest");
202
203        // Create a non-command file
204        fs::write(dir_path.join("agent1.agent.md"), "---\nname: agent1\n---\nTest").unwrap();
205
206        let registry = Arc::new(ConfigRegistry::new());
207        let loader = Arc::new(ConfigurationLoader::new(registry));
208        let integration = CommandConfigIntegration::new(loader);
209
210        let discovered = integration.discover_command_configs(&[dir_path]).unwrap();
211
212        assert_eq!(discovered.len(), 2);
213        assert!(discovered.iter().all(|f| f.config_type == ConfigFileType::Command));
214    }
215
216    #[tokio::test]
217    async fn test_load_command_configs() {
218        let temp_dir = TempDir::new().unwrap();
219        let dir_path = temp_dir.path().to_path_buf();
220
221        let command_content = r#"---
222name: test-command
223description: A test command
224parameters:
225  - name: message
226    description: Message to echo
227    required: true
228keybinding: C-t
229---
230echo {{message}}"#;
231
232        create_test_command_file(&dir_path, "test-command", command_content);
233
234        let registry = Arc::new(ConfigRegistry::new());
235        let loader = Arc::new(ConfigurationLoader::new(registry));
236        let integration = CommandConfigIntegration::new(loader);
237
238        let (commands, errors) = integration.load_command_configs(&[dir_path]).await.unwrap();
239
240        assert_eq!(commands.len(), 1);
241        assert_eq!(errors.len(), 0);
242        assert_eq!(commands[0].name, "test-command");
243        assert_eq!(commands[0].parameters.len(), 1);
244    }
245
246    #[tokio::test]
247    async fn test_load_command_configs_with_errors() {
248        let temp_dir = TempDir::new().unwrap();
249        let dir_path = temp_dir.path().to_path_buf();
250
251        // Create a valid command file
252        let valid_content = r#"---
253name: valid-command
254---
255echo test"#;
256        create_test_command_file(&dir_path, "valid-command", valid_content);
257
258        // Create an invalid command file (missing frontmatter)
259        fs::write(dir_path.join("invalid.command.md"), "# No frontmatter\nJust markdown").unwrap();
260
261        let registry = Arc::new(ConfigRegistry::new());
262        let loader = Arc::new(ConfigurationLoader::new(registry));
263        let integration = CommandConfigIntegration::new(loader);
264
265        let (commands, errors) = integration.load_command_configs(&[dir_path]).await.unwrap();
266
267        assert_eq!(commands.len(), 1);
268        assert_eq!(errors.len(), 1);
269        assert_eq!(commands[0].name, "valid-command");
270    }
271
272    #[test]
273    fn test_register_with_command_registry() {
274        let registry = Arc::new(ConfigRegistry::new());
275        let loader = Arc::new(ConfigurationLoader::new(registry));
276        let integration = CommandConfigIntegration::new(loader);
277
278        let commands = vec![
279            CommandConfig {
280                name: "cmd1".to_string(),
281                description: Some("Test command 1".to_string()),
282                template: "echo {{message}}".to_string(),
283                parameters: vec![crate::markdown_config::types::Parameter {
284                    name: "message".to_string(),
285                    description: Some("Message to echo".to_string()),
286                    required: true,
287                    default: None,
288                }],
289                keybinding: Some("C-1".to_string()),
290            },
291            CommandConfig {
292                name: "cmd2".to_string(),
293                description: Some("Test command 2".to_string()),
294                template: "ls -la".to_string(),
295                parameters: vec![],
296                keybinding: None,
297            },
298        ];
299
300        struct MockRegistrar;
301        impl CommandRegistrar for MockRegistrar {
302            fn register_command(&mut self, _command: CommandConfig) -> Result<(), String> {
303                Ok(())
304            }
305        }
306
307        let mut registrar = MockRegistrar;
308        let (success, errors, error_list) = integration
309            .register_commands(commands, &mut registrar)
310            .unwrap();
311
312        assert_eq!(success, 2);
313        assert_eq!(errors, 0);
314        assert_eq!(error_list.len(), 0);
315    }
316
317    #[test]
318    fn test_register_invalid_command() {
319        let registry = Arc::new(ConfigRegistry::new());
320        let loader = Arc::new(ConfigurationLoader::new(registry));
321        let integration = CommandConfigIntegration::new(loader);
322
323        let commands = vec![
324            CommandConfig {
325                name: String::new(), // Invalid: empty name
326                description: None,
327                template: "echo test".to_string(),
328                parameters: vec![],
329                keybinding: None,
330            },
331        ];
332
333        struct MockRegistrar;
334        impl CommandRegistrar for MockRegistrar {
335            fn register_command(&mut self, _command: CommandConfig) -> Result<(), String> {
336                Ok(())
337            }
338        }
339
340        let mut registrar = MockRegistrar;
341        let (success, errors, error_list) = integration
342            .register_commands(commands, &mut registrar)
343            .unwrap();
344
345        assert_eq!(success, 0);
346        assert_eq!(errors, 1);
347        assert_eq!(error_list.len(), 1);
348    }
349}