Skip to main content

undo_core/
effect.rs

1//! An `Effect` is a single change an agent made to the world, paired with
2//! enough information to reverse it. This is the heart of the whole system:
3//! anything that can describe its own inverse — a file, a directory, a symlink,
4//! a network call — fits into the same journal and the same one-button rollback.
5
6use crate::meta::Meta;
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9
10/// A reversible (or at least auditable) side effect.
11///
12/// The `Path*` / `File` / `Symlink` / `Dir` variants are fully reversible.
13/// `HttpMutation` and `Exec` are recorded for audit and carry the shape needed
14/// to reverse them later, but are not auto-reversed yet.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "kind", rename_all = "snake_case")]
17pub enum Effect {
18    /// A path that did not exist when we captured it. Inverse: delete whatever
19    /// is now there (file, symlink, or whole directory tree).
20    PathCreate { path: PathBuf },
21
22    /// A regular file whose prior contents + metadata were captured.
23    /// Inverse: restore the contents and re-apply mode/mtime.
24    File {
25        path: PathBuf,
26        prev_blob: String,
27        #[serde(default)]
28        meta: Meta,
29    },
30
31    /// A symlink that existed at capture time. Inverse: recreate it pointing at
32    /// `target` (we snapshot the link itself, never the file it points to).
33    Symlink { path: PathBuf, target: PathBuf },
34
35    /// A directory that existed at capture time, plus the names of its immediate
36    /// children. Inverse: ensure the directory exists with its mode, and prune
37    /// any children the agent *added* that weren't here originally.
38    Dir {
39        path: PathBuf,
40        #[serde(default)]
41        mode: u32,
42        entries: Vec<String>,
43    },
44
45    /// A network mutation (POST/PUT/PATCH/DELETE). The `compensator` is the
46    /// request that reverses it (e.g. a DELETE to undo a POST). Recorded only.
47    HttpMutation {
48        method: String,
49        url: String,
50        compensator: Option<HttpCompensator>,
51    },
52
53    /// A shell command. Audit-only; arbitrary commands have no general inverse.
54    Exec { command: String, cwd: PathBuf },
55}
56
57/// The request that reverses an `HttpMutation`.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct HttpCompensator {
60    pub method: String,
61    pub url: String,
62    pub body: Option<String>,
63}
64
65impl Effect {
66    /// Can this effect be reversed automatically?
67    pub fn reversible(&self) -> bool {
68        matches!(
69            self,
70            Effect::PathCreate { .. }
71                | Effect::File { .. }
72                | Effect::Symlink { .. }
73                | Effect::Dir { .. }
74        )
75    }
76
77    /// The filesystem path this effect concerns, if any.
78    pub fn path(&self) -> Option<&Path> {
79        match self {
80            Effect::PathCreate { path }
81            | Effect::File { path, .. }
82            | Effect::Symlink { path, .. }
83            | Effect::Dir { path, .. } => Some(path),
84            _ => None,
85        }
86    }
87
88    /// A short, human-readable, log-friendly description.
89    pub fn describe(&self) -> String {
90        match self {
91            Effect::PathCreate { path } => format!("created  {}", path.display()),
92            Effect::File { path, .. } => format!("captured {}", path.display()),
93            Effect::Symlink { path, .. } => format!("symlink  {}", path.display()),
94            Effect::Dir { path, .. } => format!("dir      {}", path.display()),
95            Effect::HttpMutation { method, url, .. } => format!("{method:<8} {url}"),
96            Effect::Exec { command, .. } => format!("ran      {command}"),
97        }
98    }
99}