slash-core 0.1.0

Orchestration layer for the slash-command language
Documentation
use std::collections::HashMap;

use slash_lang::parser::ast::Command;

use crate::command::SlashCommand;
use crate::env::SlenvLoader;
use crate::executor::{CommandOutput, CommandRunner, ExecutionError, PipeValue};

/// A table of [`SlashCommand`] implementations dispatched by name.
///
/// Implements [`CommandRunner`] so it plugs directly into the existing
/// [`Executor`](crate::executor::Executor) orchestration engine.
pub struct CommandRegistry {
    commands: HashMap<String, Box<dyn SlashCommand>>,
    env: SlenvLoader,
}

impl CommandRegistry {
    /// Create a registry with no commands (use register() to add them).
    pub fn new(env: SlenvLoader) -> Self {
        Self {
            commands: HashMap::new(),
            env,
        }
    }

    /// Register a custom command.
    pub fn register(&mut self, cmd: Box<dyn SlashCommand>) {
        self.commands.insert(cmd.name().to_string(), cmd);
    }

    /// Get a reference to the environment loader.
    pub fn env(&self) -> &SlenvLoader {
        &self.env
    }

    /// Get a mutable reference to the environment loader.
    pub fn env_mut(&mut self) -> &mut SlenvLoader {
        &mut self.env
    }
}

impl CommandRunner for CommandRegistry {
    fn run(
        &self,
        cmd: &Command,
        input: Option<&PipeValue>,
    ) -> Result<CommandOutput, ExecutionError> {
        let handler = self
            .commands
            .get(&cmd.name)
            .ok_or_else(|| ExecutionError::Runner(format!("unknown command: /{}", cmd.name)))?;

        // Resolve $KEY in primary arg.
        let primary = cmd.primary.as_ref().map(|p| self.env.resolve(p));

        // Resolve $KEY in all arg values.
        let resolved_args: Vec<slash_lang::parser::ast::Arg> = cmd
            .args
            .iter()
            .map(|a| slash_lang::parser::ast::Arg {
                name: a.name.clone(),
                value: a.value.as_ref().map(|v| self.env.resolve(v)),
            })
            .collect();

        // Validate method names against the command's declared methods.
        let valid_methods = handler.methods();
        for arg in &resolved_args {
            if !valid_methods.iter().any(|m| m.name == arg.name) {
                let known: Vec<&str> = valid_methods.iter().map(|m| m.name).collect();
                return Err(ExecutionError::Runner(format!(
                    "/{}: unknown method '.{}' — valid methods: {}",
                    cmd.name,
                    arg.name,
                    if known.is_empty() {
                        "(none)".to_string()
                    } else {
                        known.join(", ")
                    },
                )));
            }
        }

        handler.execute(primary.as_deref(), &resolved_args, input)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::executor::{Execute, Executor};

    fn registry() -> CommandRegistry {
        CommandRegistry::new(SlenvLoader::empty())
    }

    #[test]
    fn unknown_command_errors() {
        let reg = registry();
        let ex = Executor::new(reg);
        let prog = slash_lang::parser::parse("/nonexistent").unwrap();
        assert!(ex.execute(&prog).is_err());
    }

    #[test]
    fn env_resolution_in_args() {
        let mut env = SlenvLoader::empty();
        env.insert_mut("MSG", "resolved");
        let reg = CommandRegistry::new(env);
        assert_eq!(reg.env().resolve("$MSG"), "resolved");
    }
}