modcli/
loader.rs

1#[cfg(feature = "custom-commands")]
2//pub mod custom;
3#[cfg(feature = "custom-commands")]
4//use crate::custom::CustomCommand;
5#[cfg(feature = "plugins")]
6pub mod plugins;
7
8#[cfg(feature = "plugins")]
9use crate::loader::plugins::PluginLoader;
10
11#[cfg(feature = "internal-commands")]
12use crate::commands::{FrameworkCommand, HelloCommand, HelpCommand, PingCommand, ShellCommand};
13use crate::output::hook;
14
15use crate::command::Command;
16use std::collections::HashMap;
17
18#[cfg(feature = "json-loader")]
19use crate::loader::sources::CommandSource;
20
21#[cfg(feature = "json-loader")]
22pub mod sources;
23
24/// Registry for commands and optional alias/prefix routing.
25///
26/// # Example
27/// ```no_run
28/// use modcli::loader::CommandRegistry;
29/// use modcli::command::Command;
30///
31/// struct Echo;
32/// impl Command for Echo {
33///     fn name(&self) -> &str { "echo" }
34///     fn execute(&self, args: &[String]) { println!("{}", args.join(" ")) }
35/// }
36///
37/// let mut reg = CommandRegistry::new();
38/// reg.register(Box::new(Echo));
39/// reg.execute("echo", &["hi".into()]);
40/// ```
41pub struct CommandRegistry {
42    prefix: String,
43    commands: HashMap<String, Box<dyn Command>>,
44    aliases: HashMap<String, String>,
45}
46
47impl Default for CommandRegistry {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl CommandRegistry {
54    /// Creates a new command registry
55    pub fn new() -> Self {
56        let mut reg = Self {
57            prefix: String::new(),
58            commands: HashMap::new(),
59            aliases: HashMap::new(),
60        };
61
62        #[cfg(feature = "custom-commands")]
63        reg.load_custom_commands();
64
65        #[cfg(feature = "internal-commands")]
66        reg.load_internal_commands();
67
68        reg
69    }
70
71    /// Sets the command prefix
72    /// Sets an optional prefix used for routing commands of the form `prefix:cmd`.
73    pub fn set_prefix(&mut self, prefix: &str) {
74        self.prefix = prefix.to_string();
75    }
76
77    /// Gets the command prefix
78    /// Returns the configured prefix (empty string if not set).
79    pub fn get_prefix(&self) -> &str {
80        &self.prefix
81    }
82
83    /// Gets a command by name
84    /// Gets a command by its primary name.
85    pub fn get(&self, name: &str) -> Option<&dyn Command> {
86        self.commands.get(name).map(|b| b.as_ref())
87    }
88
89    /// Gets a command by name with prefix
90    /// Registers a command and records its aliases for reverse lookup.
91    pub fn register(&mut self, cmd: Box<dyn Command>) {
92        // capture name/aliases before moving the command
93        let name = cmd.name().to_string();
94        let alias_list: Vec<String> = cmd.aliases().iter().map(|a| a.to_string()).collect();
95
96        self.commands.insert(name.clone(), cmd);
97
98        // map each alias -> primary name
99        for alias in alias_list {
100            // avoid alias clobbering existing command names
101            if !self.commands.contains_key(&alias) {
102                self.aliases.insert(alias, name.clone());
103            }
104        }
105    }
106
107    /// Returns all registered commands (read-only)
108    /// Returns an iterator over all registered commands.
109    pub fn all(&self) -> impl Iterator<Item = &Box<dyn Command>> {
110        self.commands.values()
111    }
112
113    /// Registers a command with an alias
114    #[cfg(feature = "plugins")]
115    pub fn load_plugins(&mut self, path: &str) {
116        let loader = PluginLoader::new(path);
117        for plugin in loader.load_plugins() {
118            self.register(plugin);
119        }
120    }
121
122    /// Resolves and executes a command by name or alias, with optional prefix routing.
123    pub fn execute(&self, cmd: &str, args: &[String]) {
124        // Handle optional prefix routing: `<prefix>:<command>`
125        let mut token = cmd.to_string();
126        if !self.prefix.is_empty() {
127            let expect = format!("{}:", self.prefix);
128            if token.starts_with(&expect) {
129                token = token[expect.len()..].to_string();
130            }
131        }
132
133        // resolve command by direct name or alias
134        let resolved_name = if self.commands.contains_key(&token) {
135            Some(token.clone())
136        } else {
137            self.aliases.get(&token).cloned()
138        };
139
140        if let Some(name) = resolved_name {
141            let command = &self.commands[&name];
142            // Validate before execute
143            if let Err(err) = command.validate(args) {
144                let err_msg = format!("Invalid usage: {err}");
145                hook::error(&err_msg);
146                return;
147            }
148            // Execute with registry context (help and others can leverage it)
149            command.execute_with(args, self);
150        } else {
151            let unknown =
152                format!("[{cmd}]. Type `help` or `--help` for a list of available commands.");
153            hook::unknown(&unknown);
154        }
155    }
156
157    #[cfg(feature = "internal-commands")]
158    pub fn load_internal_commands(&mut self) {
159        self.register(Box::new(PingCommand));
160        self.register(Box::new(HelloCommand));
161        self.register(Box::new(ShellCommand));
162        self.register(Box::new(FrameworkCommand));
163        self.register(Box::new(HelpCommand::new()));
164    }
165
166    #[cfg(feature = "json-loader")]
167    pub fn load_from(&mut self, source: Box<dyn CommandSource>) {
168        for cmd in source.load_commands() {
169            self.register(cmd);
170        }
171    }
172
173    pub fn len(&self) -> usize {
174        self.commands.len()
175    }
176
177    pub fn is_empty(&self) -> bool {
178        self.commands.is_empty()
179    }
180
181    #[cfg(feature = "custom-commands")]
182    pub fn load_custom_commands(&mut self) {
183        //self.register(Box::new(CustomCommand));
184    }
185}