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}