1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Action {
12 pub kind: ActionKind,
14
15 pub target: Option<String>,
17
18 pub args: HashMap<String, String>,
20
21 pub reason: Option<String>,
23}
24
25impl Action {
26 pub fn new(kind: ActionKind) -> Self {
28 Self {
29 kind,
30 target: None,
31 args: HashMap::new(),
32 reason: None,
33 }
34 }
35
36 pub fn with_target(mut self, target: impl Into<String>) -> Self {
38 self.target = Some(target.into());
39 self
40 }
41
42 pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
44 self.args.insert(key.into(), value.into());
45 self
46 }
47
48 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
50 self.reason = Some(reason.into());
51 self
52 }
53
54 pub fn read(path: impl Into<String>) -> Self {
56 Self::new(ActionKind::Read).with_target(path)
57 }
58
59 pub fn grep(pattern: impl Into<String>) -> Self {
61 Self::new(ActionKind::Grep).with_arg("pattern", pattern)
62 }
63
64 pub fn glob(pattern: impl Into<String>) -> Self {
66 Self::new(ActionKind::Glob).with_arg("pattern", pattern)
67 }
68
69 pub fn mutate(mutation_type: impl Into<String>, target: impl Into<String>) -> Self {
71 Self::new(ActionKind::Mutate)
72 .with_arg("mutation_type", mutation_type)
73 .with_target(target)
74 }
75
76 pub fn rest(reason: impl Into<String>) -> Self {
78 Self::new(ActionKind::Rest).with_reason(reason)
79 }
80
81 pub fn done() -> Self {
83 Self::new(ActionKind::Done)
84 }
85
86 pub fn is_terminal(&self) -> bool {
88 matches!(self.kind, ActionKind::Done | ActionKind::Rest)
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub enum ActionKind {
95 Read,
98 Grep,
100 Glob,
102 List,
104
105 Mutate,
108 MutateBatch,
110
111 Rest,
114 Done,
116 Escalate,
118}
119
120impl ActionKind {
121 pub fn as_str(&self) -> &'static str {
123 match self {
124 Self::Read => "read",
125 Self::Grep => "grep",
126 Self::Glob => "glob",
127 Self::List => "list",
128 Self::Mutate => "mutate",
129 Self::MutateBatch => "mutate_batch",
130 Self::Rest => "rest",
131 Self::Done => "done",
132 Self::Escalate => "escalate",
133 }
134 }
135
136 pub fn is_investigation(&self) -> bool {
138 matches!(self, Self::Read | Self::Grep | Self::Glob | Self::List)
139 }
140
141 pub fn is_mutation(&self) -> bool {
143 matches!(self, Self::Mutate | Self::MutateBatch)
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ActionResult {
150 pub action: Action,
152
153 pub success: bool,
155
156 pub output: Option<String>,
158
159 pub error: Option<String>,
161
162 pub duration_us: u64,
164
165 pub affected_files: Vec<PathBuf>,
167
168 pub changes: usize,
170}
171
172impl ActionResult {
173 pub fn success(action: Action) -> Self {
175 Self {
176 action,
177 success: true,
178 output: None,
179 error: None,
180 duration_us: 0,
181 affected_files: Vec::new(),
182 changes: 0,
183 }
184 }
185
186 pub fn failure(action: Action, error: impl Into<String>) -> Self {
188 Self {
189 action,
190 success: false,
191 output: None,
192 error: Some(error.into()),
193 duration_us: 0,
194 affected_files: Vec::new(),
195 changes: 0,
196 }
197 }
198
199 pub fn with_output(mut self, output: impl Into<String>) -> Self {
201 self.output = Some(output.into());
202 self
203 }
204
205 pub fn with_duration(mut self, duration_us: u64) -> Self {
207 self.duration_us = duration_us;
208 self
209 }
210
211 pub fn with_affected_files(mut self, files: Vec<PathBuf>) -> Self {
213 self.affected_files = files;
214 self
215 }
216
217 pub fn with_changes(mut self, changes: usize) -> Self {
219 self.changes = changes;
220 self
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_action_creation() {
230 let action = Action::read("src/lib.rs");
231 assert_eq!(action.kind, ActionKind::Read);
232 assert_eq!(action.target, Some("src/lib.rs".to_string()));
233
234 let action = Action::grep("TODO").with_target("src/");
235 assert_eq!(action.kind, ActionKind::Grep);
236 assert_eq!(action.args.get("pattern"), Some(&"TODO".to_string()));
237
238 let action = Action::mutate("Rename", "old_fn");
239 assert_eq!(action.kind, ActionKind::Mutate);
240 assert_eq!(action.target, Some("old_fn".to_string()));
241 }
242
243 #[test]
244 fn test_action_result() {
245 let action = Action::read("test.rs");
246 let result = ActionResult::success(action.clone())
247 .with_output("file contents")
248 .with_duration(100);
249
250 assert!(result.success);
251 assert_eq!(result.output, Some("file contents".to_string()));
252 assert_eq!(result.duration_us, 100);
253
254 let result = ActionResult::failure(action, "File not found");
255 assert!(!result.success);
256 assert_eq!(result.error, Some("File not found".to_string()));
257 }
258}