Skip to main content

butterflow_state/
local_adapter.rs

1use 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
15/// Local state adapter (stores state in local files)
16pub struct LocalStateAdapter {
17    /// Base directory for storing state
18    base_dir: PathBuf,
19
20    /// Workflow runs
21    workflow_runs: HashMap<Uuid, WorkflowRun>,
22
23    /// Tasks
24    tasks: HashMap<Uuid, Task>,
25}
26
27impl Default for LocalStateAdapter {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl LocalStateAdapter {
34    /// Create a new local state adapter
35    pub fn new() -> Self {
36        // Get the data directory
37        let data_dir = dirs::data_dir()
38            .unwrap_or_else(|| PathBuf::from("."))
39            .join("butterflow");
40
41        // Create the data directory if it doesn't exist
42        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    /// Create a new local state adapter with a custom base directory
54    pub fn with_base_dir<P: AsRef<Path>>(base_dir: P) -> Self {
55        // Create the base directory if it doesn't exist
56        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    /// Get the path to a workflow run file
68    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    /// Get the path to a task file
75    fn task_path(&self, task_id: Uuid) -> PathBuf {
76        self.base_dir
77            .join("tasks")
78            .join(format!("{}.json", task_id))
79    }
80
81    /// Get the path to a workflow state file
82    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    /// Load a workflow run from disk
89    fn load_workflow_run(&self, workflow_run_id: Uuid) -> Result<WorkflowRun> {
90        let path = self.workflow_run_path(workflow_run_id);
91
92        // Check if the file exists
93        if !path.exists() {
94            return Err(Error::Other(format!(
95                "Workflow run {} not found",
96                workflow_run_id
97            )));
98        }
99
100        // Read the file
101        let content = fs::read_to_string(path)?;
102
103        // Parse the JSON
104        let workflow_run = serde_json::from_str(&content)?;
105
106        Ok(workflow_run)
107    }
108
109    /// Load a task from disk
110    fn load_task(&self, task_id: Uuid) -> Result<Task> {
111        let path = self.task_path(task_id);
112
113        // Check if the file exists
114        if !path.exists() {
115            return Err(Error::Other(format!("Task {} not found", task_id)));
116        }
117
118        // Read the file
119        let content = fs::read_to_string(path)?;
120
121        // Parse the JSON
122        let task = serde_json::from_str(&content)?;
123
124        Ok(task)
125    }
126
127    /// Load workflow state from disk
128    fn load_state(&self, workflow_run_id: Uuid) -> Result<HashMap<String, Value>> {
129        let path = self.state_path(workflow_run_id);
130
131        // Check if the file exists
132        if !path.exists() {
133            return Ok(HashMap::new());
134        }
135
136        // Read the file
137        let content = fs::read_to_string(path)?;
138
139        // Parse the JSON
140        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        // Create the workflow_runs directory if it doesn't exist
150        let dir = self.base_dir.join("workflow_runs");
151        fs::create_dir_all(&dir)?;
152
153        // Serialize the workflow run
154        let json = serde_json::to_string_pretty(workflow_run)?;
155
156        // Write to disk
157        let path = self.workflow_run_path(workflow_run.id);
158        fs::write(path, json)?;
159
160        // Update in-memory cache
161        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        // Get the workflow run
169        let mut workflow_run = self.get_workflow_run(diff.workflow_run_id).await?;
170
171        // Apply the diff
172        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                        // Convert the workflow run to a JSON value
177                        let mut workflow_run_value = serde_json::to_value(&workflow_run)?;
178
179                        // Update the field
180                        if let serde_json::Value::Object(obj) = &mut workflow_run_value {
181                            obj.insert(field.clone(), value.clone());
182                        }
183
184                        // Convert back to a workflow run
185                        workflow_run = serde_json::from_value(workflow_run_value)?;
186                    }
187                }
188                DiffOperation::Remove => {
189                    // Convert the workflow run to a JSON value
190                    let mut workflow_run_value = serde_json::to_value(&workflow_run)?;
191
192                    // Remove the field
193                    if let serde_json::Value::Object(obj) = &mut workflow_run_value {
194                        obj.remove(field);
195                    }
196
197                    // Convert back to a workflow run
198                    workflow_run = serde_json::from_value(workflow_run_value)?;
199                }
200            }
201        }
202
203        // Save the updated workflow run
204        self.save_workflow_run(&workflow_run).await
205    }
206
207    async fn get_workflow_run(&self, workflow_run_id: Uuid) -> Result<WorkflowRun> {
208        // Check if the workflow run is in the cache
209        if let Some(workflow_run) = self.workflow_runs.get(&workflow_run_id) {
210            return Ok(workflow_run.clone());
211        }
212
213        // Load from disk
214        self.load_workflow_run(workflow_run_id)
215    }
216
217    async fn list_workflow_runs(&self, limit: usize) -> Result<Vec<WorkflowRun>> {
218        // Create the workflow_runs directory if it doesn't exist
219        let dir = self.base_dir.join("workflow_runs");
220        fs::create_dir_all(&dir)?;
221
222        // List all files in the directory
223        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        // Sort by modification time (newest first)
235        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        // Load the workflow runs
248        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        // Create the tasks directory if it doesn't exist
264        let dir = self.base_dir.join("tasks");
265        fs::create_dir_all(&dir)?;
266
267        // Serialize the task
268        let json = serde_json::to_string_pretty(task)?;
269
270        // Write to disk
271        let path = self.task_path(task.id);
272        fs::write(path, json)?;
273
274        // Update in-memory cache
275        self.tasks.insert(task.id, task.clone());
276
277        Ok(())
278    }
279
280    async fn get_task(&self, task_id: Uuid) -> Result<Task> {
281        // Check if the task is in the cache
282        if let Some(task) = self.tasks.get(&task_id) {
283            return Ok(task.clone());
284        }
285
286        // Load from disk
287        self.load_task(task_id)
288    }
289
290    async fn get_tasks(&self, workflow_run_id: Uuid) -> Result<Vec<Task>> {
291        // Create the tasks directory if it doesn't exist
292        let dir = self.base_dir.join("tasks");
293        fs::create_dir_all(&dir)?;
294
295        // List all files in the directory
296        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        // Load the tasks
308        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        // Create the state directory if it doesn't exist
330        let dir = self.base_dir.join("state");
331        fs::create_dir_all(&dir)?;
332
333        // Serialize the state
334        let json = serde_json::to_string_pretty(&state)?;
335
336        // Write to disk
337        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        // Get the task
345        let mut task = self.get_task(diff.task_id).await?;
346
347        // Apply the diff
348        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                        // Convert the task to a JSON value
353                        let mut task_value = serde_json::to_value(&task)?;
354
355                        // Update the field
356                        if let serde_json::Value::Object(obj) = &mut task_value {
357                            obj.insert(field.clone(), value.clone());
358                        }
359
360                        // Convert back to a task
361                        task = serde_json::from_value(task_value)?;
362                    }
363                }
364                DiffOperation::Remove => {
365                    // Convert the task to a JSON value
366                    let mut task_value = serde_json::to_value(&task)?;
367
368                    // Remove the field
369                    if let serde_json::Value::Object(obj) = &mut task_value {
370                        obj.remove(field);
371                    }
372
373                    // Convert back to a task
374                    task = serde_json::from_value(task_value)?;
375                }
376            }
377        }
378
379        // Save the updated task
380        self.save_task(&task).await
381    }
382
383    async fn apply_state_diff(&mut self, diff: &StateDiff) -> Result<()> {
384        // Get the current state
385        let mut state = self.get_state(diff.workflow_run_id).await?;
386
387        // Apply the diff
388        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                    // TODO: We may want reconsider this when we add state validation
400                    if let Some(new_value) = &field_diff.value {
401                        if let Some(existing) = state.get_mut(field) {
402                            // If the existing value is an array, append to it
403                            if let serde_json::Value::Array(arr) = existing {
404                                arr.push(new_value.clone());
405                            } else {
406                                // If the existing value is not an array, replace it with a new array containing both values
407                                let old_value = existing.clone();
408                                *existing =
409                                    serde_json::Value::Array(vec![old_value, new_value.clone()]);
410                            }
411                        } else {
412                            // Field doesn't exist yet, create a new array with just this value
413                            state.insert(
414                                field.clone(),
415                                serde_json::Value::Array(vec![new_value.clone()]),
416                            );
417                        }
418                    }
419                }
420            }
421        }
422
423        // Save the updated state
424        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}