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}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 fn command(name: &str) -> Command {
130 Command {
131 name: name.to_string(),
132 description: format!("{name} command"),
133 usage: format!("/{name}"),
134 handler: Box::new(|args: &str| Ok(format!("ran {args}"))),
135 }
136 }
137
138 #[test]
139 fn registry_registers_lists_parses_and_executes_commands() {
140 let mut registry = CommandRegistry::default();
141 registry.register(command("zeta")).unwrap();
142 registry.register(command("alpha")).unwrap();
143
144 let names = registry
145 .list()
146 .into_iter()
147 .map(|cmd| cmd.name.as_str())
148 .collect::<Vec<_>>();
149 assert_eq!(names, vec!["alpha", "zeta"]);
150 assert_eq!(
151 CommandRegistry::parse_command(" /alpha beta "),
152 Some(("alpha", "beta"))
153 );
154 assert_eq!(registry.execute("/alpha payload").unwrap(), "ran payload");
155 }
156
157 #[test]
158 fn registry_rejects_invalid_or_unknown_commands() {
159 let mut registry = CommandRegistry::new();
160 assert_eq!(
161 registry.register(command(" ")).unwrap_err().code(),
162 ErrorCode::InvalidInput
163 );
164
165 assert!(!CommandRegistry::is_command("hello"));
166 assert!(!CommandRegistry::is_command("/1"));
167 assert_eq!(
168 registry.execute("hello").unwrap_err().code(),
169 ErrorCode::InvalidInput
170 );
171 assert_eq!(
172 registry.execute("/missing").unwrap_err().code(),
173 ErrorCode::InvalidInput
174 );
175 }
176}