Skip to main content

ralph_workflow/app/
effect.rs

1//! App-level effects for pre-pipeline operations.
2//!
3//! This module defines effects that represent side effects in the CLI layer
4//! before the pipeline reducer takes over. Effects are data describing what
5//! should happen, not the execution itself.
6//!
7//! # Architecture
8//!
9//! Effects follow the functional core / imperative shell pattern:
10//! - Pure functions produce [`AppEffect`] values describing desired operations
11//! - An [`AppEffectHandler`] executes the effects, performing actual I/O
12//! - This separation enables testing without real filesystem or git operations
13//!
14//! # Example
15//!
16//! ```ignore
17//! // Pure function returns effects (testable)
18//! fn setup_workspace() -> Vec<AppEffect> {
19//!     vec![
20//!         AppEffect::CreateDir { path: PathBuf::from(".agent") },
21//!         AppEffect::WriteFile {
22//!             path: PathBuf::from(".agent/config.toml"),
23//!             content: "key = value".to_string(),
24//!         },
25//!     ]
26//! }
27//!
28//! // Handler executes effects (I/O boundary)
29//! for effect in setup_workspace() {
30//!     handler.execute(effect);
31//! }
32//! ```
33
34use serde::{Deserialize, Serialize};
35use std::path::PathBuf;
36
37/// Result of a git commit operation.
38///
39/// Indicates whether a commit was successfully created or if there were
40/// no changes to commit.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum CommitResult {
43    /// Commit succeeded with the given OID (object identifier).
44    Success(String),
45    /// No changes were staged to commit.
46    NoChanges,
47}
48
49/// Result of a rebase operation.
50///
51/// Captures the various outcomes possible when rebasing a branch onto
52/// an upstream branch, including success, conflicts, and failures.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub enum RebaseResult {
55    /// Rebase completed successfully with no conflicts.
56    Success,
57    /// Rebase resulted in conflicts that need resolution.
58    ///
59    /// Contains the list of conflicted file paths.
60    Conflicts(Vec<String>),
61    /// No rebase was needed (already up-to-date or same commit).
62    NoOp {
63        /// Human-readable explanation of why no rebase was needed.
64        reason: String,
65    },
66    /// Rebase failed with an error.
67    Failed(String),
68}
69
70/// App-level effects for CLI operations.
71///
72/// Each variant represents a side effect that can occur during CLI
73/// operations. Effects are data structures that describe what should
74/// happen without actually performing the operation.
75///
76/// # Categories
77///
78/// Effects are organized into logical categories:
79/// - **Working Directory**: Process working directory management
80/// - **Filesystem**: File and directory operations
81/// - **Git**: Version control operations
82/// - **Environment**: Environment variable access
83/// - **Logging**: User-facing output
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub enum AppEffect {
86    // =========================================================================
87    // Working Directory Effects
88    // =========================================================================
89    /// Set the current working directory for the process.
90    SetCurrentDir {
91        /// The path to set as the current directory.
92        path: PathBuf,
93    },
94
95    // =========================================================================
96    // Filesystem Effects
97    // =========================================================================
98    /// Write content to a file, creating it if it doesn't exist.
99    WriteFile {
100        /// Path to the file to write.
101        path: PathBuf,
102        /// Content to write to the file.
103        content: String,
104    },
105
106    /// Read the contents of a file.
107    ReadFile {
108        /// Path to the file to read.
109        path: PathBuf,
110    },
111
112    /// Delete a file.
113    DeleteFile {
114        /// Path to the file to delete.
115        path: PathBuf,
116    },
117
118    /// Create a directory and all parent directories as needed.
119    CreateDir {
120        /// Path to the directory to create.
121        path: PathBuf,
122    },
123
124    /// Check if a path exists.
125    PathExists {
126        /// Path to check for existence.
127        path: PathBuf,
128    },
129
130    /// Set or clear the read-only flag on a file.
131    SetReadOnly {
132        /// Path to the file to modify.
133        path: PathBuf,
134        /// Whether to make the file read-only.
135        readonly: bool,
136    },
137
138    // =========================================================================
139    // Git Effects
140    // =========================================================================
141    /// Verify that we're in a git repository.
142    GitRequireRepo,
143
144    /// Get the root directory of the git repository.
145    GitGetRepoRoot,
146
147    /// Get the OID (object identifier) of HEAD.
148    GitGetHeadOid,
149
150    /// Get the diff of uncommitted changes.
151    GitDiff,
152
153    /// Get the diff from a specific commit OID to HEAD.
154    GitDiffFrom {
155        /// The starting commit OID.
156        start_oid: String,
157    },
158
159    /// Get the diff from the saved start commit to HEAD.
160    GitDiffFromStart,
161
162    /// Create a snapshot of the current state (stash-like operation).
163    GitSnapshot,
164
165    /// Stage all changes for commit.
166    GitAddAll,
167
168    /// Create a commit with the given message.
169    GitCommit {
170        /// The commit message.
171        message: String,
172        /// Optional user name for the commit author.
173        user_name: Option<String>,
174        /// Optional user email for the commit author.
175        user_email: Option<String>,
176    },
177
178    /// Save the current HEAD as the start commit reference.
179    GitSaveStartCommit,
180
181    /// Reset the start commit reference to the merge-base.
182    GitResetStartCommit,
183
184    /// Rebase the current branch onto an upstream branch.
185    GitRebaseOnto {
186        /// The upstream branch to rebase onto.
187        upstream_branch: String,
188    },
189
190    /// Get the list of files with merge conflicts.
191    GitGetConflictedFiles,
192
193    /// Continue an in-progress rebase after conflicts are resolved.
194    GitContinueRebase,
195
196    /// Abort an in-progress rebase.
197    GitAbortRebase,
198
199    /// Get the default branch name (main or master).
200    GitGetDefaultBranch,
201
202    /// Check if the current branch is main or master.
203    GitIsMainBranch,
204
205    // =========================================================================
206    // Environment Effects
207    // =========================================================================
208    /// Get the value of an environment variable.
209    GetEnvVar {
210        /// Name of the environment variable.
211        name: String,
212    },
213
214    /// Set an environment variable.
215    SetEnvVar {
216        /// Name of the environment variable.
217        name: String,
218        /// Value to set.
219        value: String,
220    },
221
222    // =========================================================================
223    // Logging Effects
224    // =========================================================================
225    /// Log an informational message.
226    LogInfo {
227        /// The message to log.
228        message: String,
229    },
230
231    /// Log a success message.
232    LogSuccess {
233        /// The message to log.
234        message: String,
235    },
236
237    /// Log a warning message.
238    LogWarn {
239        /// The message to log.
240        message: String,
241    },
242
243    /// Log an error message.
244    LogError {
245        /// The message to log.
246        message: String,
247    },
248}
249
250/// Result of executing an [`AppEffect`].
251///
252/// Each effect execution produces a result that either indicates success
253/// (with optional return data) or an error. The variant used depends on
254/// what data the effect produces.
255#[derive(Debug, Clone)]
256pub enum AppEffectResult {
257    /// Operation completed with no return value.
258    Ok,
259    /// Operation returned a string value.
260    String(String),
261    /// Operation returned a path value.
262    Path(PathBuf),
263    /// Operation returned a boolean value.
264    Bool(bool),
265    /// Commit operation result.
266    Commit(CommitResult),
267    /// Rebase operation result.
268    Rebase(RebaseResult),
269    /// Operation returned a list of strings.
270    StringList(Vec<String>),
271    /// Operation failed with an error message.
272    Error(String),
273}
274
275/// Trait for executing app-level effects.
276///
277/// Implementors of this trait perform the actual I/O operations described
278/// by [`AppEffect`] values. This separation enables:
279/// - **Testing**: Mock handlers can record effects without performing I/O
280/// - **Batching**: Handlers can optimize by batching similar operations
281/// - **Logging**: Handlers can log all operations for debugging
282///
283/// # Example
284///
285/// ```ignore
286/// struct MockHandler {
287///     effects: Vec<AppEffect>,
288/// }
289///
290/// impl AppEffectHandler for MockHandler {
291///     fn execute(&mut self, effect: AppEffect) -> AppEffectResult {
292///         self.effects.push(effect.clone());
293///         AppEffectResult::Ok
294///     }
295/// }
296/// ```
297pub trait AppEffectHandler {
298    /// Execute an effect and return the result.
299    ///
300    /// Implementations should:
301    /// - Perform the actual I/O operation described by the effect
302    /// - Return the appropriate result variant
303    /// - Return `AppEffectResult::Error` if the operation fails
304    fn execute(&mut self, effect: AppEffect) -> AppEffectResult;
305}