Skip to main content

ralph_workflow/app/
effect_handler.rs

1//! Real implementation of `AppEffectHandler`.
2//!
3//! This handler executes actual side effects for production use.
4//! It provides concrete implementations for all [`AppEffect`] variants
5//! by delegating to the appropriate system calls or internal modules.
6
7use crate::app::effect::{AppEffect, AppEffectHandler, AppEffectResult, CommitResult};
8use std::path::{Path, PathBuf};
9
10pub struct RealAppEffectHandler {
11    workspace_root: Option<PathBuf>,
12}
13
14impl RealAppEffectHandler {
15    #[must_use]
16    pub const fn new() -> Self {
17        Self {
18            workspace_root: None,
19        }
20    }
21
22    #[must_use]
23    pub const fn with_workspace_root(root: PathBuf) -> Self {
24        Self {
25            workspace_root: Some(root),
26        }
27    }
28
29    fn resolve_path(&self, path: &Path) -> PathBuf {
30        crate::app::io::effect_io::resolve_path(&self.workspace_root, path)
31    }
32
33    fn execute_set_current_dir(&self, path: &Path) -> AppEffectResult {
34        let resolved = self.resolve_path(path);
35        match crate::app::io::effect_io::set_current_dir(&resolved) {
36            Ok(()) => AppEffectResult::Ok,
37            Err(error) => AppEffectResult::Error(format!(
38                "Failed to set current directory to '{}': {}",
39                resolved.display(),
40                error
41            )),
42        }
43    }
44
45    fn execute_write_file(&self, path: &Path, content: String) -> AppEffectResult {
46        let resolved = self.resolve_path(path);
47        match crate::app::io::effect_io::write_file(&resolved, content) {
48            Ok(()) => AppEffectResult::Ok,
49            Err(error) => AppEffectResult::Error(format!(
50                "Failed to write file '{}': {}",
51                resolved.display(),
52                error
53            )),
54        }
55    }
56
57    fn execute_read_file(&self, path: &Path) -> AppEffectResult {
58        let resolved = self.resolve_path(path);
59        match crate::app::io::effect_io::read_file(&resolved) {
60            Ok(content) => AppEffectResult::String(content),
61            Err(error) => AppEffectResult::Error(format!(
62                "Failed to read file '{}': {}",
63                resolved.display(),
64                error
65            )),
66        }
67    }
68
69    fn execute_delete_file(&self, path: &Path) -> AppEffectResult {
70        let resolved = self.resolve_path(path);
71        match crate::app::io::effect_io::delete_file(&resolved) {
72            Ok(()) => AppEffectResult::Ok,
73            Err(error) => AppEffectResult::Error(format!(
74                "Failed to delete file '{}': {}",
75                resolved.display(),
76                error
77            )),
78        }
79    }
80
81    fn execute_create_dir(&self, path: &Path) -> AppEffectResult {
82        let resolved = self.resolve_path(path);
83        match crate::app::io::effect_io::create_dir(&resolved) {
84            Ok(()) => AppEffectResult::Ok,
85            Err(error) => AppEffectResult::Error(format!(
86                "Failed to create directory '{}': {}",
87                resolved.display(),
88                error
89            )),
90        }
91    }
92
93    fn execute_path_exists(&self, path: &Path) -> AppEffectResult {
94        let resolved = self.resolve_path(path);
95        AppEffectResult::Bool(crate::app::io::effect_io::path_exists(&resolved))
96    }
97
98    fn execute_set_read_only(&self, path: &Path, readonly: bool) -> AppEffectResult {
99        let resolved = self.resolve_path(path);
100        match crate::app::io::effect_io::set_read_only(&resolved, readonly) {
101            Ok(()) => AppEffectResult::Ok,
102            Err(error) => AppEffectResult::Error(format!(
103                "Failed to set permissions on '{}': {}",
104                resolved.display(),
105                error
106            )),
107        }
108    }
109
110    fn execute_git_require_repo() -> AppEffectResult {
111        match crate::git_helpers::require_git_repo() {
112            Ok(()) => AppEffectResult::Ok,
113            Err(error) => AppEffectResult::Error(format!("Not in a git repository: {error}")),
114        }
115    }
116
117    fn execute_git_get_repo_root() -> AppEffectResult {
118        match crate::git_helpers::get_repo_root() {
119            Ok(root) => AppEffectResult::Path(root),
120            Err(error) => AppEffectResult::Error(format!("Failed to get repository root: {error}")),
121        }
122    }
123
124    fn execute_git_get_head_oid() -> AppEffectResult {
125        match crate::git_helpers::get_current_head_oid() {
126            Ok(oid) => AppEffectResult::String(oid),
127            Err(error) => AppEffectResult::Error(format!("Failed to get HEAD OID: {error}")),
128        }
129    }
130
131    fn execute_git_diff() -> AppEffectResult {
132        match crate::git_helpers::git_diff() {
133            Ok(diff) => AppEffectResult::String(diff),
134            Err(error) => AppEffectResult::Error(format!("Failed to get git diff: {error}")),
135        }
136    }
137
138    fn execute_git_diff_from(start_oid: &str) -> AppEffectResult {
139        match crate::git_helpers::git_diff_from(start_oid) {
140            Ok(diff) => AppEffectResult::String(diff),
141            Err(error) => {
142                AppEffectResult::Error(format!("Failed to get diff from '{start_oid}': {error}"))
143            }
144        }
145    }
146
147    fn execute_git_diff_from_start() -> AppEffectResult {
148        match crate::git_helpers::get_git_diff_from_start() {
149            Ok(diff) => AppEffectResult::String(diff),
150            Err(error) => {
151                AppEffectResult::Error(format!("Failed to get diff from start commit: {error}"))
152            }
153        }
154    }
155
156    fn execute_git_snapshot() -> AppEffectResult {
157        match crate::git_helpers::git_snapshot() {
158            Ok(snapshot) => AppEffectResult::String(snapshot),
159            Err(error) => AppEffectResult::Error(format!("Failed to create git snapshot: {error}")),
160        }
161    }
162
163    fn execute_git_add_all() -> AppEffectResult {
164        match crate::git_helpers::git_add_all() {
165            Ok(staged) => AppEffectResult::Bool(staged),
166            Err(error) => AppEffectResult::Error(format!("Failed to stage all changes: {error}")),
167        }
168    }
169
170    fn execute_git_commit(
171        message: &str,
172        user_name: Option<&str>,
173        user_email: Option<&str>,
174    ) -> AppEffectResult {
175        match crate::git_helpers::git_commit(message, user_name, user_email, None, None) {
176            Ok(Some(oid)) => AppEffectResult::Commit(CommitResult::Success(oid.to_string())),
177            Ok(None) => AppEffectResult::Commit(CommitResult::NoChanges),
178            Err(error) => AppEffectResult::Error(format!("Failed to create commit: {error}")),
179        }
180    }
181
182    fn execute_git_save_start_commit() -> AppEffectResult {
183        match crate::git_helpers::save_start_commit() {
184            Ok(()) => AppEffectResult::Ok,
185            Err(error) => AppEffectResult::Error(format!("Failed to save start commit: {error}")),
186        }
187    }
188
189    fn execute_git_reset_start_commit() -> AppEffectResult {
190        match crate::git_helpers::reset_start_commit() {
191            Ok(result) => AppEffectResult::String(result.oid),
192            Err(error) => AppEffectResult::Error(format!("Failed to reset start commit: {error}")),
193        }
194    }
195
196    fn execute_git_rebase_onto(_upstream_branch: String) -> AppEffectResult {
197        AppEffectResult::Error(
198            "GitRebaseOnto requires executor injection - use pipeline runner".to_string(),
199        )
200    }
201
202    fn execute_git_get_conflicted_files() -> AppEffectResult {
203        match crate::git_helpers::get_conflicted_files() {
204            Ok(files) => AppEffectResult::StringList(files),
205            Err(error) => {
206                AppEffectResult::Error(format!("Failed to get conflicted files: {error}"))
207            }
208        }
209    }
210
211    fn execute_git_continue_rebase() -> AppEffectResult {
212        AppEffectResult::Error(
213            "GitContinueRebase requires executor injection - use pipeline runner".to_string(),
214        )
215    }
216
217    fn execute_git_abort_rebase() -> AppEffectResult {
218        AppEffectResult::Error(
219            "GitAbortRebase requires executor injection - use pipeline runner".to_string(),
220        )
221    }
222
223    fn execute_git_get_default_branch() -> AppEffectResult {
224        match crate::git_helpers::get_default_branch() {
225            Ok(branch) => AppEffectResult::String(branch),
226            Err(error) => AppEffectResult::Error(format!("Failed to get default branch: {error}")),
227        }
228    }
229
230    fn execute_git_is_main_branch() -> AppEffectResult {
231        match crate::git_helpers::is_main_or_master_branch() {
232            Ok(is_main) => AppEffectResult::Bool(is_main),
233            Err(error) => AppEffectResult::Error(format!("Failed to check branch: {error}")),
234        }
235    }
236
237    fn execute_get_env_var(name: &str) -> AppEffectResult {
238        match crate::app::io::effect_io::get_env_var(name) {
239            Ok(value) => AppEffectResult::String(value),
240            Err(std::env::VarError::NotPresent) => {
241                AppEffectResult::Error(format!("Environment variable '{name}' not set"))
242            }
243            Err(std::env::VarError::NotUnicode(_)) => AppEffectResult::Error(format!(
244                "Environment variable '{name}' contains invalid Unicode"
245            )),
246        }
247    }
248
249    fn execute_set_env_var(name: &str, value: &str) -> AppEffectResult {
250        crate::app::io::effect_io::set_env_var(name, value);
251        AppEffectResult::Ok
252    }
253}
254
255impl Default for RealAppEffectHandler {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261impl AppEffectHandler for RealAppEffectHandler {
262    fn execute(&mut self, effect: AppEffect) -> AppEffectResult {
263        match effect {
264            AppEffect::SetCurrentDir { path } => self.execute_set_current_dir(&path),
265            AppEffect::WriteFile { path, content } => self.execute_write_file(&path, content),
266            AppEffect::ReadFile { path } => self.execute_read_file(&path),
267            AppEffect::DeleteFile { path } => self.execute_delete_file(&path),
268            AppEffect::CreateDir { path } => self.execute_create_dir(&path),
269            AppEffect::PathExists { path } => self.execute_path_exists(&path),
270            AppEffect::SetReadOnly { path, readonly } => {
271                self.execute_set_read_only(&path, readonly)
272            }
273            AppEffect::GitRequireRepo => Self::execute_git_require_repo(),
274            AppEffect::GitGetRepoRoot => Self::execute_git_get_repo_root(),
275            AppEffect::GitGetHeadOid => Self::execute_git_get_head_oid(),
276            AppEffect::GitDiff => Self::execute_git_diff(),
277            AppEffect::GitDiffFrom { start_oid } => Self::execute_git_diff_from(&start_oid),
278            AppEffect::GitDiffFromStart => Self::execute_git_diff_from_start(),
279            AppEffect::GitSnapshot => Self::execute_git_snapshot(),
280            AppEffect::GitAddAll => Self::execute_git_add_all(),
281            AppEffect::GitCommit {
282                message,
283                user_name,
284                user_email,
285            } => Self::execute_git_commit(&message, user_name.as_deref(), user_email.as_deref()),
286            AppEffect::GitSaveStartCommit => Self::execute_git_save_start_commit(),
287            AppEffect::GitResetStartCommit => Self::execute_git_reset_start_commit(),
288            AppEffect::GitRebaseOnto { upstream_branch } => {
289                Self::execute_git_rebase_onto(upstream_branch)
290            }
291            AppEffect::GitGetConflictedFiles => Self::execute_git_get_conflicted_files(),
292            AppEffect::GitContinueRebase => Self::execute_git_continue_rebase(),
293            AppEffect::GitAbortRebase => Self::execute_git_abort_rebase(),
294            AppEffect::GitGetDefaultBranch => Self::execute_git_get_default_branch(),
295            AppEffect::GitIsMainBranch => Self::execute_git_is_main_branch(),
296            AppEffect::GetEnvVar { name } => Self::execute_get_env_var(&name),
297            AppEffect::SetEnvVar { name, value } => Self::execute_set_env_var(&name, &value),
298            AppEffect::LogInfo { message: _ }
299            | AppEffect::LogSuccess { message: _ }
300            | AppEffect::LogWarn { message: _ }
301            | AppEffect::LogError { message: _ } => AppEffectResult::Ok,
302        }
303    }
304}