rskit_agent/command/
registry.rs1use std::collections::HashMap;
2
3use rskit_errors::{AppError, ErrorCode};
4
5pub trait CommandHandler: Send + Sync {
9 fn execute(&self, args: &str) -> Result<String, AppError>;
11}
12
13impl<F> CommandHandler for F
16where
17 F: Fn(&str) -> Result<String, AppError> + Send + Sync,
18{
19 fn execute(&self, args: &str) -> Result<String, AppError> {
20 (self)(args)
21 }
22}
23
24pub struct Command {
28 pub name: String,
30 pub description: String,
32 pub usage: String,
34 pub handler: Box<dyn CommandHandler>,
36}
37
38pub struct CommandRegistry {
42 commands: HashMap<String, Command>,
43}
44
45impl Default for CommandRegistry {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl CommandRegistry {
52 pub fn new() -> Self {
54 Self {
55 commands: HashMap::new(),
56 }
57 }
58
59 pub fn register(&mut self, cmd: Command) -> Result<(), AppError> {
62 if cmd.name.trim().is_empty() {
63 return Err(AppError::new(
64 ErrorCode::InvalidInput,
65 "command name must not be empty",
66 ));
67 }
68 self.commands.insert(cmd.name.clone(), cmd);
69 Ok(())
70 }
71
72 pub fn get(&self, name: &str) -> Option<&Command> {
74 self.commands.get(name)
75 }
76
77 pub fn list(&self) -> Vec<&Command> {
79 let mut cmds: Vec<&Command> = self.commands.values().collect();
80 cmds.sort_by(|a, b| a.name.cmp(&b.name));
81 cmds
82 }
83
84 pub fn is_command(input: &str) -> bool {
86 let trimmed = input.trim();
87 trimmed.starts_with('/')
88 && trimmed
89 .chars()
90 .nth(1)
91 .is_some_and(|c| c.is_ascii_alphabetic())
92 }
93
94 pub fn parse_command(input: &str) -> Option<(&str, &str)> {
97 let trimmed = input.trim();
98 if !Self::is_command(trimmed) {
99 return None;
100 }
101
102 let without_slash = &trimmed[1..];
103 let Some(pos) = without_slash.find(|c: char| c.is_whitespace()) else {
104 return Some((without_slash, ""));
105 };
106 let name = &without_slash[..pos];
107 let args = without_slash[pos..].trim_start();
108 Some((name, args))
109 }
110
111 pub fn execute(&self, input: &str) -> Result<String, AppError> {
113 let (name, args) = Self::parse_command(input).ok_or_else(|| {
114 AppError::new(ErrorCode::InvalidInput, "input is not a slash command")
115 })?;
116
117 let cmd = self.get(name).ok_or_else(|| {
118 AppError::new(ErrorCode::InvalidInput, format!("unknown command: /{name}"))
119 })?;
120
121 cmd.handler.execute(args)
122 }
123}