use crate::generator::{
code_generator::{CodeGenerator, Visibility},
operation_converter::OperationConverter,
schema_converter::SchemaConverter,
schema_graph::SchemaGraph,
};
const OAS3_GEN_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug)]
pub struct Orchestrator {
spec: oas3::Spec,
visibility: Visibility,
}
#[derive(Debug, Clone)]
pub struct CodeMetadata {
pub title: String,
pub version: String,
pub description: Option<String>,
}
#[derive(Debug)]
pub struct GenerationStats {
pub types_generated: usize,
pub operations_converted: usize,
pub cycles_detected: usize,
pub cycle_details: Vec<Vec<String>>,
pub warnings: Vec<String>,
}
impl Orchestrator {
#[must_use]
pub const fn new(spec: oas3::Spec, visibility: Visibility) -> Self {
Self { spec, visibility }
}
pub fn metadata(&self) -> CodeMetadata {
CodeMetadata {
title: self.spec.info.title.clone(),
version: self.spec.info.version.clone(),
description: self.spec.info.description.clone(),
}
}
pub async fn generate(&self) -> anyhow::Result<(String, GenerationStats)> {
let mut graph = SchemaGraph::new(self.spec.clone())?;
graph.build_dependencies();
let cycle_details = graph.detect_cycles();
let schema_converter = SchemaConverter::new(&graph);
let mut rust_types = Vec::new();
let mut warnings = Vec::new();
for schema_name in graph.schema_names() {
if let Some(schema) = graph.get_schema(schema_name) {
match schema_converter.convert_schema(schema_name, schema).await {
Ok(types) => rust_types.extend(types),
Err(e) => warnings.push(format!("Failed to convert schema {schema_name}: {e}")),
}
}
}
let operation_converter = OperationConverter::new(&schema_converter, graph.spec());
let mut operations_info = Vec::new();
if let Some(ref paths) = graph.spec().paths {
for (path, path_item) in paths {
for (method, operation) in path_item.methods() {
let method_str = method.as_str();
let operation_id = operation.operation_id.as_deref().unwrap_or("unknown");
match operation_converter
.convert_operation(operation_id, method_str, path, operation)
.await
{
Ok((types, op_info)) => {
rust_types.extend(types);
operations_info.push(op_info);
}
Err(e) => {
warnings.push(format!("Failed to convert operation {method_str} {path}: {e}"));
}
}
}
}
}
let type_usage = CodeGenerator::build_type_usage_map(&operations_info);
let code = CodeGenerator::generate(&rust_types, &type_usage, &graph.all_headers(), self.visibility);
let syntax_tree = syn::parse2(code)?;
let formatted = prettyplease::unparse(&syntax_tree);
let stats = GenerationStats {
types_generated: rust_types.len(),
operations_converted: operations_info.len(),
cycles_detected: cycle_details.len(),
cycle_details,
warnings,
};
Ok((formatted, stats))
}
pub async fn generate_with_header(&self, source_path: &str) -> anyhow::Result<(String, GenerationStats)> {
let (code, stats) = self.generate().await?;
let metadata = self.metadata();
let description = metadata.description.as_ref().map_or_else(
|| String::from("No description provided"),
|d| d.replace('\n', "\n//! "),
);
let final_code = format!(
r"#![allow(clippy::doc_markdown)]
#![allow(clippy::large_enum_variant)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::result_large_err)]
//!
//! AUTO-GENERATED CODE - DO NOT EDIT!
//!
//! {}
//! Source: {}
//! Version: {}
//! Generated by `oas3-gen v{}`
//!
//! {}
{}
fn main() {{}}
",
metadata.title, source_path, metadata.version, OAS3_GEN_VERSION, description, code
);
Ok((final_code, stats))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_orchestrator_empty_spec() {
let spec_json = r#"{
"openapi": "3.1.0",
"info": {
"title": "Empty API",
"version": "1.0.0"
},
"paths": {}
}"#;
let spec: oas3::Spec = oas3::from_json(spec_json).unwrap();
let orchestrator = Orchestrator::new(spec, Visibility::default());
let metadata = orchestrator.metadata();
assert_eq!(metadata.title, "Empty API");
assert_eq!(metadata.version, "1.0.0");
}
#[tokio::test]
async fn test_orchestrator_generate_empty() {
let spec_json = r#"{
"openapi": "3.1.0",
"info": {
"title": "Empty API",
"version": "1.0.0"
},
"paths": {}
}"#;
let spec: oas3::Spec = oas3::from_json(spec_json).unwrap();
let orchestrator = Orchestrator::new(spec, Visibility::default());
let result = orchestrator.generate().await;
assert!(result.is_ok());
let (code, stats) = result.unwrap();
assert!(!code.is_empty());
assert_eq!(stats.types_generated, 0);
assert_eq!(stats.operations_converted, 0);
assert_eq!(stats.cycles_detected, 0);
assert_eq!(stats.warnings.len(), 0);
}
#[tokio::test]
async fn test_orchestrator_generate_with_header() {
let spec_json = r#"{
"openapi": "3.1.0",
"info": {
"title": "Test API",
"version": "2.0.0",
"description": "A test API"
},
"paths": {}
}"#;
let spec: oas3::Spec = oas3::from_json(spec_json).unwrap();
let orchestrator = Orchestrator::new(spec, Visibility::default());
let result = orchestrator.generate_with_header("/path/to/spec.json").await;
assert!(result.is_ok());
let (code, _) = result.unwrap();
assert!(code.contains("//! Test API"));
assert!(code.contains("//! Source: /path/to/spec.json"));
assert!(code.contains("//! Version: 2.0.0"));
assert!(code.contains("//! A test API"));
assert!(code.contains("fn main()"));
}
#[test]
fn test_code_metadata() {
let spec_json = r#"{
"openapi": "3.1.0",
"info": {
"title": "Test API",
"version": "1.0.0",
"description": "Multi\nline\ndescription"
},
"paths": {}
}"#;
let spec: oas3::Spec = oas3::from_json(spec_json).unwrap();
let orchestrator = Orchestrator::new(spec, Visibility::default());
let metadata = orchestrator.metadata();
assert_eq!(metadata.title, "Test API");
assert_eq!(metadata.version, "1.0.0");
assert_eq!(metadata.description, Some("Multi\nline\ndescription".to_string()));
}
}