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
15where
16 F: Fn(&str) -> Result<String, AppError> + Send + Sync,
17{
18 fn execute(&self, args: &str) -> Result<String, AppError> {
19 (self)(args)
20 }
21}
22
23pub struct Command {
27 pub name: String,
29 pub description: String,
31 pub usage: String,
33 pub handler: Box<dyn CommandHandler>,
35}
36
37pub struct CommandRegistry {
41 commands: HashMap<String, Command>,
42}
43
44impl Default for CommandRegistry {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl CommandRegistry {
51 pub fn new() -> Self {
53 Self {
54 commands: HashMap::new(),
55 }
56 }
57
58 pub fn register(&mut self, cmd: Command) -> Result<(), AppError> {
61 if cmd.name.trim().is_empty() {
62 return Err(AppError::new(
63 ErrorCode::InvalidInput,
64 "command name must not be empty",
65 ));
66 }
67 self.commands.insert(cmd.name.clone(), cmd);
68 Ok(())
69 }
70
71 pub fn get(&self, name: &str) -> Option<&Command> {
73 self.commands.get(name)
74 }
75
76 pub fn list(&self) -> Vec<&Command> {
78 let mut cmds: Vec<&Command> = self.commands.values().collect();
79 cmds.sort_by(|a, b| a.name.cmp(&b.name));
80 cmds
81 }
82
83 pub fn is_command(input: &str) -> bool {
85 let trimmed = input.trim();
86 trimmed.starts_with('/')
87 && trimmed
88 .chars()
89 .nth(1)
90 .is_some_and(|c| c.is_ascii_alphabetic())
91 }
92
93 pub fn parse_command(input: &str) -> Option<(&str, &str)> {
95 let trimmed = input.trim();
96 if !Self::is_command(trimmed) {
97 return None;
98 }
99
100 let without_slash = &trimmed[1..];
101 let Some(pos) = without_slash.find(|c: char| c.is_whitespace()) else {
102 return Some((without_slash, ""));
103 };
104 let name = &without_slash[..pos];
105 let args = without_slash[pos..].trim_start();
106 Some((name, args))
107 }
108
109 pub fn execute(&self, input: &str) -> Result<String, AppError> {
111 let (name, args) = Self::parse_command(input).ok_or_else(|| {
112 AppError::new(ErrorCode::InvalidInput, "input is not a slash command")
113 })?;
114
115 let cmd = self.get(name).ok_or_else(|| {
116 AppError::new(ErrorCode::InvalidInput, format!("unknown command: /{name}"))
117 })?;
118
119 cmd.handler.execute(args)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn command(name: &str) -> Command {
128 Command {
129 name: name.to_string(),
130 description: format!("{name} command"),
131 usage: format!("/{name}"),
132 handler: Box::new(|args: &str| Ok(format!("ran {args}"))),
133 }
134 }
135
136 #[test]
137 fn registry_registers_lists_parses_and_executes_commands() {
138 let mut registry = CommandRegistry::default();
139 registry.register(command("zeta")).unwrap();
140 registry.register(command("alpha")).unwrap();
141
142 let names = registry
143 .list()
144 .into_iter()
145 .map(|cmd| cmd.name.as_str())
146 .collect::<Vec<_>>();
147 assert_eq!(names, vec!["alpha", "zeta"]);
148 assert_eq!(
149 CommandRegistry::parse_command(" /alpha beta "),
150 Some(("alpha", "beta"))
151 );
152 assert_eq!(registry.execute("/alpha payload").unwrap(), "ran payload");
153 }
154
155 #[test]
156 fn registry_rejects_invalid_or_unknown_commands() {
157 let mut registry = CommandRegistry::new();
158 assert_eq!(
159 registry.register(command(" ")).unwrap_err().code(),
160 ErrorCode::InvalidInput
161 );
162
163 assert!(!CommandRegistry::is_command("hello"));
164 assert!(!CommandRegistry::is_command("/1"));
165 assert_eq!(
166 registry.execute("hello").unwrap_err().code(),
167 ErrorCode::InvalidInput
168 );
169 assert_eq!(
170 registry.execute("/missing").unwrap_err().code(),
171 ErrorCode::InvalidInput
172 );
173 }
174}