Skip to main content

dodot_lib/operations/
mod.rs

1//! Operation types — the atomic units of work dodot performs.
2//!
3//! dodot only does four things. Everything else is orchestration.
4
5use std::path::PathBuf;
6
7use serde::Serialize;
8
9/// The four atomic operations dodot performs.
10///
11/// Uses enum variants with associated data so that each variant carries
12/// exactly the fields it needs — impossible states are unrepresentable.
13#[derive(Debug, Clone, Serialize)]
14pub enum Operation {
15    /// Link a source file into the datastore.
16    /// `handler_data_dir(pack, handler) / filename -> source`
17    CreateDataLink {
18        pack: String,
19        handler: String,
20        source: PathBuf,
21    },
22
23    /// Create a user-visible symlink.
24    /// `user_path -> datastore_path`
25    CreateUserLink {
26        pack: String,
27        handler: String,
28        datastore_path: PathBuf,
29        user_path: PathBuf,
30    },
31
32    /// Execute a command and record a sentinel on success.
33    RunCommand {
34        pack: String,
35        handler: String,
36        executable: String,
37        arguments: Vec<String>,
38        sentinel: String,
39    },
40
41    /// Check whether a sentinel exists (query, not mutation).
42    CheckSentinel {
43        pack: String,
44        handler: String,
45        sentinel: String,
46    },
47}
48
49impl Operation {
50    pub fn pack(&self) -> &str {
51        match self {
52            Self::CreateDataLink { pack, .. }
53            | Self::CreateUserLink { pack, .. }
54            | Self::RunCommand { pack, .. }
55            | Self::CheckSentinel { pack, .. } => pack,
56        }
57    }
58
59    pub fn handler(&self) -> &str {
60        match self {
61            Self::CreateDataLink { handler, .. }
62            | Self::CreateUserLink { handler, .. }
63            | Self::RunCommand { handler, .. }
64            | Self::CheckSentinel { handler, .. } => handler,
65        }
66    }
67
68    /// Human-readable label for the operation type.
69    pub fn kind(&self) -> &'static str {
70        match self {
71            Self::CreateDataLink { .. } => "CreateDataLink",
72            Self::CreateUserLink { .. } => "CreateUserLink",
73            Self::RunCommand { .. } => "RunCommand",
74            Self::CheckSentinel { .. } => "CheckSentinel",
75        }
76    }
77}
78
79/// Higher-level intent produced by handlers.
80///
81/// Handlers declare *what* they want, not *how* to do it. The executor
82/// converts intents into [`Operation`]s and [`DataStore`](crate::datastore::DataStore) calls.
83///
84/// This avoids the awkward pattern where `CreateUserLink` would need a
85/// placeholder datastore path that the executor fills later — instead
86/// `Link` carries the full intent and the executor splits it into two
87/// atomic operations.
88#[derive(Debug, Clone, Serialize)]
89pub enum HandlerIntent {
90    /// Symlink handler: create both legs of the double-link.
91    /// Executor splits this into CreateDataLink + CreateUserLink.
92    Link {
93        pack: String,
94        handler: String,
95        source: PathBuf,
96        user_path: PathBuf,
97    },
98
99    /// Shell/path handlers: stage a file in the datastore.
100    /// Shell init reads it from there.
101    Stage {
102        pack: String,
103        handler: String,
104        source: PathBuf,
105    },
106
107    /// Install/homebrew handlers: run a command with sentinel tracking.
108    Run {
109        pack: String,
110        handler: String,
111        executable: String,
112        arguments: Vec<String>,
113        sentinel: String,
114    },
115}
116
117impl HandlerIntent {
118    pub fn pack(&self) -> &str {
119        match self {
120            Self::Link { pack, .. } | Self::Stage { pack, .. } | Self::Run { pack, .. } => pack,
121        }
122    }
123
124    pub fn handler(&self) -> &str {
125        match self {
126            Self::Link { handler, .. }
127            | Self::Stage { handler, .. }
128            | Self::Run { handler, .. } => handler,
129        }
130    }
131}
132
133/// The outcome of executing a single operation.
134#[derive(Debug, Clone, Serialize)]
135pub struct OperationResult {
136    pub operation: Operation,
137    pub success: bool,
138    pub message: String,
139}
140
141impl OperationResult {
142    pub fn ok(operation: Operation, message: impl Into<String>) -> Self {
143        Self {
144            operation,
145            success: true,
146            message: message.into(),
147        }
148    }
149
150    pub fn fail(operation: Operation, message: impl Into<String>) -> Self {
151        Self {
152            operation,
153            success: false,
154            message: message.into(),
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn operation_accessors() {
165        let op = Operation::CreateDataLink {
166            pack: "vim".into(),
167            handler: "symlink".into(),
168            source: PathBuf::from("/src/vimrc"),
169        };
170        assert_eq!(op.pack(), "vim");
171        assert_eq!(op.handler(), "symlink");
172        assert_eq!(op.kind(), "CreateDataLink");
173    }
174
175    #[test]
176    fn handler_intent_accessors() {
177        let intent = HandlerIntent::Link {
178            pack: "git".into(),
179            handler: "symlink".into(),
180            source: PathBuf::from("/src/gitconfig"),
181            user_path: PathBuf::from("/home/.gitconfig"),
182        };
183        assert_eq!(intent.pack(), "git");
184        assert_eq!(intent.handler(), "symlink");
185    }
186
187    #[test]
188    fn operation_result_constructors() {
189        let op = Operation::CheckSentinel {
190            pack: "vim".into(),
191            handler: "install".into(),
192            sentinel: "abc".into(),
193        };
194        let ok = OperationResult::ok(op.clone(), "done");
195        assert!(ok.success);
196
197        let fail = OperationResult::fail(op, "oops");
198        assert!(!fail.success);
199    }
200
201    #[test]
202    fn operation_serializes() {
203        let op = Operation::RunCommand {
204            pack: "vim".into(),
205            handler: "install".into(),
206            executable: "echo".into(),
207            arguments: vec!["hi".into()],
208            sentinel: "s1".into(),
209        };
210        let json = serde_json::to_string(&op).unwrap();
211        assert!(json.contains("RunCommand"));
212        assert!(json.contains("echo"));
213        assert!(json.contains("hi"));
214    }
215}