use crate::generator::{CodeGenerator, OperationConverter, SchemaConverter, SchemaGraph};
use clap::Parser;
use std::path::PathBuf;
mod generator;
mod reserved;
#[derive(Parser, Debug)]
#[command(name = "openapi-gen")]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long, value_name = "FILE")]
input: PathBuf,
#[arg(short, long, value_name = "FILE")]
output: PathBuf,
#[arg(short, long, default_value_t = false)]
verbose: bool,
#[arg(short, long, default_value_t = false)]
quiet: bool,
}
macro_rules! log_info {
($cli:expr, $($arg:tt)*) => {
if !$cli.quiet {
println!($($arg)*);
}
};
}
macro_rules! log_verbose {
($cli:expr, $($arg:tt)*) => {
if $cli.verbose && !$cli.quiet {
println!($($arg)*);
}
};
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
log_info!(cli, "Loading OpenAPI spec from: {}", cli.input.display());
let file_content = tokio::fs::read_to_string(&cli.input).await?;
let spec = oas3::from_json(file_content)?;
log_info!(cli, "Building schema graph...");
let info = spec.info.clone();
let mut graph = SchemaGraph::new(spec)?;
log_info!(cli, "Found {} schemas", graph.schema_names().len());
log_verbose!(cli, "\nBuilding dependency graph...");
graph.build_dependencies();
log_verbose!(cli, "Detecting cycles...");
let cycles = graph.detect_cycles();
if !cycles.is_empty() {
log_info!(cli, "Found {} cycles:", cycles.len());
for (i, cycle) in cycles.iter().enumerate() {
log_verbose!(cli, " Cycle {}: {}", i + 1, cycle.join(" -> "));
}
} else {
log_verbose!(cli, "No cycles detected!");
}
log_info!(cli, "\nConverting schemas to Rust AST...");
let schema_converter = SchemaConverter::new(&graph);
let mut rust_types = 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) {
Ok(types) => rust_types.extend(types),
Err(e) => eprintln!("Warning: Failed to convert schema {}: {}", schema_name, e),
}
}
}
log_info!(cli, "Converted {} schema types", rust_types.len());
log_info!(cli, "\nConverting operations to Rust AST...");
let operation_converter = OperationConverter::new(&schema_converter, graph.spec());
let mut operations_info = Vec::new();
if let Some(ref paths) = graph.spec().paths {
let mut path_entries: Vec<_> = paths.iter().collect();
path_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
for (path, path_item) in path_entries {
let mut methods: Vec<_> = path_item.methods().into_iter().collect();
methods.sort_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()));
for (method, operation) in 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) {
Ok((types, op_info)) => {
rust_types.extend(types);
operations_info.push(op_info);
}
Err(e) => {
eprintln!("Warning: Failed to convert operation {} {}: {}", method_str, path, e);
}
}
}
}
}
log_verbose!(cli, "Converted {} operations", operations_info.len());
log_info!(cli, "Total types generated: {}", rust_types.len());
log_info!(cli, "\nGenerating Rust code...");
let type_usage = CodeGenerator::build_type_usage_map(&operations_info);
let code = CodeGenerator::generate(&rust_types, &type_usage);
log_info!(cli, "Formatting code...");
let syntax_tree = syn::parse2(code)?;
let mut formatted = prettyplease::unparse(&syntax_tree);
formatted = format!(
r#"//! AUTO-GENERATED CODE - DO NOT EDIT!
//!
//! {}
//! Source: {}
//! Version: {}
//! Generated by `openapi-gen`
//!
{}
#![allow(clippy::large_enum_variant)]
{}
fn main() {{}}
"#,
info.title,
cli.input.display(),
info.version,
info.description.unwrap_or_default().replace("\n", "\n//! "),
formatted
);
log_info!(cli, "Writing to output file: {}", cli.output.display());
if let Some(parent) = cli.output.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(&cli.output, formatted).await?;
log_info!(cli, "\n✓ Successfully generated Rust types!");
log_info!(cli, " Input: {}", cli.input.display());
log_info!(cli, " Output: {}", cli.output.display());
log_info!(cli, " Types: {}", rust_types.len());
if !cycles.is_empty() {
log_info!(cli, " Cycles: {}", cycles.len());
}
Ok(())
}