Skip to main content

unistructgen_openapi_parser/
parser.rs

1//! Main OpenAPI parser implementation
2
3use crate::client::ClientGenerator;
4use crate::error::{OpenApiError, Result};
5use crate::options::OpenApiParserOptions;
6use crate::schema::SchemaConverter;
7use openapiv3::OpenAPI;
8use unistructgen_core::{IRModule, Parser, ParserMetadata};
9
10/// OpenAPI/Swagger parser
11///
12/// Converts OpenAPI 3.0 specifications into UniStructGen's Intermediate Representation (IR).
13/// Supports both YAML and JSON formats.
14///
15/// # Examples
16///
17/// ```no_run
18/// use unistructgen_openapi_parser::{OpenApiParser, OpenApiParserOptions};
19/// use unistructgen_core::Parser;
20///
21/// let options = OpenApiParserOptions::builder()
22///     .generate_client(true)
23///     .generate_validation(true)
24///     .build();
25///
26/// let mut parser = OpenApiParser::new(options);
27///
28/// let yaml = std::fs::read_to_string("openapi.yaml").unwrap();
29/// let ir_module = parser.parse(&yaml).unwrap();
30///
31/// println!("Generated {} types", ir_module.types.len());
32/// ```
33pub struct OpenApiParser {
34    options: OpenApiParserOptions,
35}
36
37impl OpenApiParser {
38    /// Create a new OpenAPI parser with the given options
39    pub fn new(options: OpenApiParserOptions) -> Self {
40        Self { options }
41    }
42
43    /// Create a new parser with default options
44    pub fn with_defaults() -> Self {
45        Self::new(OpenApiParserOptions::default())
46    }
47
48    /// Parse OpenAPI spec from YAML string
49    fn parse_yaml(&self, input: &str) -> Result<OpenAPI> {
50        serde_yaml::from_str(input).map_err(OpenApiError::from)
51    }
52
53    /// Parse OpenAPI spec from JSON string
54    fn parse_json(&self, input: &str) -> Result<OpenAPI> {
55        serde_json::from_str(input).map_err(OpenApiError::from)
56    }
57
58    /// Parse OpenAPI spec (auto-detect format)
59    fn parse_spec(&self, input: &str) -> Result<OpenAPI> {
60        // Try JSON first (faster)
61        if let Ok(spec) = self.parse_json(input) {
62            return Ok(spec);
63        }
64
65        // Fall back to YAML
66        self.parse_yaml(input)
67    }
68
69    /// Validate OpenAPI specification
70    fn validate_spec(&self, spec: &OpenAPI) -> Result<()> {
71        // Check OpenAPI version
72        if !spec.openapi.starts_with("3.0") && !spec.openapi.starts_with("3.1") {
73            return Err(OpenApiError::invalid_spec(format!(
74                "Unsupported OpenAPI version: {}. Only 3.0 and 3.1 are supported.",
75                spec.openapi
76            )));
77        }
78
79        // Check that spec has components or paths
80        if spec.components.is_none() && spec.paths.paths.is_empty() {
81            return Err(OpenApiError::invalid_spec(
82                "OpenAPI spec must have either components or paths",
83            ));
84        }
85
86        Ok(())
87    }
88}
89
90impl Parser for OpenApiParser {
91    type Error = OpenApiError;
92
93    fn parse(&mut self, input: &str) -> Result<IRModule> {
94        // Parse the OpenAPI specification
95        let spec = self.parse_spec(input)?;
96
97        // Validate the spec
98        self.validate_spec(&spec)?;
99
100        // Create IR module
101        let module_name = if !spec.info.title.is_empty() {
102            spec.info.title.clone()
103        } else {
104            "Api".to_string()
105        };
106        let mut ir_module = IRModule::new(module_name);
107
108        // Convert schemas to IR types
109        let mut converter = SchemaConverter::new(&spec, &self.options);
110        let schema_types = converter.convert_all_schemas()?;
111
112        for ir_type in schema_types {
113            ir_module.add_type(ir_type);
114        }
115
116        // Generate API client types if requested
117        if self.options.generate_client {
118            let client_gen = ClientGenerator::new(&spec, &self.options);
119            let client_types = client_gen.generate_client_types()?;
120
121            for ir_type in client_types {
122                ir_module.add_type(ir_type);
123            }
124        }
125
126        Ok(ir_module)
127    }
128
129    fn name(&self) -> &'static str {
130        "OpenAPI"
131    }
132
133    fn extensions(&self) -> &[&'static str] {
134        &["yaml", "yml", "json"]
135    }
136
137    fn validate(&self, input: &str) -> Result<()> {
138        let spec = self.parse_spec(input)?;
139        self.validate_spec(&spec)?;
140        Ok(())
141    }
142
143    fn metadata(&self) -> ParserMetadata {
144        let mut custom = std::collections::HashMap::new();
145        custom.insert("name".to_string(), self.name().to_string());
146        custom.insert(
147            "supported_formats".to_string(),
148            self.extensions().join(", "),
149        );
150
151        ParserMetadata {
152            version: Some(env!("CARGO_PKG_VERSION").to_string()),
153            description: Some(
154                "Parses OpenAPI 3.0/3.1 specifications and generates Rust types".to_string(),
155            ),
156            features: vec![
157                "Schema to Struct conversion".to_string(),
158                "Enum generation from string enums".to_string(),
159                "API client trait generation".to_string(),
160                "Validation constraint extraction".to_string(),
161                "Reference ($ref) resolution".to_string(),
162                "Schema composition (allOf, oneOf, anyOf)".to_string(),
163            ],
164            custom,
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    const SIMPLE_OPENAPI: &str = r#"
174openapi: 3.0.0
175info:
176  title: Test API
177  version: 1.0.0
178paths: {}
179components:
180  schemas:
181    User:
182      type: object
183      required:
184        - id
185        - name
186      properties:
187        id:
188          type: integer
189          format: int64
190        name:
191          type: string
192        email:
193          type: string
194          format: email
195    "#;
196
197    #[test]
198    fn test_parse_simple_spec() {
199        let options = OpenApiParserOptions::default();
200        let mut parser = OpenApiParser::new(options);
201
202        let result = parser.parse(SIMPLE_OPENAPI);
203        if let Err(ref e) = result {
204            eprintln!("Parse error: {:?}", e);
205        }
206        assert!(result.is_ok());
207
208        let module = result.unwrap();
209        assert_eq!(module.types.len(), 1);
210    }
211
212    #[test]
213    fn test_parser_metadata() {
214        let parser = OpenApiParser::with_defaults();
215        let metadata = parser.metadata();
216
217        assert_eq!(metadata.custom.get("name"), Some(&"OpenAPI".to_string()));
218        assert!(metadata
219            .custom
220            .get("supported_formats")
221            .map(|s| s.contains("yaml"))
222            .unwrap_or(false));
223        assert!(!metadata.features.is_empty());
224    }
225
226    #[test]
227    fn test_validate_spec() {
228        let parser = OpenApiParser::with_defaults();
229        let result = parser.validate(SIMPLE_OPENAPI);
230        assert!(result.is_ok());
231    }
232
233    #[test]
234    fn test_invalid_version() {
235        let invalid_spec = r#"
236openapi: 2.0.0
237info:
238  title: Old API
239  version: 1.0.0
240paths: {}
241        "#;
242
243        let mut parser = OpenApiParser::with_defaults();
244        let result = parser.parse(invalid_spec);
245        assert!(result.is_err());
246    }
247}