modcli/
loader.rs

1// pub mod custom; // feature = "custom-commands"
2// use crate::custom::CustomCommand; // feature = "custom-commands"
3
4#[cfg(feature = "internal-commands")]
5use crate::commands::{FrameworkCommand, HelloCommand, HelpCommand, PingCommand};
6use crate::output::hook;
7
8use crate::command::Command;
9use crate::error::ModCliError;
10use std::collections::HashMap;
11
12/// Registry for commands and optional alias/prefix routing.
13///
14/// # Example
15/// ```no_run
16/// use modcli::loader::CommandRegistry;
17/// use modcli::command::Command;
18///
19/// struct Echo;
20/// impl Command for Echo {
21///     fn name(&self) -> &str { "echo" }
22///     fn execute(&self, args: &[String]) { println!("{}", args.join(" ")) }
23/// }
24///
25/// let mut reg = CommandRegistry::new();
26/// reg.register(Box::new(Echo));
27/// reg.execute("echo", &["hi".into()]);
28/// ```
29pub struct CommandRegistry {
30    prefix: String,
31    commands: HashMap<String, Box<dyn Command>>,
32    aliases: HashMap<String, String>,
33    #[cfg(feature = "dispatch-cache")]
34    cache: std::sync::Mutex<Option<(String, String)>>,
35}
36
37impl Default for CommandRegistry {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl CommandRegistry {
44    /// Creates a new command registry
45    pub fn new() -> Self {
46        let mut reg = Self {
47            prefix: String::new(),
48            commands: HashMap::new(),
49            aliases: HashMap::new(),
50            #[cfg(feature = "dispatch-cache")]
51            cache: std::sync::Mutex::new(None),
52        };
53
54        #[cfg(feature = "custom-commands")]
55        reg.load_custom_commands();
56
57        #[cfg(feature = "internal-commands")]
58        reg.load_internal_commands();
59
60        reg
61    }
62
63    /// Sets the command prefix
64    /// Sets an optional prefix used for routing commands of the form `prefix:cmd`.
65    pub fn set_prefix(&mut self, prefix: &str) {
66        self.prefix = prefix.to_string();
67    }
68
69    /// Gets the command prefix
70    /// Returns the configured prefix (empty string if not set).
71    pub fn get_prefix(&self) -> &str {
72        &self.prefix
73    }
74
75    /// Gets a command by name
76    /// Gets a command by its primary name.
77    #[inline(always)]
78    pub fn get(&self, name: &str) -> Option<&dyn Command> {
79        self.commands.get(name).map(|b| b.as_ref())
80    }
81
82    /// Gets a command by name with prefix
83    /// Registers a command and records its aliases for reverse lookup.
84    #[inline(always)]
85    pub fn register(&mut self, cmd: Box<dyn Command>) {
86        // capture name before moving the command
87        let name = cmd.name().to_string();
88        self.commands.insert(name.clone(), cmd);
89
90        // map each alias -> primary name without intermediate Vec allocations
91        for &alias in self.commands[&name].aliases() {
92            // avoid alias clobbering existing command names
93            if !self.commands.contains_key(alias) {
94                // store alias as owned String
95                self.aliases.insert(alias.to_string(), name.clone());
96            }
97        }
98    }
99
100    /// Returns all registered commands (read-only)
101    /// Returns an iterator over all registered commands.
102    pub fn all(&self) -> impl Iterator<Item = &Box<dyn Command>> {
103        self.commands.values()
104    }
105
106    // Note: runtime plugin loading has been removed from core for security/perf.
107
108    /// Resolves and executes a command by name or alias, with optional prefix routing.
109    ///
110    /// Behavior:
111    /// - Applies optional prefix routing (e.g., `tool:hello`).
112    /// - Resolves aliases to primary command names.
113    /// - Validates args via `Command::validate()` and logs a themed error on failure.
114    /// - Executes the command via `execute_with()`.
115    /// - Prints user-facing messages via `output::hook` and does not return an error.
116    ///
117    /// Example (illustrative):
118    /// ```ignore
119    /// use modcli::loader::CommandRegistry;
120    /// let reg = CommandRegistry::new();
121    /// // Will log an unknown command message via output hooks
122    /// reg.execute("does-not-exist", &vec![]);
123    /// ```
124    #[inline(always)]
125    pub fn execute(&self, cmd: &str, args: &[String]) {
126        if let Err(err) = self.try_execute(cmd, args) {
127            match err {
128                ModCliError::InvalidUsage(msg) => hook::error(&format!("Invalid usage: {msg}")),
129                ModCliError::UnknownCommand(name) => hook::unknown(&format!(
130                    "[{name}]. Type `help` or `--help` for a list of available commands."
131                )),
132                other => hook::error(&format!("{other}")),
133            }
134        }
135    }
136
137    /// Resolves and executes a command by name or alias, with optional prefix routing.
138    /// Returns a structured error instead of printing/logging directly.
139    ///
140    /// Error mapping:
141    /// - `InvalidUsage(String)`: when `validate()` returns an error string.
142    /// - `UnknownCommand(String)`: command not found after alias/prefix resolution.
143    ///
144    /// Examples (illustrative):
145    ///
146    /// ```ignore
147    /// use modcli::loader::CommandRegistry;
148    /// // Assume `reg` has commands registered
149    /// let reg = CommandRegistry::new();
150    /// // Success
151    /// let _ = reg.try_execute("help", &vec![]);
152    /// // Error mapping (unknown)
153    /// match reg.try_execute("does-not-exist", &vec![]) {
154    ///     Err(modcli::error::ModCliError::UnknownCommand(name)) => assert_eq!(name, "does-not-exist"),
155    ///     _ => {}
156    /// }
157    /// ```
158    #[inline(always)]
159    pub fn try_execute(&self, cmd: &str, args: &[String]) -> Result<(), ModCliError> {
160        // Strip optional prefix `<prefix>:` without intermediate allocations
161        let token: &str = if !self.prefix.is_empty() && cmd.len() > self.prefix.len() + 1 {
162            let (maybe_prefix, rest_with_colon) = cmd.split_at(self.prefix.len());
163            if maybe_prefix == self.prefix && rest_with_colon.as_bytes().first() == Some(&b':') {
164                &rest_with_colon[1..]
165            } else {
166                cmd
167            }
168        } else {
169            cmd
170        };
171
172        #[cfg(feature = "dispatch-cache")]
173        if let Ok(guard) = self.cache.lock() {
174            if let Some((ref t, ref p)) = *guard {
175                if t == token {
176                    if let Some(command) = self.commands.get(p.as_str()) {
177                        if let Err(err) = command.validate(args) {
178                            return Err(ModCliError::InvalidUsage(err));
179                        }
180                        command.execute_with(args, self);
181                        return Ok(());
182                    }
183                }
184            }
185        }
186
187        // Resolve command by direct name or alias with at most two lookups
188        if let Some(command) = self.commands.get(token) {
189            if let Err(err) = command.validate(args) {
190                return Err(ModCliError::InvalidUsage(err));
191            }
192            command.execute_with(args, self);
193            #[cfg(feature = "dispatch-cache")]
194            if let Ok(mut guard) = self.cache.lock() {
195                *guard = Some((token.to_string(), token.to_string()));
196            }
197            return Ok(());
198        }
199
200        if let Some(primary) = self.aliases.get(token) {
201            if let Some(command) = self.commands.get(primary.as_str()) {
202                if let Err(err) = command.validate(args) {
203                    return Err(ModCliError::InvalidUsage(err));
204                }
205                command.execute_with(args, self);
206                #[cfg(feature = "dispatch-cache")]
207                if let Ok(mut guard) = self.cache.lock() {
208                    *guard = Some((token.to_string(), primary.clone()));
209                }
210                return Ok(());
211            }
212        }
213
214        Err(ModCliError::UnknownCommand(cmd.to_string()))
215    }
216
217    #[cfg(feature = "internal-commands")]
218    pub fn load_internal_commands(&mut self) {
219        self.register(Box::new(PingCommand));
220        self.register(Box::new(HelloCommand));
221        self.register(Box::new(FrameworkCommand));
222        self.register(Box::new(HelpCommand::new()));
223    }
224
225    // Note: JSON loader has been removed from core. Use code registration.
226
227    pub fn len(&self) -> usize {
228        self.commands.len()
229    }
230
231    pub fn is_empty(&self) -> bool {
232        self.commands.is_empty()
233    }
234
235    #[cfg(feature = "custom-commands")]
236    pub fn load_custom_commands(&mut self) {
237        //self.register(Box::new(CustomCommand));
238    }
239}