ricecoder_storage/markdown_config/integration/
commands.rs1use 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
10pub type RegistrationResult = (usize, usize, Vec<(String, String)>);
12
13pub trait CommandRegistrar: Send + Sync {
18 fn register_command(&mut self, command: CommandConfig) -> Result<(), String>;
20}
21
22pub struct CommandConfigIntegration {
27 loader: Arc<ConfigurationLoader>,
28}
29
30impl CommandConfigIntegration {
31 pub fn new(loader: Arc<ConfigurationLoader>) -> Self {
33 Self { loader }
34 }
35
36 pub fn discover_command_configs(&self, paths: &[PathBuf]) -> MarkdownConfigResult<Vec<ConfigFile>> {
44 let all_files = self.loader.discover(paths)?;
45
46 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 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 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 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 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 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 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_file(&dir_path, "cmd1", "---\nname: cmd1\n---\nTest");
201 create_test_command_file(&dir_path, "cmd2", "---\nname: cmd2\n---\nTest");
202
203 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 let valid_content = r#"---
253name: valid-command
254---
255echo test"#;
256 create_test_command_file(&dir_path, "valid-command", valid_content);
257
258 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(), 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}