Skip to main content

basalt_api/command/
registry.rs

1//! Command registry for looking up and executing commands by name.
2
3use std::collections::HashMap;
4
5use crate::context::Context;
6
7use super::args::CommandArgs;
8use super::dispatch::Command;
9
10/// A registry of named commands.
11///
12/// Commands are registered at startup and looked up by name during
13/// play. The registry is immutable after construction.
14pub struct CommandRegistry {
15    commands: HashMap<String, Box<dyn Command>>,
16}
17
18impl CommandRegistry {
19    /// Creates an empty command registry.
20    pub fn new() -> Self {
21        Self {
22            commands: HashMap::new(),
23        }
24    }
25
26    /// Registers a command. Overwrites any existing command with
27    /// the same name.
28    pub fn register(&mut self, command: impl Command + 'static) {
29        let name = command.name().to_string();
30        self.commands.insert(name, Box::new(command));
31    }
32
33    /// Executes a command by name with parsed arguments.
34    ///
35    /// Returns `true` if the command was found and executed.
36    pub fn execute(&self, name: &str, args: &CommandArgs, ctx: &dyn Context) -> bool {
37        if let Some(cmd) = self.commands.get(name) {
38            cmd.execute(args, ctx);
39            true
40        } else {
41            false
42        }
43    }
44
45    /// Returns an iterator over all registered commands.
46    pub fn commands(&self) -> impl Iterator<Item = &dyn Command> {
47        self.commands.values().map(|c| c.as_ref())
48    }
49
50    /// Looks up a command by name.
51    pub fn get(&self, name: &str) -> Option<&dyn Command> {
52        self.commands.get(name).map(|c| c.as_ref())
53    }
54
55    /// Returns the number of registered commands.
56    pub fn len(&self) -> usize {
57        self.commands.len()
58    }
59
60    /// Returns true if no commands are registered.
61    pub fn is_empty(&self) -> bool {
62        self.commands.is_empty()
63    }
64}
65
66impl Default for CommandRegistry {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use crate::testing::NoopContext;
75
76    use super::super::args::Arg;
77    use super::*;
78
79    struct PingCommand;
80    impl Command for PingCommand {
81        fn name(&self) -> &str {
82            "ping"
83        }
84        fn description(&self) -> &str {
85            "Responds with pong"
86        }
87        fn execute(&self, _args: &CommandArgs, ctx: &dyn Context) {
88            ctx.chat().send("Pong!");
89        }
90    }
91
92    struct EchoCommand;
93    impl Command for EchoCommand {
94        fn name(&self) -> &str {
95            "echo"
96        }
97        fn description(&self) -> &str {
98            "Echoes the arguments"
99        }
100        fn execute(&self, args: &CommandArgs, ctx: &dyn Context) {
101            ctx.chat().send(args.raw());
102        }
103    }
104
105    #[test]
106    fn register_and_execute() {
107        let mut registry = CommandRegistry::new();
108        registry.register(PingCommand);
109
110        let args = CommandArgs::new(String::new());
111        let ctx = NoopContext;
112        assert!(registry.execute("ping", &args, &ctx));
113    }
114
115    #[test]
116    fn unknown_command_returns_false() {
117        let registry = CommandRegistry::new();
118        let args = CommandArgs::new(String::new());
119        assert!(!registry.execute("nonexistent", &args, &NoopContext));
120    }
121
122    #[test]
123    fn multiple_commands() {
124        let mut registry = CommandRegistry::new();
125        registry.register(PingCommand);
126        registry.register(EchoCommand);
127        assert_eq!(registry.len(), 2);
128    }
129
130    #[test]
131    fn get_command() {
132        let mut registry = CommandRegistry::new();
133        registry.register(PingCommand);
134        assert!(registry.get("ping").is_some());
135        assert!(registry.get("nonexistent").is_none());
136    }
137
138    #[test]
139    fn iterate_commands() {
140        let mut registry = CommandRegistry::new();
141        registry.register(PingCommand);
142        registry.register(EchoCommand);
143        let names: Vec<&str> = registry.commands().map(|c| c.name()).collect();
144        assert_eq!(names.len(), 2);
145    }
146
147    #[test]
148    fn empty_registry() {
149        let registry = CommandRegistry::new();
150        assert!(registry.is_empty());
151    }
152
153    #[test]
154    fn default_is_empty() {
155        let registry = CommandRegistry::default();
156        assert!(registry.is_empty());
157    }
158
159    #[test]
160    fn parse_and_execute() {
161        let mut registry = CommandRegistry::new();
162        registry.register(EchoCommand);
163
164        let schema = vec![super::super::args::CommandArg {
165            name: "msg".into(),
166            arg_type: Arg::Message,
167            validation: super::super::args::Validation::Auto,
168            required: true,
169        }];
170        let args = super::super::args::parse_args("hello world", &schema).unwrap();
171        registry.execute("echo", &args, &NoopContext);
172    }
173}