oxify_model/
yaml.rs

1//! YAML serialization and deserialization for workflows
2//!
3//! This module provides utilities for reading and writing workflows
4//! in YAML format, which is commonly used for configuration files.
5
6use crate::{Workflow, WorkflowTemplate};
7use std::fs;
8use std::path::Path;
9use thiserror::Error;
10
11/// Errors that can occur during YAML operations
12#[derive(Debug, Error)]
13pub enum YamlError {
14    /// YAML serialization error
15    #[error("YAML serialization error: {0}")]
16    Serialization(#[from] serde_yaml::Error),
17
18    /// File I/O error
19    #[error("File I/O error: {0}")]
20    Io(#[from] std::io::Error),
21
22    /// Invalid YAML format
23    #[error("Invalid YAML format: {0}")]
24    InvalidFormat(String),
25}
26
27/// Serialize a workflow to a YAML string
28pub fn workflow_to_yaml(workflow: &Workflow) -> Result<String, YamlError> {
29    serde_yaml::to_string(workflow).map_err(YamlError::from)
30}
31
32/// Deserialize a workflow from a YAML string
33pub fn workflow_from_yaml(yaml: &str) -> Result<Workflow, YamlError> {
34    serde_yaml::from_str(yaml).map_err(YamlError::from)
35}
36
37/// Save a workflow to a YAML file
38pub fn save_workflow_yaml<P: AsRef<Path>>(workflow: &Workflow, path: P) -> Result<(), YamlError> {
39    let yaml = workflow_to_yaml(workflow)?;
40    fs::write(path, yaml).map_err(YamlError::from)
41}
42
43/// Load a workflow from a YAML file
44pub fn load_workflow_yaml<P: AsRef<Path>>(path: P) -> Result<Workflow, YamlError> {
45    let yaml = fs::read_to_string(path)?;
46    workflow_from_yaml(&yaml)
47}
48
49/// Serialize a workflow template to a YAML string
50pub fn template_to_yaml(template: &WorkflowTemplate) -> Result<String, YamlError> {
51    serde_yaml::to_string(template).map_err(YamlError::from)
52}
53
54/// Deserialize a workflow template from a YAML string
55pub fn template_from_yaml(yaml: &str) -> Result<WorkflowTemplate, YamlError> {
56    serde_yaml::from_str(yaml).map_err(YamlError::from)
57}
58
59/// Save a workflow template to a YAML file
60pub fn save_template_yaml<P: AsRef<Path>>(
61    template: &WorkflowTemplate,
62    path: P,
63) -> Result<(), YamlError> {
64    let yaml = template_to_yaml(template)?;
65    fs::write(path, yaml).map_err(YamlError::from)
66}
67
68/// Load a workflow template from a YAML file
69pub fn load_template_yaml<P: AsRef<Path>>(path: P) -> Result<WorkflowTemplate, YamlError> {
70    let yaml = fs::read_to_string(path)?;
71    template_from_yaml(&yaml)
72}
73
74/// Convert a workflow from JSON to YAML
75pub fn json_to_yaml(json: &str) -> Result<String, YamlError> {
76    let workflow: Workflow = serde_json::from_str(json)
77        .map_err(|e| YamlError::InvalidFormat(format!("Invalid JSON: {}", e)))?;
78    workflow_to_yaml(&workflow)
79}
80
81/// Convert a workflow from YAML to JSON
82pub fn yaml_to_json(yaml: &str) -> Result<String, YamlError> {
83    let workflow = workflow_from_yaml(yaml)?;
84    serde_json::to_string_pretty(&workflow)
85        .map_err(|e| YamlError::InvalidFormat(format!("JSON serialization failed: {}", e)))
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::{Edge, Node, NodeKind};
92    use tempfile::NamedTempFile;
93
94    #[test]
95    fn test_workflow_to_yaml() {
96        let mut workflow = Workflow::new("Test Workflow".to_string());
97
98        let start_node = Node::new("Start".to_string(), NodeKind::Start);
99        let start_id = start_node.id;
100        workflow.add_node(start_node);
101
102        let end_node = Node::new("End".to_string(), NodeKind::End);
103        let end_id = end_node.id;
104        workflow.add_node(end_node);
105
106        workflow.add_edge(Edge::new(start_id, end_id));
107
108        let yaml = workflow_to_yaml(&workflow).unwrap();
109
110        // Check that YAML contains expected fields
111        assert!(yaml.contains("name: Test Workflow"));
112        assert!(yaml.contains("nodes:"));
113        assert!(yaml.contains("edges:"));
114    }
115
116    #[test]
117    fn test_workflow_from_yaml() {
118        let yaml = r#"
119id: 550e8400-e29b-41d4-a716-446655440000
120metadata:
121  id: 550e8400-e29b-41d4-a716-446655440000
122  name: Test Workflow
123  description: A test workflow
124  version: "1.0.0"
125  created_at: "2026-01-01T00:00:00Z"
126  updated_at: "2026-01-01T00:00:00Z"
127  tags:
128    - test
129nodes:
130  - id: 550e8400-e29b-41d4-a716-446655440001
131    name: Start
132    kind:
133      type: Start
134    position: null
135    retry_config: null
136    timeout_config: null
137  - id: 550e8400-e29b-41d4-a716-446655440002
138    name: End
139    kind:
140      type: End
141    position: null
142    retry_config: null
143    timeout_config: null
144edges:
145  - id: 550e8400-e29b-41d4-a716-446655440003
146    from: 550e8400-e29b-41d4-a716-446655440001
147    to: 550e8400-e29b-41d4-a716-446655440002
148"#;
149
150        let workflow = workflow_from_yaml(yaml).unwrap();
151
152        assert_eq!(workflow.metadata.name, "Test Workflow");
153        assert_eq!(
154            workflow.metadata.description,
155            Some("A test workflow".to_string())
156        );
157        assert_eq!(workflow.metadata.version, "1.0.0");
158        assert_eq!(workflow.nodes.len(), 2);
159        assert_eq!(workflow.edges.len(), 1);
160    }
161
162    #[test]
163    fn test_save_and_load_workflow_yaml() {
164        let mut workflow = Workflow::new("File Test".to_string());
165
166        let start_node = Node::new("Start".to_string(), NodeKind::Start);
167        workflow.add_node(start_node);
168
169        // Create a temporary file
170        let temp_file = NamedTempFile::new().unwrap();
171        let temp_path = temp_file.path().to_path_buf();
172
173        // Save workflow
174        save_workflow_yaml(&workflow, &temp_path).unwrap();
175
176        // Load workflow
177        let loaded_workflow = load_workflow_yaml(&temp_path).unwrap();
178
179        assert_eq!(loaded_workflow.metadata.name, "File Test");
180        assert_eq!(loaded_workflow.nodes.len(), 1);
181    }
182
183    #[test]
184    fn test_json_to_yaml_conversion() {
185        let json = r#"{
186  "id": "550e8400-e29b-41d4-a716-446655440000",
187  "metadata": {
188    "id": "550e8400-e29b-41d4-a716-446655440000",
189    "name": "Conversion Test",
190    "description": null,
191    "version": "1.0.0",
192    "created_at": "2026-01-01T00:00:00Z",
193    "updated_at": "2026-01-01T00:00:00Z",
194    "tags": []
195  },
196  "nodes": [],
197  "edges": []
198}"#;
199
200        let yaml = json_to_yaml(json).unwrap();
201
202        assert!(yaml.contains("name: Conversion Test"));
203        assert!(yaml.contains("version:"));
204    }
205
206    #[test]
207    fn test_yaml_to_json_conversion() {
208        let yaml = r#"
209id: 550e8400-e29b-41d4-a716-446655440000
210metadata:
211  id: 550e8400-e29b-41d4-a716-446655440000
212  name: YAML Test
213  description: null
214  version: "1.0.0"
215  created_at: "2026-01-01T00:00:00Z"
216  updated_at: "2026-01-01T00:00:00Z"
217  tags: []
218nodes: []
219edges: []
220"#;
221
222        let json = yaml_to_json(yaml).unwrap();
223
224        assert!(json.contains("YAML Test"));
225        assert!(json.contains("version"));
226    }
227
228    #[test]
229    fn test_roundtrip_yaml_serialization() {
230        let mut workflow = Workflow::new("Roundtrip Test".to_string());
231        workflow.metadata.description = Some("Test description".to_string());
232        workflow.metadata.tags.push("tag1".to_string());
233
234        let start_node = Node::new("Start".to_string(), NodeKind::Start);
235        let start_id = start_node.id;
236        workflow.add_node(start_node);
237
238        let end_node = Node::new("End".to_string(), NodeKind::End);
239        let end_id = end_node.id;
240        workflow.add_node(end_node);
241
242        workflow.add_edge(Edge::new(start_id, end_id));
243
244        // Serialize to YAML
245        let yaml = workflow_to_yaml(&workflow).unwrap();
246
247        // Deserialize from YAML
248        let loaded = workflow_from_yaml(&yaml).unwrap();
249
250        // Verify data integrity
251        assert_eq!(loaded.metadata.name, workflow.metadata.name);
252        assert_eq!(loaded.metadata.description, workflow.metadata.description);
253        assert_eq!(loaded.metadata.tags, workflow.metadata.tags);
254        assert_eq!(loaded.nodes.len(), workflow.nodes.len());
255        assert_eq!(loaded.edges.len(), workflow.edges.len());
256    }
257
258    #[test]
259    fn test_template_yaml_serialization() {
260        use crate::{ParameterType, TemplateParameter, WorkflowTemplate};
261
262        let mut template = WorkflowTemplate {
263            id: uuid::Uuid::new_v4(),
264            name: "Test Template".to_string(),
265            description: Some("A test template".to_string()),
266            version: "1.0.0".to_string(),
267            author: Some("Test Author".to_string()),
268            created_at: chrono::Utc::now(),
269            updated_at: chrono::Utc::now(),
270            category: Some("testing".to_string()),
271            tags: vec!["test".to_string()],
272            parameters: vec![],
273            workflow_json: "{}".to_string(),
274            usage_count: 0,
275            is_public: true,
276            owner_id: Some(uuid::Uuid::new_v4()),
277        };
278
279        template.parameters.push(TemplateParameter {
280            name: "test_param".to_string(),
281            label: "Test Parameter".to_string(),
282            description: Some("A test parameter".to_string()),
283            param_type: ParameterType::String,
284            required: true,
285            default_value: None,
286            validation: None,
287            allowed_values: vec![],
288            group: None,
289            order: 0,
290        });
291
292        let yaml = template_to_yaml(&template).unwrap();
293        assert!(yaml.contains("Test Template"));
294        assert!(yaml.contains("test_param"));
295
296        let loaded = template_from_yaml(&yaml).unwrap();
297        assert_eq!(loaded.name, template.name);
298        assert_eq!(loaded.parameters.len(), 1);
299    }
300
301    #[test]
302    fn test_invalid_yaml() {
303        let invalid_yaml = "this is not: valid: yaml:::::";
304        let result = workflow_from_yaml(invalid_yaml);
305        assert!(result.is_err());
306    }
307
308    #[test]
309    fn test_yaml_error_display() {
310        let invalid_yaml = "invalid: yaml: structure: {{{";
311        let result = workflow_from_yaml(invalid_yaml);
312
313        match result {
314            Err(e) => {
315                let error_msg = format!("{}", e);
316                assert!(error_msg.contains("YAML serialization error"));
317            }
318            Ok(_) => panic!("Expected error but got Ok"),
319        }
320    }
321}