Skip to main content

rskit_agent/command/
registry.rs

1use std::collections::HashMap;
2
3use rskit_errors::{AppError, ErrorCode};
4
5// ── CommandHandler trait ────────────────────────────────────────────────────
6
7/// Handler for a single slash command.
8pub trait CommandHandler: Send + Sync {
9    /// Execute the command with the given arguments string.
10    fn execute(&self, args: &str) -> Result<String, AppError>;
11}
12
13/// Blanket implementation: any `Fn(&str) -> Result<String, AppError>` can be
14/// used as a handler.
15impl<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
24// ── Command ─────────────────────────────────────────────────────────────────
25
26/// A registered slash command.
27pub struct Command {
28    /// Command name without the leading slash.
29    pub name: String,
30    /// Short human-readable description shown in command listings.
31    pub description: String,
32    /// Usage string showing accepted arguments.
33    pub usage: String,
34    /// Handler invoked when the command is executed.
35    pub handler: Box<dyn CommandHandler>,
36}
37
38// ── CommandRegistry ─────────────────────────────────────────────────────────
39
40/// Registry of slash commands.
41pub 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    /// Create an empty registry.
53    pub fn new() -> Self {
54        Self {
55            commands: HashMap::new(),
56        }
57    }
58
59    /// Register a command. Returns an error if the name is empty.
60    /// Overwrites any existing command with the same name.
61    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    /// Look up a command by name (without the leading `/`).
73    pub fn get(&self, name: &str) -> Option<&Command> {
74        self.commands.get(name)
75    }
76
77    /// Return all registered commands (sorted by name for deterministic output).
78    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    /// Check whether the input looks like a slash command.
85    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    /// Parse input into `(command_name, args)`.  Returns `None` if the input
95    /// is not a slash command.
96    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    /// Execute a slash-command input string.
112    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}