use std::collections::HashMap;
use slash_lang::parser::ast::Command;
use crate::builtins;
use crate::command::SlashCommand;
use crate::env::SlenvLoader;
use crate::executor::{CommandOutput, CommandRunner, ExecutionError, PipeValue};
pub struct CommandRegistry {
commands: HashMap<String, Box<dyn SlashCommand>>,
env: SlenvLoader,
}
impl CommandRegistry {
pub fn new(env: SlenvLoader) -> Self {
let mut commands = HashMap::new();
for cmd in builtins::all() {
commands.insert(cmd.name().to_string(), cmd);
}
Self { commands, env }
}
pub fn register(&mut self, cmd: Box<dyn SlashCommand>) {
self.commands.insert(cmd.name().to_string(), cmd);
}
}
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)))?;
let primary = cmd.primary.as_ref().map(|p| self.env.resolve(p));
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();
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};
use slash_lang::parser::parse;
fn registry() -> CommandRegistry {
CommandRegistry::new(SlenvLoader::empty())
}
#[test]
fn echo_primary_arg() {
let reg = registry();
let ex = Executor::new(reg);
let prog = parse("/echo(hello world)").unwrap();
let result = ex.execute(&prog).unwrap();
match result {
Some(PipeValue::Bytes(b)) => assert_eq!(String::from_utf8(b).unwrap(), "hello world\n"),
_ => panic!("expected bytes"),
}
}
#[test]
fn echo_text_method() {
let reg = registry();
let ex = Executor::new(reg);
let prog = parse("/echo.text(hello)").unwrap();
let result = ex.execute(&prog).unwrap();
match result {
Some(PipeValue::Bytes(b)) => assert_eq!(String::from_utf8(b).unwrap(), "hello\n"),
_ => panic!("expected bytes"),
}
}
#[test]
fn read_file() {
let manifest = env!("CARGO_MANIFEST_DIR");
let path = format!("{}/Cargo.toml", manifest);
let reg = registry();
let ex = Executor::new(reg);
let prog = parse(&format!("/read({})", path)).unwrap();
let result = ex.execute(&prog).unwrap();
match result {
Some(PipeValue::Bytes(b)) => {
let s = String::from_utf8(b).unwrap();
assert!(s.contains("[package]"));
}
_ => panic!("expected bytes"),
}
}
#[test]
fn read_missing_file_fails() {
let reg = registry();
let ex = Executor::new(reg);
let prog = parse("/read(nonexistent_file_xyz.txt)").unwrap();
assert!(ex.execute(&prog).is_err());
}
#[test]
fn pipe_read_to_write_roundtrip() {
let tmp = std::env::temp_dir().join("slash_test_write.txt");
let tmp_path = tmp.display().to_string();
let reg = registry();
let ex = Executor::new(reg);
let prog = parse(&format!("/echo(test content) | /write({})", tmp_path)).unwrap();
ex.execute(&prog).unwrap();
let content = std::fs::read_to_string(&tmp).unwrap();
assert_eq!(content, "test content\n");
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn exec_runs_command() {
let reg = registry();
let ex = Executor::new(reg);
let prog = parse("/exec(echo hello)").unwrap();
let result = ex.execute(&prog).unwrap();
match result {
Some(PipeValue::Bytes(b)) => assert_eq!(String::from_utf8(b).unwrap().trim(), "hello"),
_ => panic!("expected bytes"),
}
}
#[test]
fn exec_failure_propagates() {
let reg = registry();
let ex = Executor::new(reg);
let prog = parse("/exec(false) && /echo(should not run)").unwrap();
let result = ex.execute(&prog).unwrap();
assert!(result.is_none());
}
#[test]
fn unknown_method_errors() {
let reg = registry();
let ex = Executor::new(reg);
let prog = parse("/echo.nonexistent(val)").unwrap();
let err = match ex.execute(&prog) {
Err(e) => e,
Ok(_) => panic!("expected error for unknown method"),
};
let msg = format!("{:?}", err);
assert!(msg.contains("unknown method '.nonexistent'"), "got: {msg}");
assert!(
msg.contains("text"),
"should list valid methods, got: {msg}"
);
}
#[test]
fn unknown_command_errors() {
let reg = registry();
let ex = Executor::new(reg);
let prog = 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);
let ex = Executor::new(reg);
let prog = parse("/echo($MSG)").unwrap();
let result = ex.execute(&prog).unwrap();
match result {
Some(PipeValue::Bytes(b)) => assert_eq!(String::from_utf8(b).unwrap(), "resolved\n"),
_ => panic!("expected bytes"),
}
}
#[test]
fn find_glob() {
let reg = registry();
let ex = Executor::new(reg);
let prog = parse("/find(src/*.rs)").unwrap();
let manifest = env!("CARGO_MANIFEST_DIR");
std::env::set_current_dir(manifest).unwrap();
let result = ex.execute(&prog).unwrap();
match result {
Some(PipeValue::Bytes(b)) => {
let s = String::from_utf8(b).unwrap();
assert!(s.contains("lib.rs"));
}
_ => panic!("expected bytes"),
}
}
}