Skip to main content

slash_core/
registry.rs

1use std::collections::HashMap;
2
3use slash_lang::parser::ast::Command;
4
5use crate::command::SlashCommand;
6use crate::env::SlenvLoader;
7use crate::executor::{CommandOutput, CommandRunner, ExecutionError, PipeValue};
8
9/// A table of [`SlashCommand`] implementations dispatched by name.
10///
11/// Implements [`CommandRunner`] so it plugs directly into the existing
12/// [`Executor`](crate::executor::Executor) orchestration engine.
13pub struct CommandRegistry {
14    commands: HashMap<String, Box<dyn SlashCommand>>,
15    env: SlenvLoader,
16}
17
18impl CommandRegistry {
19    /// Create a registry with no commands (use register() to add them).
20    pub fn new(env: SlenvLoader) -> Self {
21        Self {
22            commands: HashMap::new(),
23            env,
24        }
25    }
26
27    /// Register a custom command.
28    pub fn register(&mut self, cmd: Box<dyn SlashCommand>) {
29        self.commands.insert(cmd.name().to_string(), cmd);
30    }
31
32    /// Get a reference to the environment loader.
33    pub fn env(&self) -> &SlenvLoader {
34        &self.env
35    }
36
37    /// Get a mutable reference to the environment loader.
38    pub fn env_mut(&mut self) -> &mut SlenvLoader {
39        &mut self.env
40    }
41}
42
43impl CommandRunner for CommandRegistry {
44    fn run(
45        &self,
46        cmd: &Command,
47        input: Option<&PipeValue>,
48    ) -> Result<CommandOutput, ExecutionError> {
49        let handler = self
50            .commands
51            .get(&cmd.name)
52            .ok_or_else(|| ExecutionError::Runner(format!("unknown command: /{}", cmd.name)))?;
53
54        // Resolve $KEY in primary arg.
55        let primary = cmd.primary.as_ref().map(|p| self.env.resolve(p));
56
57        // Resolve $KEY in all arg values.
58        let resolved_args: Vec<slash_lang::parser::ast::Arg> = cmd
59            .args
60            .iter()
61            .map(|a| slash_lang::parser::ast::Arg {
62                name: a.name.clone(),
63                value: a.value.as_ref().map(|v| self.env.resolve(v)),
64            })
65            .collect();
66
67        // Validate method names against the command's declared methods.
68        let valid_methods = handler.methods();
69        for arg in &resolved_args {
70            if !valid_methods.iter().any(|m| m.name == arg.name) {
71                let known: Vec<&str> = valid_methods.iter().map(|m| m.name).collect();
72                return Err(ExecutionError::Runner(format!(
73                    "/{}: unknown method '.{}' — valid methods: {}",
74                    cmd.name,
75                    arg.name,
76                    if known.is_empty() {
77                        "(none)".to_string()
78                    } else {
79                        known.join(", ")
80                    },
81                )));
82            }
83        }
84
85        handler.execute(primary.as_deref(), &resolved_args, input)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::executor::{Execute, Executor};
93
94    fn registry() -> CommandRegistry {
95        CommandRegistry::new(SlenvLoader::empty())
96    }
97
98    #[test]
99    fn unknown_command_errors() {
100        let reg = registry();
101        let ex = Executor::new(reg);
102        let prog = slash_lang::parser::parse("/nonexistent").unwrap();
103        assert!(ex.execute(&prog).is_err());
104    }
105
106    #[test]
107    fn env_resolution_in_args() {
108        let mut env = SlenvLoader::empty();
109        env.insert_mut("MSG", "resolved");
110        let reg = CommandRegistry::new(env);
111        assert_eq!(reg.env().resolve("$MSG"), "resolved");
112    }
113}