ricecoder_execution/
step_creator.rs

1//! Step creation utilities for building execution steps
2
3use crate::error::{ExecutionError, ExecutionResult};
4use crate::models::{ExecutionStep, RollbackAction, RollbackType, StepAction};
5use ricecoder_storage::PathResolver;
6use serde_json::json;
7use std::path::Path;
8
9/// Helper for creating execution steps with validation
10pub struct StepCreator;
11
12impl StepCreator {
13    /// Create a file creation step
14    ///
15    /// # Arguments
16    /// * `path` - File path (validated with PathResolver)
17    /// * `content` - File content
18    ///
19    /// # Returns
20    /// An ExecutionStep configured to create the file
21    ///
22    /// # Errors
23    /// Returns error if path is invalid
24    pub fn create_file(path: String, content: String) -> ExecutionResult<ExecutionStep> {
25        // Validate path using PathResolver
26        let _resolved = PathResolver::expand_home(Path::new(&path))
27            .map_err(|e| ExecutionError::ValidationError(format!("Invalid path: {}", e)))?;
28
29        let mut step = ExecutionStep::new(
30            format!("Create file: {}", path),
31            StepAction::CreateFile {
32                path: path.clone(),
33                content,
34            },
35        );
36
37        // Set rollback action to delete the created file
38        step.rollback_action = Some(RollbackAction {
39            action_type: RollbackType::DeleteFile,
40            data: json!({ "path": path }),
41        });
42
43        Ok(step)
44    }
45
46    /// Create a file modification step
47    ///
48    /// # Arguments
49    /// * `path` - File path (validated with PathResolver)
50    /// * `diff` - Diff to apply
51    ///
52    /// # Returns
53    /// An ExecutionStep configured to modify the file
54    ///
55    /// # Errors
56    /// Returns error if path is invalid
57    pub fn modify_file(path: String, diff: String) -> ExecutionResult<ExecutionStep> {
58        // Validate path using PathResolver
59        let _resolved = PathResolver::expand_home(Path::new(&path))
60            .map_err(|e| ExecutionError::ValidationError(format!("Invalid path: {}", e)))?;
61
62        let mut step = ExecutionStep::new(
63            format!("Modify file: {}", path),
64            StepAction::ModifyFile {
65                path: path.clone(),
66                diff,
67            },
68        );
69
70        // Set rollback action to restore the file from backup
71        step.rollback_action = Some(RollbackAction {
72            action_type: RollbackType::RestoreFile,
73            data: json!({ "path": path }),
74        });
75
76        Ok(step)
77    }
78
79    /// Create a file deletion step
80    ///
81    /// # Arguments
82    /// * `path` - File path (validated with PathResolver)
83    ///
84    /// # Returns
85    /// An ExecutionStep configured to delete the file
86    ///
87    /// # Errors
88    /// Returns error if path is invalid
89    pub fn delete_file(path: String) -> ExecutionResult<ExecutionStep> {
90        // Validate path using PathResolver
91        let _resolved = PathResolver::expand_home(Path::new(&path))
92            .map_err(|e| ExecutionError::ValidationError(format!("Invalid path: {}", e)))?;
93
94        let mut step = ExecutionStep::new(
95            format!("Delete file: {}", path),
96            StepAction::DeleteFile { path: path.clone() },
97        );
98
99        // Set rollback action to restore the file from backup
100        step.rollback_action = Some(RollbackAction {
101            action_type: RollbackType::RestoreFile,
102            data: json!({ "path": path }),
103        });
104
105        Ok(step)
106    }
107
108    /// Create a command execution step
109    ///
110    /// # Arguments
111    /// * `command` - Command to execute
112    /// * `args` - Command arguments
113    ///
114    /// # Returns
115    /// An ExecutionStep configured to run the command
116    pub fn run_command(command: String, args: Vec<String>) -> ExecutionStep {
117        let mut step = ExecutionStep::new(
118            format!("Run command: {} {}", command, args.join(" ")),
119            StepAction::RunCommand {
120                command: command.clone(),
121                args: args.clone(),
122            },
123        );
124
125        // Set rollback action to run reverse command
126        step.rollback_action = Some(RollbackAction {
127            action_type: RollbackType::RunCommand,
128            data: json!({
129                "command": command,
130                "args": args,
131                "reverse": true
132            }),
133        });
134
135        step
136    }
137
138    /// Create a test execution step
139    ///
140    /// # Arguments
141    /// * `pattern` - Optional test pattern to filter tests
142    ///
143    /// # Returns
144    /// An ExecutionStep configured to run tests
145    pub fn run_tests(pattern: Option<String>) -> ExecutionStep {
146        let description = if let Some(ref p) = pattern {
147            format!("Run tests matching: {}", p)
148        } else {
149            "Run all tests".to_string()
150        };
151
152        ExecutionStep::new(description, StepAction::RunTests { pattern })
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_create_file_step() {
162        let result = StepCreator::create_file("test.txt".to_string(), "content".to_string());
163        assert!(result.is_ok());
164        let step = result.unwrap();
165        assert!(matches!(step.action, StepAction::CreateFile { .. }));
166        assert!(step.rollback_action.is_some());
167    }
168
169    #[test]
170    fn test_modify_file_step() {
171        let result = StepCreator::modify_file("test.txt".to_string(), "diff".to_string());
172        assert!(result.is_ok());
173        let step = result.unwrap();
174        assert!(matches!(step.action, StepAction::ModifyFile { .. }));
175        assert!(step.rollback_action.is_some());
176    }
177
178    #[test]
179    fn test_delete_file_step() {
180        let result = StepCreator::delete_file("test.txt".to_string());
181        assert!(result.is_ok());
182        let step = result.unwrap();
183        assert!(matches!(step.action, StepAction::DeleteFile { .. }));
184        assert!(step.rollback_action.is_some());
185    }
186
187    #[test]
188    fn test_run_command_step() {
189        let step = StepCreator::run_command("echo".to_string(), vec!["hello".to_string()]);
190        assert!(matches!(step.action, StepAction::RunCommand { .. }));
191        assert!(step.rollback_action.is_some());
192    }
193
194    #[test]
195    fn test_run_tests_step() {
196        let step = StepCreator::run_tests(Some("*.rs".to_string()));
197        assert!(matches!(step.action, StepAction::RunTests { .. }));
198    }
199
200    #[test]
201    fn test_rollback_action_for_create() {
202        let step = StepCreator::create_file("test.txt".to_string(), "content".to_string()).unwrap();
203        let rollback = step.rollback_action.unwrap();
204        assert_eq!(rollback.action_type, RollbackType::DeleteFile);
205    }
206
207    #[test]
208    fn test_rollback_action_for_modify() {
209        let step = StepCreator::modify_file("test.txt".to_string(), "diff".to_string()).unwrap();
210        let rollback = step.rollback_action.unwrap();
211        assert_eq!(rollback.action_type, RollbackType::RestoreFile);
212    }
213
214    #[test]
215    fn test_rollback_action_for_delete() {
216        let step = StepCreator::delete_file("test.txt".to_string()).unwrap();
217        let rollback = step.rollback_action.unwrap();
218        assert_eq!(rollback.action_type, RollbackType::RestoreFile);
219    }
220
221    #[test]
222    fn test_rollback_action_for_command() {
223        let step = StepCreator::run_command("echo".to_string(), vec!["hello".to_string()]);
224        let rollback = step.rollback_action.unwrap();
225        assert_eq!(rollback.action_type, RollbackType::RunCommand);
226    }
227}