openapi_from_source/
serializer.rs

1//! Serialization module for converting OpenAPI documents to YAML or JSON format.
2//!
3//! This module provides functions to serialize OpenAPI documents into standard formats
4//! and write them to files or return them as strings.
5
6use crate::openapi_builder::OpenApiDocument;
7use anyhow::{Context, Result};
8use log::debug;
9use std::fs;
10use std::path::Path;
11
12/// Serializes an OpenAPI document to YAML format.
13///
14/// The output is formatted as standard YAML, suitable for use with OpenAPI tools
15/// and documentation generators.
16///
17/// # Arguments
18///
19/// * `doc` - The OpenAPI document to serialize
20///
21/// # Returns
22///
23/// Returns the YAML string representation of the document.
24///
25/// # Errors
26///
27/// Returns an error if serialization fails.
28///
29/// # Example
30///
31/// ```ignore
32/// use openapi_from_source::openapi_builder::OpenApiBuilder;
33/// use openapi_from_source::serializer::serialize_yaml;
34/// use openapi_from_source::schema_generator::SchemaGenerator;
35/// use openapi_from_source::type_resolver::TypeResolver;
36///
37/// let builder = OpenApiBuilder::new();
38/// let type_resolver = TypeResolver::new(vec![]);
39/// let schema_gen = SchemaGenerator::new(type_resolver);
40/// let doc = builder.build(schema_gen);
41/// let yaml = serialize_yaml(&doc).unwrap();
42/// println!("{}", yaml);
43/// ```
44pub fn serialize_yaml(doc: &OpenApiDocument) -> Result<String> {
45    debug!("Serializing OpenAPI document to YAML");
46    serde_yaml::to_string(doc)
47        .context("Failed to serialize OpenAPI document to YAML")
48}
49
50/// Serializes an OpenAPI document to JSON format with pretty printing.
51///
52/// The output is formatted with indentation for readability, making it suitable
53/// for human review and version control.
54///
55/// # Arguments
56///
57/// * `doc` - The OpenAPI document to serialize
58///
59/// # Returns
60///
61/// Returns the JSON string representation of the document.
62///
63/// # Errors
64///
65/// Returns an error if serialization fails.
66///
67/// # Example
68///
69/// ```ignore
70/// use openapi_from_source::openapi_builder::OpenApiBuilder;
71/// use openapi_from_source::serializer::serialize_json;
72/// use openapi_from_source::schema_generator::SchemaGenerator;
73/// use openapi_from_source::type_resolver::TypeResolver;
74///
75/// let builder = OpenApiBuilder::new();
76/// let type_resolver = TypeResolver::new(vec![]);
77/// let schema_gen = SchemaGenerator::new(type_resolver);
78/// let doc = builder.build(schema_gen);
79/// let json = serialize_json(&doc).unwrap();
80/// println!("{}", json);
81/// ```
82pub fn serialize_json(doc: &OpenApiDocument) -> Result<String> {
83    debug!("Serializing OpenAPI document to JSON");
84    serde_json::to_string_pretty(doc)
85        .context("Failed to serialize OpenAPI document to JSON")
86}
87
88/// Writes string content to a file.
89///
90/// Creates the file if it doesn't exist, or overwrites it if it does.
91/// Parent directories are not created automatically.
92///
93/// # Arguments
94///
95/// * `content` - The string content to write
96/// * `path` - The file path to write to
97///
98/// # Returns
99///
100/// Returns `Ok(())` on success.
101///
102/// # Errors
103///
104/// Returns an error if the file cannot be created or written to.
105pub fn write_to_file(content: &str, path: &Path) -> Result<()> {
106    debug!("Writing content to file: {}", path.display());
107    
108    // Create parent directories if they don't exist
109    if let Some(parent) = path.parent() {
110        fs::create_dir_all(parent)
111            .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
112    }
113    
114    fs::write(path, content)
115        .with_context(|| format!("Failed to write to file: {}", path.display()))?;
116    
117    debug!("Successfully wrote {} bytes to {}", content.len(), path.display());
118    Ok(())
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::openapi_builder::{Info, OpenApiBuilder, OpenApiDocument};
125    use std::collections::HashMap;
126    use tempfile::TempDir;
127
128    /// Helper function to create a minimal OpenAPI document for testing
129    fn create_test_document() -> OpenApiDocument {
130        OpenApiDocument {
131            openapi: "3.0.0".to_string(),
132            info: Info {
133                title: "Test API".to_string(),
134                version: "1.0.0".to_string(),
135                description: Some("A test API".to_string()),
136            },
137            paths: HashMap::new(),
138            components: None,
139        }
140    }
141
142    #[test]
143    fn test_serialize_yaml() {
144        let doc = create_test_document();
145        let result = serialize_yaml(&doc);
146        
147        assert!(result.is_ok());
148        let yaml = result.unwrap();
149        
150        // Check that YAML contains expected fields
151        assert!(yaml.contains("openapi:"));
152        assert!(yaml.contains("3.0.0"));
153        assert!(yaml.contains("info:"));
154        assert!(yaml.contains("title:"));
155        assert!(yaml.contains("Test API"));
156        assert!(yaml.contains("version:"));
157        assert!(yaml.contains("1.0.0"));
158        assert!(yaml.contains("description:"));
159        assert!(yaml.contains("A test API"));
160        assert!(yaml.contains("paths:"));
161    }
162
163    #[test]
164    fn test_serialize_json() {
165        let doc = create_test_document();
166        let result = serialize_json(&doc);
167        
168        assert!(result.is_ok());
169        let json = result.unwrap();
170        
171        // Check that JSON contains expected fields
172        assert!(json.contains("\"openapi\""));
173        assert!(json.contains("\"3.0.0\""));
174        assert!(json.contains("\"info\""));
175        assert!(json.contains("\"title\""));
176        assert!(json.contains("\"Test API\""));
177        assert!(json.contains("\"version\""));
178        assert!(json.contains("\"1.0.0\""));
179        assert!(json.contains("\"description\""));
180        assert!(json.contains("\"A test API\""));
181        assert!(json.contains("\"paths\""));
182        
183        // Verify it's valid JSON by parsing it back
184        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
185        assert_eq!(parsed["openapi"], "3.0.0");
186        assert_eq!(parsed["info"]["title"], "Test API");
187    }
188
189    #[test]
190    fn test_serialize_json_pretty_format() {
191        let doc = create_test_document();
192        let json = serialize_json(&doc).unwrap();
193        
194        // Check that JSON is pretty-printed (contains newlines and indentation)
195        assert!(json.contains('\n'));
196        assert!(json.contains("  ")); // Should have indentation
197        
198        // Count lines - pretty printed JSON should have multiple lines
199        let line_count = json.lines().count();
200        assert!(line_count > 5, "Pretty printed JSON should have multiple lines");
201    }
202
203    #[test]
204    fn test_write_to_file() {
205        let temp_dir = TempDir::new().unwrap();
206        let file_path = temp_dir.path().join("test.yaml");
207        let content = "test content";
208        
209        let result = write_to_file(content, &file_path);
210        
211        assert!(result.is_ok());
212        assert!(file_path.exists());
213        
214        let read_content = fs::read_to_string(&file_path).unwrap();
215        assert_eq!(read_content, content);
216    }
217
218    #[test]
219    fn test_write_to_file_creates_directories() {
220        let temp_dir = TempDir::new().unwrap();
221        let file_path = temp_dir.path().join("subdir").join("nested").join("test.yaml");
222        let content = "test content";
223        
224        let result = write_to_file(content, &file_path);
225        
226        assert!(result.is_ok());
227        assert!(file_path.exists());
228        
229        let read_content = fs::read_to_string(&file_path).unwrap();
230        assert_eq!(read_content, content);
231    }
232
233    #[test]
234    fn test_write_to_file_overwrites_existing() {
235        let temp_dir = TempDir::new().unwrap();
236        let file_path = temp_dir.path().join("test.yaml");
237        
238        // Write initial content
239        write_to_file("initial content", &file_path).unwrap();
240        
241        // Overwrite with new content
242        let new_content = "new content";
243        let result = write_to_file(new_content, &file_path);
244        
245        assert!(result.is_ok());
246        
247        let read_content = fs::read_to_string(&file_path).unwrap();
248        assert_eq!(read_content, new_content);
249    }
250
251    #[test]
252    fn test_serialize_yaml_with_complex_document() {
253        use crate::extractor::{HttpMethod, RouteInfo};
254        use crate::parser::AstParser;
255        use crate::schema_generator::SchemaGenerator;
256        use crate::type_resolver::TypeResolver;
257        use std::io::Write;
258        
259        // Create a test file with a struct
260        let temp_dir = TempDir::new().unwrap();
261        let file_path = temp_dir.path().join("test.rs");
262        let mut file = fs::File::create(&file_path).unwrap();
263        file.write_all(b"pub struct User { pub id: u32, pub name: String }").unwrap();
264        
265        // Parse and create document
266        let parsed = AstParser::parse_file(&file_path).unwrap();
267        let type_resolver = TypeResolver::new(vec![parsed]);
268        let mut schema_gen = SchemaGenerator::new(type_resolver);
269        
270        let mut builder = OpenApiBuilder::new();
271        let route = RouteInfo::new(
272            "/users".to_string(),
273            HttpMethod::Get,
274            "get_users".to_string(),
275        );
276        builder.add_route(&route, &mut schema_gen);
277        
278        let doc = builder.build(schema_gen);
279        let yaml = serialize_yaml(&doc).unwrap();
280        
281        // Verify YAML structure
282        assert!(yaml.contains("openapi:"));
283        assert!(yaml.contains("paths:"));
284        assert!(yaml.contains("/users:"));
285        assert!(yaml.contains("get:"));
286    }
287
288    #[test]
289    fn test_serialize_json_with_complex_document() {
290        use crate::extractor::{HttpMethod, Parameter, ParameterLocation, RouteInfo, TypeInfo};
291        use crate::parser::AstParser;
292        use crate::schema_generator::SchemaGenerator;
293        use crate::type_resolver::TypeResolver;
294        use std::io::Write;
295        
296        // Create a test file with a struct
297        let temp_dir = TempDir::new().unwrap();
298        let file_path = temp_dir.path().join("test.rs");
299        let mut file = fs::File::create(&file_path).unwrap();
300        file.write_all(b"pub struct User { pub id: u32, pub name: String }").unwrap();
301        
302        // Parse and create document
303        let parsed = AstParser::parse_file(&file_path).unwrap();
304        let type_resolver = TypeResolver::new(vec![parsed]);
305        let mut schema_gen = SchemaGenerator::new(type_resolver);
306        
307        let mut builder = OpenApiBuilder::new();
308        let mut route = RouteInfo::new(
309            "/users/:id".to_string(),
310            HttpMethod::Get,
311            "get_user".to_string(),
312        );
313        route.parameters.push(Parameter::new(
314            "id".to_string(),
315            ParameterLocation::Path,
316            TypeInfo::new("u32".to_string()),
317            true,
318        ));
319        builder.add_route(&route, &mut schema_gen);
320        
321        let doc = builder.build(schema_gen);
322        let json = serialize_json(&doc).unwrap();
323        
324        // Verify JSON structure
325        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
326        assert_eq!(parsed["openapi"], "3.0.0");
327        assert!(parsed["paths"].is_object());
328        assert!(parsed["paths"]["/users/{id}"].is_object());
329        assert!(parsed["paths"]["/users/{id}"]["get"].is_object());
330    }
331
332    #[test]
333    fn test_roundtrip_yaml_serialization() {
334        let doc = create_test_document();
335        let yaml = serialize_yaml(&doc).unwrap();
336        
337        // Deserialize back
338        let deserialized: OpenApiDocument = serde_yaml::from_str(&yaml).unwrap();
339        
340        assert_eq!(deserialized.openapi, doc.openapi);
341        assert_eq!(deserialized.info.title, doc.info.title);
342        assert_eq!(deserialized.info.version, doc.info.version);
343        assert_eq!(deserialized.info.description, doc.info.description);
344    }
345
346    #[test]
347    fn test_roundtrip_json_serialization() {
348        let doc = create_test_document();
349        let json = serialize_json(&doc).unwrap();
350        
351        // Deserialize back
352        let deserialized: OpenApiDocument = serde_json::from_str(&json).unwrap();
353        
354        assert_eq!(deserialized.openapi, doc.openapi);
355        assert_eq!(deserialized.info.title, doc.info.title);
356        assert_eq!(deserialized.info.version, doc.info.version);
357        assert_eq!(deserialized.info.description, doc.info.description);
358    }
359
360    #[test]
361    fn test_write_yaml_file_end_to_end() {
362        let temp_dir = TempDir::new().unwrap();
363        let file_path = temp_dir.path().join("openapi.yaml");
364        
365        let doc = create_test_document();
366        let yaml = serialize_yaml(&doc).unwrap();
367        
368        write_to_file(&yaml, &file_path).unwrap();
369        
370        // Read back and verify
371        let content = fs::read_to_string(&file_path).unwrap();
372        let deserialized: OpenApiDocument = serde_yaml::from_str(&content).unwrap();
373        
374        assert_eq!(deserialized.info.title, "Test API");
375    }
376
377    #[test]
378    fn test_write_json_file_end_to_end() {
379        let temp_dir = TempDir::new().unwrap();
380        let file_path = temp_dir.path().join("openapi.json");
381        
382        let doc = create_test_document();
383        let json = serialize_json(&doc).unwrap();
384        
385        write_to_file(&json, &file_path).unwrap();
386        
387        // Read back and verify
388        let content = fs::read_to_string(&file_path).unwrap();
389        let deserialized: OpenApiDocument = serde_json::from_str(&content).unwrap();
390        
391        assert_eq!(deserialized.info.title, "Test API");
392    }
393}