1use crate::{Workflow, WorkflowTemplate};
7use std::fs;
8use std::path::Path;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
13pub enum YamlError {
14 #[error("YAML serialization error: {0}")]
16 Serialization(#[from] serde_yaml::Error),
17
18 #[error("File I/O error: {0}")]
20 Io(#[from] std::io::Error),
21
22 #[error("Invalid YAML format: {0}")]
24 InvalidFormat(String),
25}
26
27pub fn workflow_to_yaml(workflow: &Workflow) -> Result<String, YamlError> {
29 serde_yaml::to_string(workflow).map_err(YamlError::from)
30}
31
32pub fn workflow_from_yaml(yaml: &str) -> Result<Workflow, YamlError> {
34 serde_yaml::from_str(yaml).map_err(YamlError::from)
35}
36
37pub 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
43pub 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
49pub fn template_to_yaml(template: &WorkflowTemplate) -> Result<String, YamlError> {
51 serde_yaml::to_string(template).map_err(YamlError::from)
52}
53
54pub fn template_from_yaml(yaml: &str) -> Result<WorkflowTemplate, YamlError> {
56 serde_yaml::from_str(yaml).map_err(YamlError::from)
57}
58
59pub 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
68pub 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
74pub 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
81pub 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 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 let temp_file = NamedTempFile::new().unwrap();
171 let temp_path = temp_file.path().to_path_buf();
172
173 save_workflow_yaml(&workflow, &temp_path).unwrap();
175
176 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 let yaml = workflow_to_yaml(&workflow).unwrap();
246
247 let loaded = workflow_from_yaml(&yaml).unwrap();
249
250 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}