1use std::collections::HashMap;
2
3use slash_lang::parser::ast::Command;
4
5use crate::builtins;
6use crate::command::SlashCommand;
7use crate::env::SlenvLoader;
8use crate::executor::{CommandOutput, CommandRunner, ExecutionError, PipeValue};
9
10pub struct CommandRegistry {
15 commands: HashMap<String, Box<dyn SlashCommand>>,
16 env: SlenvLoader,
17}
18
19impl CommandRegistry {
20 pub fn new(env: SlenvLoader) -> Self {
22 let mut commands = HashMap::new();
23 for cmd in builtins::all() {
24 commands.insert(cmd.name().to_string(), cmd);
25 }
26 Self { commands, env }
27 }
28
29 pub fn register(&mut self, cmd: Box<dyn SlashCommand>) {
31 self.commands.insert(cmd.name().to_string(), cmd);
32 }
33}
34
35impl CommandRunner for CommandRegistry {
36 fn run(
37 &self,
38 cmd: &Command,
39 input: Option<&PipeValue>,
40 ) -> Result<CommandOutput, ExecutionError> {
41 let handler = self
42 .commands
43 .get(&cmd.name)
44 .ok_or_else(|| ExecutionError::Runner(format!("unknown command: /{}", cmd.name)))?;
45
46 let primary = cmd.primary.as_ref().map(|p| self.env.resolve(p));
48
49 let resolved_args: Vec<slash_lang::parser::ast::Arg> = cmd
51 .args
52 .iter()
53 .map(|a| slash_lang::parser::ast::Arg {
54 name: a.name.clone(),
55 value: a.value.as_ref().map(|v| self.env.resolve(v)),
56 })
57 .collect();
58
59 let valid_methods = handler.methods();
61 for arg in &resolved_args {
62 if !valid_methods.iter().any(|m| m.name == arg.name) {
63 let known: Vec<&str> = valid_methods.iter().map(|m| m.name).collect();
64 return Err(ExecutionError::Runner(format!(
65 "/{}: unknown method '.{}' — valid methods: {}",
66 cmd.name,
67 arg.name,
68 if known.is_empty() {
69 "(none)".to_string()
70 } else {
71 known.join(", ")
72 },
73 )));
74 }
75 }
76
77 handler.execute(primary.as_deref(), &resolved_args, input)
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::executor::{Execute, Executor};
85 use slash_lang::parser::parse;
86
87 fn registry() -> CommandRegistry {
88 CommandRegistry::new(SlenvLoader::empty())
89 }
90
91 #[test]
92 fn echo_primary_arg() {
93 let reg = registry();
94 let ex = Executor::new(reg);
95 let prog = parse("/echo(hello world)").unwrap();
96 let result = ex.execute(&prog).unwrap();
97 match result {
98 Some(PipeValue::Bytes(b)) => assert_eq!(String::from_utf8(b).unwrap(), "hello world\n"),
99 _ => panic!("expected bytes"),
100 }
101 }
102
103 #[test]
104 fn echo_text_method() {
105 let reg = registry();
106 let ex = Executor::new(reg);
107 let prog = parse("/echo.text(hello)").unwrap();
108 let result = ex.execute(&prog).unwrap();
109 match result {
110 Some(PipeValue::Bytes(b)) => assert_eq!(String::from_utf8(b).unwrap(), "hello\n"),
111 _ => panic!("expected bytes"),
112 }
113 }
114
115 #[test]
116 fn read_file() {
117 let manifest = env!("CARGO_MANIFEST_DIR");
119 let path = format!("{}/Cargo.toml", manifest);
120 let reg = registry();
121 let ex = Executor::new(reg);
122 let prog = parse(&format!("/read({})", path)).unwrap();
123 let result = ex.execute(&prog).unwrap();
124 match result {
125 Some(PipeValue::Bytes(b)) => {
126 let s = String::from_utf8(b).unwrap();
127 assert!(s.contains("[package]"));
128 }
129 _ => panic!("expected bytes"),
130 }
131 }
132
133 #[test]
134 fn read_missing_file_fails() {
135 let reg = registry();
136 let ex = Executor::new(reg);
137 let prog = parse("/read(nonexistent_file_xyz.txt)").unwrap();
138 assert!(ex.execute(&prog).is_err());
139 }
140
141 #[test]
142 fn pipe_read_to_write_roundtrip() {
143 let tmp = std::env::temp_dir().join("slash_test_write.txt");
144 let tmp_path = tmp.display().to_string();
145 let reg = registry();
146 let ex = Executor::new(reg);
147
148 let prog = parse(&format!("/echo(test content) | /write({})", tmp_path)).unwrap();
150 ex.execute(&prog).unwrap();
151
152 let content = std::fs::read_to_string(&tmp).unwrap();
153 assert_eq!(content, "test content\n");
154 let _ = std::fs::remove_file(&tmp);
155 }
156
157 #[test]
158 fn exec_runs_command() {
159 let reg = registry();
160 let ex = Executor::new(reg);
161 let prog = parse("/exec(echo hello)").unwrap();
162 let result = ex.execute(&prog).unwrap();
163 match result {
164 Some(PipeValue::Bytes(b)) => assert_eq!(String::from_utf8(b).unwrap().trim(), "hello"),
165 _ => panic!("expected bytes"),
166 }
167 }
168
169 #[test]
170 fn exec_failure_propagates() {
171 let reg = registry();
172 let ex = Executor::new(reg);
173 let prog = parse("/exec(false) && /echo(should not run)").unwrap();
174 let result = ex.execute(&prog).unwrap();
175 assert!(result.is_none());
177 }
178
179 #[test]
180 fn unknown_method_errors() {
181 let reg = registry();
182 let ex = Executor::new(reg);
183 let prog = parse("/echo.nonexistent(val)").unwrap();
184 let err = match ex.execute(&prog) {
185 Err(e) => e,
186 Ok(_) => panic!("expected error for unknown method"),
187 };
188 let msg = format!("{:?}", err);
189 assert!(msg.contains("unknown method '.nonexistent'"), "got: {msg}");
190 assert!(
191 msg.contains("text"),
192 "should list valid methods, got: {msg}"
193 );
194 }
195
196 #[test]
197 fn unknown_command_errors() {
198 let reg = registry();
199 let ex = Executor::new(reg);
200 let prog = parse("/nonexistent").unwrap();
201 assert!(ex.execute(&prog).is_err());
202 }
203
204 #[test]
205 fn env_resolution_in_args() {
206 let mut env = SlenvLoader::empty();
207 env.insert_mut("MSG", "resolved");
208 let reg = CommandRegistry::new(env);
209 let ex = Executor::new(reg);
210 let prog = parse("/echo($MSG)").unwrap();
211 let result = ex.execute(&prog).unwrap();
212 match result {
213 Some(PipeValue::Bytes(b)) => assert_eq!(String::from_utf8(b).unwrap(), "resolved\n"),
214 _ => panic!("expected bytes"),
215 }
216 }
217
218 #[test]
219 fn find_glob() {
220 let reg = registry();
222 let ex = Executor::new(reg);
223 let prog = parse("/find(src/*.rs)").unwrap();
224
225 let manifest = env!("CARGO_MANIFEST_DIR");
227 std::env::set_current_dir(manifest).unwrap();
228
229 let result = ex.execute(&prog).unwrap();
230 match result {
231 Some(PipeValue::Bytes(b)) => {
232 let s = String::from_utf8(b).unwrap();
233 assert!(s.contains("lib.rs"));
234 }
235 _ => panic!("expected bytes"),
236 }
237 }
238}