butterflow_state/
local_adapter.rs1use std::collections::HashMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use async_trait::async_trait;
6use serde_json::Value;
7use uuid::Uuid;
8
9use butterflow_models::{
10 DiffOperation, Error, Result, StateDiff, Task, TaskDiff, WorkflowRun, WorkflowRunDiff,
11};
12
13use crate::StateAdapter;
14
15pub struct LocalStateAdapter {
17 base_dir: PathBuf,
19
20 workflow_runs: HashMap<Uuid, WorkflowRun>,
22
23 tasks: HashMap<Uuid, Task>,
25}
26
27impl Default for LocalStateAdapter {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl LocalStateAdapter {
34 pub fn new() -> Self {
36 let data_dir = dirs::data_dir()
38 .unwrap_or_else(|| PathBuf::from("."))
39 .join("butterflow");
40
41 fs::create_dir_all(&data_dir).unwrap_or_else(|e| {
43 eprintln!("Failed to create data directory: {}", e);
44 });
45
46 Self {
47 base_dir: data_dir,
48 workflow_runs: HashMap::new(),
49 tasks: HashMap::new(),
50 }
51 }
52
53 pub fn with_base_dir<P: AsRef<Path>>(base_dir: P) -> Self {
55 fs::create_dir_all(&base_dir).unwrap_or_else(|e| {
57 eprintln!("Failed to create base directory: {}", e);
58 });
59
60 Self {
61 base_dir: base_dir.as_ref().to_path_buf(),
62 workflow_runs: HashMap::new(),
63 tasks: HashMap::new(),
64 }
65 }
66
67 fn workflow_run_path(&self, workflow_run_id: Uuid) -> PathBuf {
69 self.base_dir
70 .join("workflow_runs")
71 .join(format!("{}.json", workflow_run_id))
72 }
73
74 fn task_path(&self, task_id: Uuid) -> PathBuf {
76 self.base_dir
77 .join("tasks")
78 .join(format!("{}.json", task_id))
79 }
80
81 fn state_path(&self, workflow_run_id: Uuid) -> PathBuf {
83 self.base_dir
84 .join("state")
85 .join(format!("{}.json", workflow_run_id))
86 }
87
88 fn load_workflow_run(&self, workflow_run_id: Uuid) -> Result<WorkflowRun> {
90 let path = self.workflow_run_path(workflow_run_id);
91
92 if !path.exists() {
94 return Err(Error::Other(format!(
95 "Workflow run {} not found",
96 workflow_run_id
97 )));
98 }
99
100 let content = fs::read_to_string(path)?;
102
103 let workflow_run = serde_json::from_str(&content)?;
105
106 Ok(workflow_run)
107 }
108
109 fn load_task(&self, task_id: Uuid) -> Result<Task> {
111 let path = self.task_path(task_id);
112
113 if !path.exists() {
115 return Err(Error::Other(format!("Task {} not found", task_id)));
116 }
117
118 let content = fs::read_to_string(path)?;
120
121 let task = serde_json::from_str(&content)?;
123
124 Ok(task)
125 }
126
127 fn load_state(&self, workflow_run_id: Uuid) -> Result<HashMap<String, Value>> {
129 let path = self.state_path(workflow_run_id);
130
131 if !path.exists() {
133 return Ok(HashMap::new());
134 }
135
136 let content = fs::read_to_string(path)?;
138
139 let state = serde_json::from_str(&content)?;
141
142 Ok(state)
143 }
144}
145
146#[async_trait]
147impl StateAdapter for LocalStateAdapter {
148 async fn save_workflow_run(&mut self, workflow_run: &WorkflowRun) -> Result<()> {
149 let dir = self.base_dir.join("workflow_runs");
151 fs::create_dir_all(&dir)?;
152
153 let json = serde_json::to_string_pretty(workflow_run)?;
155
156 let path = self.workflow_run_path(workflow_run.id);
158 fs::write(path, json)?;
159
160 self.workflow_runs
162 .insert(workflow_run.id, workflow_run.clone());
163
164 Ok(())
165 }
166
167 async fn apply_workflow_run_diff(&mut self, diff: &WorkflowRunDiff) -> Result<()> {
168 let mut workflow_run = self.get_workflow_run(diff.workflow_run_id).await?;
170
171 for (field, field_diff) in &diff.fields {
173 match field_diff.operation {
174 DiffOperation::Add | DiffOperation::Update | DiffOperation::Append => {
175 if let Some(value) = &field_diff.value {
176 let mut workflow_run_value = serde_json::to_value(&workflow_run)?;
178
179 if let serde_json::Value::Object(obj) = &mut workflow_run_value {
181 obj.insert(field.clone(), value.clone());
182 }
183
184 workflow_run = serde_json::from_value(workflow_run_value)?;
186 }
187 }
188 DiffOperation::Remove => {
189 let mut workflow_run_value = serde_json::to_value(&workflow_run)?;
191
192 if let serde_json::Value::Object(obj) = &mut workflow_run_value {
194 obj.remove(field);
195 }
196
197 workflow_run = serde_json::from_value(workflow_run_value)?;
199 }
200 }
201 }
202
203 self.save_workflow_run(&workflow_run).await
205 }
206
207 async fn get_workflow_run(&self, workflow_run_id: Uuid) -> Result<WorkflowRun> {
208 if let Some(workflow_run) = self.workflow_runs.get(&workflow_run_id) {
210 return Ok(workflow_run.clone());
211 }
212
213 self.load_workflow_run(workflow_run_id)
215 }
216
217 async fn list_workflow_runs(&self, limit: usize) -> Result<Vec<WorkflowRun>> {
218 let dir = self.base_dir.join("workflow_runs");
220 fs::create_dir_all(&dir)?;
221
222 let mut entries = fs::read_dir(dir)?
224 .filter_map(|entry| entry.ok())
225 .filter(|entry| {
226 entry
227 .path()
228 .extension()
229 .map(|ext| ext == "json")
230 .unwrap_or(false)
231 })
232 .collect::<Vec<_>>();
233
234 entries.sort_by(|a, b| {
236 let a_time = a
237 .metadata()
238 .and_then(|m| m.modified())
239 .unwrap_or_else(|_| std::time::SystemTime::now());
240 let b_time = b
241 .metadata()
242 .and_then(|m| m.modified())
243 .unwrap_or_else(|_| std::time::SystemTime::now());
244 b_time.cmp(&a_time)
245 });
246
247 let mut workflow_runs = Vec::new();
249 for entry in entries.iter().take(limit) {
250 let path = entry.path();
251 let file_name = path.file_stem().unwrap().to_string_lossy();
252 if let Ok(workflow_run_id) = Uuid::parse_str(&file_name) {
253 if let Ok(workflow_run) = self.load_workflow_run(workflow_run_id) {
254 workflow_runs.push(workflow_run);
255 }
256 }
257 }
258
259 Ok(workflow_runs)
260 }
261
262 async fn save_task(&mut self, task: &Task) -> Result<()> {
263 let dir = self.base_dir.join("tasks");
265 fs::create_dir_all(&dir)?;
266
267 let json = serde_json::to_string_pretty(task)?;
269
270 let path = self.task_path(task.id);
272 fs::write(path, json)?;
273
274 self.tasks.insert(task.id, task.clone());
276
277 Ok(())
278 }
279
280 async fn get_task(&self, task_id: Uuid) -> Result<Task> {
281 if let Some(task) = self.tasks.get(&task_id) {
283 return Ok(task.clone());
284 }
285
286 self.load_task(task_id)
288 }
289
290 async fn get_tasks(&self, workflow_run_id: Uuid) -> Result<Vec<Task>> {
291 let dir = self.base_dir.join("tasks");
293 fs::create_dir_all(&dir)?;
294
295 let entries = fs::read_dir(dir)?
297 .filter_map(|entry| entry.ok())
298 .filter(|entry| {
299 entry
300 .path()
301 .extension()
302 .map(|ext| ext == "json")
303 .unwrap_or(false)
304 })
305 .collect::<Vec<_>>();
306
307 let mut tasks = Vec::new();
309 for entry in entries {
310 let path = entry.path();
311 let file_name = path.file_stem().unwrap().to_string_lossy();
312 if let Ok(task_id) = Uuid::parse_str(&file_name) {
313 if let Ok(task) = self.load_task(task_id) {
314 if task.workflow_run_id == workflow_run_id {
315 tasks.push(task);
316 }
317 }
318 }
319 }
320
321 Ok(tasks)
322 }
323
324 async fn update_state(
325 &mut self,
326 workflow_run_id: Uuid,
327 state: HashMap<String, Value>,
328 ) -> Result<()> {
329 let dir = self.base_dir.join("state");
331 fs::create_dir_all(&dir)?;
332
333 let json = serde_json::to_string_pretty(&state)?;
335
336 let path = self.state_path(workflow_run_id);
338 fs::write(path, json)?;
339
340 Ok(())
341 }
342
343 async fn apply_task_diff(&mut self, diff: &TaskDiff) -> Result<()> {
344 let mut task = self.get_task(diff.task_id).await?;
346
347 for (field, field_diff) in &diff.fields {
349 match field_diff.operation {
350 DiffOperation::Add | DiffOperation::Update | DiffOperation::Append => {
351 if let Some(value) = &field_diff.value {
352 let mut task_value = serde_json::to_value(&task)?;
354
355 if let serde_json::Value::Object(obj) = &mut task_value {
357 obj.insert(field.clone(), value.clone());
358 }
359
360 task = serde_json::from_value(task_value)?;
362 }
363 }
364 DiffOperation::Remove => {
365 let mut task_value = serde_json::to_value(&task)?;
367
368 if let serde_json::Value::Object(obj) = &mut task_value {
370 obj.remove(field);
371 }
372
373 task = serde_json::from_value(task_value)?;
375 }
376 }
377 }
378
379 self.save_task(&task).await
381 }
382
383 async fn apply_state_diff(&mut self, diff: &StateDiff) -> Result<()> {
384 let mut state = self.get_state(diff.workflow_run_id).await?;
386
387 for (field, field_diff) in &diff.fields {
389 match field_diff.operation {
390 DiffOperation::Add | DiffOperation::Update => {
391 if let Some(value) = &field_diff.value {
392 state.insert(field.clone(), value.clone());
393 }
394 }
395 DiffOperation::Remove => {
396 state.remove(field);
397 }
398 DiffOperation::Append => {
399 if let Some(new_value) = &field_diff.value {
401 if let Some(existing) = state.get_mut(field) {
402 if let serde_json::Value::Array(arr) = existing {
404 arr.push(new_value.clone());
405 } else {
406 let old_value = existing.clone();
408 *existing =
409 serde_json::Value::Array(vec![old_value, new_value.clone()]);
410 }
411 } else {
412 state.insert(
414 field.clone(),
415 serde_json::Value::Array(vec![new_value.clone()]),
416 );
417 }
418 }
419 }
420 }
421 }
422
423 self.update_state(diff.workflow_run_id, state).await
425 }
426
427 async fn get_state(&self, workflow_run_id: Uuid) -> Result<HashMap<String, Value>> {
428 self.load_state(workflow_run_id)
429 }
430}