1use std::collections::HashMap;
4
5use thiserror::Error;
6
7use super::types::{
8 CaptureStep, ErrorPolicy, LoadedMacro, MacroResult, MacroSpec, MacroStep, ShellStep,
9 StepResult, TemplateStep,
10};
11use crate::templates::engine::render_string;
12
13#[derive(Debug, Error)]
15pub enum MacroRunError {
16 #[error("step {step} failed: {message}")]
17 StepFailed { step: usize, message: String },
18
19 #[error("shell execution requires --trust flag")]
20 TrustRequired,
21
22 #[error("shell execution is disabled in config")]
23 ShellDisabled,
24
25 #[error("template error: {0}")]
26 TemplateError(String),
27
28 #[error("capture error: {0}")]
29 CaptureError(String),
30
31 #[error("shell error: {0}")]
32 ShellError(String),
33
34 #[error("variable error: {0}")]
35 VariableError(String),
36}
37
38#[derive(Debug, Clone, Default)]
40pub struct RunOptions {
41 pub trust: bool,
43
44 pub allow_shell: bool,
46
47 pub dry_run: bool,
49}
50
51#[derive(Debug, Clone)]
53pub struct RunContext {
54 pub vars: HashMap<String, String>,
56
57 pub options: RunOptions,
59
60 pub previous_results: Vec<StepResult>,
62}
63
64impl RunContext {
65 pub fn new(vars: HashMap<String, String>, options: RunOptions) -> Self {
67 Self { vars, options, previous_results: Vec::new() }
68 }
69
70 pub fn with_step_vars(
72 &self,
73 step_vars: &HashMap<String, String>,
74 ) -> HashMap<String, String> {
75 let mut merged = self.vars.clone();
76
77 for (key, value) in step_vars {
79 let rendered =
80 render_string(value, &merged).unwrap_or_else(|_| value.clone());
81 merged.insert(key.clone(), rendered);
82 }
83
84 merged
85 }
86
87 pub fn add_result(&mut self, result: StepResult) {
89 if let Some(ref path) = result.output_path {
91 let var_name = format!("step_{}_output", result.step_index);
92 self.vars.insert(var_name, path.to_string_lossy().to_string());
93 }
94 self.previous_results.push(result);
95 }
96}
97
98pub trait StepExecutor {
103 fn execute_template(
105 &self,
106 step: &TemplateStep,
107 ctx: &RunContext,
108 ) -> Result<StepResult, MacroRunError>;
109
110 fn execute_capture(
112 &self,
113 step: &CaptureStep,
114 ctx: &RunContext,
115 ) -> Result<StepResult, MacroRunError>;
116
117 fn execute_shell(
119 &self,
120 step: &ShellStep,
121 ctx: &RunContext,
122 ) -> Result<StepResult, MacroRunError>;
123}
124
125pub fn run_macro<E: StepExecutor>(
127 loaded: &LoadedMacro,
128 executor: &E,
129 mut ctx: RunContext,
130) -> MacroResult {
131 let spec = &loaded.spec;
132 let mut all_success = true;
133 let mut step_results = Vec::new();
134
135 for (index, step) in spec.steps.iter().enumerate() {
136 let result = execute_step(executor, step, index, &ctx);
137
138 match result {
139 Ok(step_result) => {
140 ctx.add_result(step_result.clone());
141 step_results.push(step_result);
142 }
143 Err(e) => {
144 all_success = false;
145 let error_result = StepResult {
146 step_index: index,
147 success: false,
148 message: e.to_string(),
149 output_path: None,
150 };
151 step_results.push(error_result);
152
153 if spec.on_error == ErrorPolicy::Abort {
155 break;
156 }
157 }
158 }
159 }
160
161 let message = if all_success {
162 format!("Completed {} steps successfully", step_results.len())
163 } else {
164 let failed_count = step_results.iter().filter(|r| !r.success).count();
165 format!(
166 "Completed with {} failures out of {} steps",
167 failed_count,
168 step_results.len()
169 )
170 };
171
172 MacroResult {
173 macro_name: loaded.logical_name.clone(),
174 step_results,
175 success: all_success,
176 message,
177 }
178}
179
180fn execute_step<E: StepExecutor>(
181 executor: &E,
182 step: &MacroStep,
183 _index: usize,
184 ctx: &RunContext,
185) -> Result<StepResult, MacroRunError> {
186 if step.requires_trust() {
188 if !ctx.options.trust {
189 return Err(MacroRunError::TrustRequired);
190 }
191 if !ctx.options.allow_shell {
192 return Err(MacroRunError::ShellDisabled);
193 }
194 }
195
196 match step {
197 MacroStep::Template(t) => executor.execute_template(t, ctx),
198 MacroStep::Capture(c) => executor.execute_capture(c, ctx),
199 MacroStep::Shell(s) => executor.execute_shell(s, ctx),
200 }
201}
202
203pub fn requires_trust(spec: &MacroSpec) -> bool {
205 spec.steps.iter().any(|s| s.requires_trust())
206}
207
208pub fn get_shell_commands(spec: &MacroSpec) -> Vec<String> {
210 spec.steps
211 .iter()
212 .filter_map(|s| match s {
213 MacroStep::Shell(shell) => Some(shell.shell.clone()),
214 _ => None,
215 })
216 .collect()
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use std::path::PathBuf;
223
224 struct MockExecutor;
225
226 impl StepExecutor for MockExecutor {
227 fn execute_template(
228 &self,
229 step: &TemplateStep,
230 _ctx: &RunContext,
231 ) -> Result<StepResult, MacroRunError> {
232 Ok(StepResult {
233 step_index: 0,
234 success: true,
235 message: format!("Created template: {}", step.template),
236 output_path: Some(PathBuf::from("test.md")),
237 })
238 }
239
240 fn execute_capture(
241 &self,
242 step: &CaptureStep,
243 _ctx: &RunContext,
244 ) -> Result<StepResult, MacroRunError> {
245 Ok(StepResult {
246 step_index: 0,
247 success: true,
248 message: format!("Executed capture: {}", step.capture),
249 output_path: None,
250 })
251 }
252
253 fn execute_shell(
254 &self,
255 step: &ShellStep,
256 _ctx: &RunContext,
257 ) -> Result<StepResult, MacroRunError> {
258 Ok(StepResult {
259 step_index: 0,
260 success: true,
261 message: format!("Executed: {}", step.shell),
262 output_path: None,
263 })
264 }
265 }
266
267 #[test]
268 fn test_run_macro_simple() {
269 let spec = MacroSpec {
270 name: "test".to_string(),
271 description: String::new(),
272 vars: None,
273 steps: vec![MacroStep::Template(TemplateStep {
274 template: "meeting".to_string(),
275 output: None,
276 vars_with: HashMap::new(),
277 })],
278 on_error: ErrorPolicy::Abort,
279 };
280
281 let loaded = LoadedMacro {
282 logical_name: "test".to_string(),
283 path: PathBuf::from("test.yaml"),
284 spec,
285 };
286
287 let ctx = RunContext::new(HashMap::new(), RunOptions::default());
288 let result = run_macro(&loaded, &MockExecutor, ctx);
289
290 assert!(result.success);
291 assert_eq!(result.step_results.len(), 1);
292 }
293
294 #[test]
295 fn test_shell_requires_trust() {
296 let spec = MacroSpec {
297 name: "test".to_string(),
298 description: String::new(),
299 vars: None,
300 steps: vec![MacroStep::Shell(ShellStep {
301 shell: "echo hello".to_string(),
302 description: String::new(),
303 })],
304 on_error: ErrorPolicy::Abort,
305 };
306
307 let loaded = LoadedMacro {
308 logical_name: "test".to_string(),
309 path: PathBuf::from("test.yaml"),
310 spec,
311 };
312
313 let ctx = RunContext::new(HashMap::new(), RunOptions::default());
315 let result = run_macro(&loaded, &MockExecutor, ctx);
316 assert!(!result.success);
317
318 let ctx = RunContext::new(
320 HashMap::new(),
321 RunOptions { trust: true, allow_shell: false, dry_run: false },
322 );
323 let result = run_macro(&loaded, &MockExecutor, ctx);
324 assert!(!result.success);
325
326 let ctx = RunContext::new(
328 HashMap::new(),
329 RunOptions { trust: true, allow_shell: true, dry_run: false },
330 );
331 let result = run_macro(&loaded, &MockExecutor, ctx);
332 assert!(result.success);
333 }
334
335 #[test]
336 fn test_requires_trust_check() {
337 let spec_with_shell = MacroSpec {
338 name: "test".to_string(),
339 description: String::new(),
340 vars: None,
341 steps: vec![
342 MacroStep::Template(TemplateStep {
343 template: "meeting".to_string(),
344 output: None,
345 vars_with: HashMap::new(),
346 }),
347 MacroStep::Shell(ShellStep {
348 shell: "git add .".to_string(),
349 description: String::new(),
350 }),
351 ],
352 on_error: ErrorPolicy::Abort,
353 };
354
355 let spec_without_shell = MacroSpec {
356 name: "test".to_string(),
357 description: String::new(),
358 vars: None,
359 steps: vec![MacroStep::Template(TemplateStep {
360 template: "meeting".to_string(),
361 output: None,
362 vars_with: HashMap::new(),
363 })],
364 on_error: ErrorPolicy::Abort,
365 };
366
367 assert!(requires_trust(&spec_with_shell));
368 assert!(!requires_trust(&spec_without_shell));
369 }
370}