unistructgen_openapi_parser/
parser.rs1use 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
10pub struct OpenApiParser {
34 options: OpenApiParserOptions,
35}
36
37impl OpenApiParser {
38 pub fn new(options: OpenApiParserOptions) -> Self {
40 Self { options }
41 }
42
43 pub fn with_defaults() -> Self {
45 Self::new(OpenApiParserOptions::default())
46 }
47
48 fn parse_yaml(&self, input: &str) -> Result<OpenAPI> {
50 serde_yaml::from_str(input).map_err(OpenApiError::from)
51 }
52
53 fn parse_json(&self, input: &str) -> Result<OpenAPI> {
55 serde_json::from_str(input).map_err(OpenApiError::from)
56 }
57
58 fn parse_spec(&self, input: &str) -> Result<OpenAPI> {
60 if let Ok(spec) = self.parse_json(input) {
62 return Ok(spec);
63 }
64
65 self.parse_yaml(input)
67 }
68
69 fn validate_spec(&self, spec: &OpenAPI) -> Result<()> {
71 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 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 let spec = self.parse_spec(input)?;
96
97 self.validate_spec(&spec)?;
99
100 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 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 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}