use crate::client::ClientGenerator;
use crate::error::{OpenApiError, Result};
use crate::options::OpenApiParserOptions;
use crate::schema::SchemaConverter;
use openapiv3::OpenAPI;
use unistructgen_core::{IRModule, Parser, ParserMetadata};
pub struct OpenApiParser {
options: OpenApiParserOptions,
}
impl OpenApiParser {
pub fn new(options: OpenApiParserOptions) -> Self {
Self { options }
}
pub fn with_defaults() -> Self {
Self::new(OpenApiParserOptions::default())
}
fn parse_yaml(&self, input: &str) -> Result<OpenAPI> {
serde_yaml::from_str(input).map_err(OpenApiError::from)
}
fn parse_json(&self, input: &str) -> Result<OpenAPI> {
serde_json::from_str(input).map_err(OpenApiError::from)
}
fn parse_spec(&self, input: &str) -> Result<OpenAPI> {
if let Ok(spec) = self.parse_json(input) {
return Ok(spec);
}
self.parse_yaml(input)
}
fn validate_spec(&self, spec: &OpenAPI) -> Result<()> {
if !spec.openapi.starts_with("3.0") && !spec.openapi.starts_with("3.1") {
return Err(OpenApiError::invalid_spec(format!(
"Unsupported OpenAPI version: {}. Only 3.0 and 3.1 are supported.",
spec.openapi
)));
}
if spec.components.is_none() && spec.paths.paths.is_empty() {
return Err(OpenApiError::invalid_spec(
"OpenAPI spec must have either components or paths",
));
}
Ok(())
}
}
impl Parser for OpenApiParser {
type Error = OpenApiError;
fn parse(&mut self, input: &str) -> Result<IRModule> {
let spec = self.parse_spec(input)?;
self.validate_spec(&spec)?;
let module_name = if !spec.info.title.is_empty() {
spec.info.title.clone()
} else {
"Api".to_string()
};
let mut ir_module = IRModule::new(module_name);
let mut converter = SchemaConverter::new(&spec, &self.options);
let schema_types = converter.convert_all_schemas()?;
for ir_type in schema_types {
ir_module.add_type(ir_type);
}
if self.options.generate_client {
let client_gen = ClientGenerator::new(&spec, &self.options);
let client_types = client_gen.generate_client_types()?;
for ir_type in client_types {
ir_module.add_type(ir_type);
}
}
Ok(ir_module)
}
fn name(&self) -> &'static str {
"OpenAPI"
}
fn extensions(&self) -> &[&'static str] {
&["yaml", "yml", "json"]
}
fn validate(&self, input: &str) -> Result<()> {
let spec = self.parse_spec(input)?;
self.validate_spec(&spec)?;
Ok(())
}
fn metadata(&self) -> ParserMetadata {
let mut custom = std::collections::HashMap::new();
custom.insert("name".to_string(), self.name().to_string());
custom.insert(
"supported_formats".to_string(),
self.extensions().join(", "),
);
ParserMetadata {
version: Some(env!("CARGO_PKG_VERSION").to_string()),
description: Some(
"Parses OpenAPI 3.0/3.1 specifications and generates Rust types".to_string(),
),
features: vec![
"Schema to Struct conversion".to_string(),
"Enum generation from string enums".to_string(),
"API client trait generation".to_string(),
"Validation constraint extraction".to_string(),
"Reference ($ref) resolution".to_string(),
"Schema composition (allOf, oneOf, anyOf)".to_string(),
],
custom,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const SIMPLE_OPENAPI: &str = r#"
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths: {}
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
email:
type: string
format: email
"#;
#[test]
fn test_parse_simple_spec() {
let options = OpenApiParserOptions::default();
let mut parser = OpenApiParser::new(options);
let result = parser.parse(SIMPLE_OPENAPI);
if let Err(ref e) = result {
eprintln!("Parse error: {:?}", e);
}
assert!(result.is_ok());
let module = result.unwrap();
assert_eq!(module.types.len(), 1);
}
#[test]
fn test_parser_metadata() {
let parser = OpenApiParser::with_defaults();
let metadata = parser.metadata();
assert_eq!(metadata.custom.get("name"), Some(&"OpenAPI".to_string()));
assert!(metadata
.custom
.get("supported_formats")
.map(|s| s.contains("yaml"))
.unwrap_or(false));
assert!(!metadata.features.is_empty());
}
#[test]
fn test_validate_spec() {
let parser = OpenApiParser::with_defaults();
let result = parser.validate(SIMPLE_OPENAPI);
assert!(result.is_ok());
}
#[test]
fn test_invalid_version() {
let invalid_spec = r#"
openapi: 2.0.0
info:
title: Old API
version: 1.0.0
paths: {}
"#;
let mut parser = OpenApiParser::with_defaults();
let result = parser.parse(invalid_spec);
assert!(result.is_err());
}
}