Skip to main content

jellyflow_runtime/io/files/
graph.rs

1use std::path::Path;
2
3use jellyflow_core::core::{Graph, GraphId};
4use serde::{Deserialize, Serialize};
5
6/// Graph file format version (v1).
7pub const GRAPH_FILE_VERSION: u32 = 1;
8
9/// Graph persistence file (v1).
10///
11/// This wrapper enables stable schema evolution while keeping the inner `Graph` model reusable.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GraphFileV1 {
14    /// Graph id (duplicated for quick lookup / validation).
15    pub graph_id: GraphId,
16    /// File wrapper version.
17    pub graph_version: u32,
18    /// Graph document.
19    pub graph: Graph,
20}
21
22impl GraphFileV1 {
23    /// Wraps a graph into a v1 file object.
24    pub fn from_graph(graph: Graph) -> Self {
25        Self {
26            graph_id: graph.graph_id,
27            graph_version: GRAPH_FILE_VERSION,
28            graph,
29        }
30    }
31
32    /// Validates wrapper invariants.
33    pub fn validate(&self) -> Result<(), GraphFileError> {
34        if self.graph_id != self.graph.graph_id {
35            return Err(GraphFileError::InconsistentGraphId);
36        }
37        Ok(())
38    }
39
40    /// Loads a JSON file.
41    pub fn load_json(path: impl AsRef<Path>) -> Result<Self, GraphFileError> {
42        let path = path.as_ref();
43        let bytes = std::fs::read(path).map_err(|source| GraphFileError::Read {
44            path: path.display().to_string(),
45            source,
46        })?;
47
48        let v = serde_json::from_slice::<Self>(&bytes).map_err(|source| GraphFileError::Parse {
49            path: path.display().to_string(),
50            source,
51        })?;
52        v.validate()?;
53        Ok(v)
54    }
55
56    /// Loads the JSON file if it exists.
57    pub fn load_json_if_exists(path: impl AsRef<Path>) -> Result<Option<Self>, GraphFileError> {
58        let path = path.as_ref();
59        if !path.exists() {
60            return Ok(None);
61        }
62        Self::load_json(path).map(Some)
63    }
64
65    /// Saves the JSON file (pretty-printed).
66    pub fn save_json(&self, path: impl AsRef<Path>) -> Result<(), GraphFileError> {
67        let path = path.as_ref();
68        if let Some(parent) = path.parent() {
69            std::fs::create_dir_all(parent).map_err(|source| GraphFileError::Write {
70                path: path.display().to_string(),
71                source,
72            })?;
73        }
74        let bytes =
75            serde_json::to_vec_pretty(self).map_err(|source| GraphFileError::Serialize {
76                path: path.display().to_string(),
77                source,
78            })?;
79        std::fs::write(path, bytes).map_err(|source| GraphFileError::Write {
80            path: path.display().to_string(),
81            source,
82        })
83    }
84}
85
86/// Errors for reading/writing graph files.
87#[derive(Debug, thiserror::Error)]
88pub enum GraphFileError {
89    /// Read failure.
90    #[error("failed to read graph file: {path}")]
91    Read {
92        path: String,
93        source: std::io::Error,
94    },
95    /// JSON parse failure.
96    #[error("failed to parse graph file JSON: {path}")]
97    Parse {
98        path: String,
99        source: serde_json::Error,
100    },
101    /// Write failure.
102    #[error("failed to write graph file: {path}")]
103    Write {
104        path: String,
105        source: std::io::Error,
106    },
107    /// JSON serialization failure.
108    #[error("failed to serialize graph file JSON: {path}")]
109    Serialize {
110        path: String,
111        source: serde_json::Error,
112    },
113    /// Wrapper id mismatch.
114    #[error("graph file wrapper graph_id does not match graph.graph_id")]
115    InconsistentGraphId,
116}