mod config;
mod diff;
mod markdown;
mod models;
mod parser;
mod report;
mod selection;
mod stats;
mod utils;
use std::fs::{self, File};
use std::io::{BufWriter, Write, stdout};
use std::path::Path;
use std::process::ExitCode;
use anyhow::{Context, Result, bail};
use clap::{CommandFactory, Parser};
use log::info;
use crate::config::{Cli, Commands, DiffArgs, DiffFormatArg, build_config};
use crate::markdown::generate_markdown;
use crate::models::{ApiDocumentation, DocConfig};
use crate::parser::{parse_openapi, parse_openapi_bytes};
fn write_output<W: Write>(
writer: &mut W,
api_doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
if !config.include_report {
return generate_markdown(writer, api_doc, config).context("Failed to generate markdown");
}
let mut body = Vec::new();
generate_markdown(&mut body, api_doc, config).context("Failed to generate markdown")?;
while body.last() == Some(&b'\n') {
body.pop();
}
body.push(b'\n');
writer
.write_all(&body)
.context("Failed to write documentation")?;
let hygiene = report::analyze(api_doc, config);
report::write_report(writer, &hygiene).context("Failed to write spec hygiene report")?;
Ok(())
}
const EXIT_BREAKING_CHANGES: u8 = 3;
fn run_diff(args: &DiffArgs) -> Result<ExitCode> {
let old_bytes = fs::read(&args.old)
.with_context(|| format!("Failed to parse OpenAPI file: {:?}", args.old))?;
let new_bytes = fs::read(&args.new)
.with_context(|| format!("Failed to parse OpenAPI file: {:?}", args.new))?;
let old_sha = diff::json::sha256_hex(&old_bytes);
let new_sha = diff::json::sha256_hex(&new_bytes);
let old = parse_openapi_bytes(&old_bytes, &file_extension(&args.old), Some(&args.old))
.with_context(|| format!("Failed to parse OpenAPI file: {:?}", args.old))?;
let new = parse_openapi_bytes(&new_bytes, &file_extension(&args.new), Some(&args.new))
.with_context(|| format!("Failed to parse OpenAPI file: {:?}", args.new))?;
let spec_diff = diff::diff(&old, &new);
let deltas = if args.report {
Some(diff::compute_deltas(&old, &new).context("Failed to compute deltas")?)
} else {
None
};
match (&args.output, args.format) {
(Some(output_path), DiffFormatArg::Markdown) => {
let mut writer = BufWriter::new(create_output_file(output_path)?);
diff::write_diff(&mut writer, &spec_diff, deltas.as_ref())
.context("Failed to write diff")?;
writer.flush().context("Failed to write diff")?;
info!("Diff written to: {:?}", output_path);
}
(Some(output_path), DiffFormatArg::Json) => {
let document = diff::json::to_json(&spec_diff, deltas.as_ref(), &old_sha, &new_sha);
let mut writer = BufWriter::new(create_output_file(output_path)?);
write_json_diff(&mut writer, &document)?;
info!("Diff written to: {:?}", output_path);
}
(None, DiffFormatArg::Markdown) => {
diff::write_diff(&mut stdout(), &spec_diff, deltas.as_ref())
.context("Failed to write diff")?;
stdout().flush().context("Failed to write diff")?;
}
(None, DiffFormatArg::Json) => {
let document = diff::json::to_json(&spec_diff, deltas.as_ref(), &old_sha, &new_sha);
write_json_diff(&mut stdout(), &document)?;
}
}
if args.fail_on_breaking && spec_diff.has_breaking() {
return Ok(ExitCode::from(EXIT_BREAKING_CHANGES));
}
Ok(ExitCode::SUCCESS)
}
fn file_extension(path: &Path) -> String {
path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or_default()
.to_string()
}
fn write_json_diff<W: Write>(writer: &mut W, document: &diff::json::JsonDiff) -> Result<()> {
serde_json::to_writer_pretty(&mut *writer, document).context("Failed to write diff")?;
writer.write_all(b"\n").context("Failed to write diff")?;
writer.flush().context("Failed to write diff")
}
fn warn_filtered_out_selectors(api_doc: &ApiDocumentation, config: &DocConfig) {
for warning in selection::filtered_out_warnings(api_doc, config) {
eprintln!("{warning}");
}
}
fn create_output_file(path: &Path) -> Result<File> {
File::create(path).with_context(|| format!("Failed to create output file: {:?}", path))
}
fn run() -> Result<ExitCode> {
env_logger::init();
let cli = Cli::parse();
match &cli.command {
Some(Commands::Completions { shell }) => {
clap_complete::generate(*shell, &mut Cli::command(), "vimanam", &mut stdout());
return Ok(ExitCode::SUCCESS);
}
Some(Commands::Diff(args)) => return run_diff(args),
None => {}
}
let Some(input) = &cli.input else {
bail!("missing input file; run `vimanam --help` for usage");
};
let config = build_config(&cli);
let api_doc =
parse_openapi(input).with_context(|| format!("Failed to parse OpenAPI file: {input:?}"))?;
if let Some(selector) = &config.operation_selector
&& let Some(message) = selection::unmatched_selectors(&api_doc, selector)
{
bail!(message);
}
markdown::schema_selection::validate(&api_doc, &config)?;
if cli.stats {
let stats = stats::compute(&api_doc, &config).context("Failed to compute stats")?;
stats::write_stats(&mut stdout(), &stats).context("Failed to write stats")?;
warn_filtered_out_selectors(&api_doc, &config);
return Ok(ExitCode::SUCCESS);
}
let tree_layout = cli
.split
.map(markdown::split::TreeLayout::Split)
.or_else(|| cli.output_mode.map(|_| markdown::split::TreeLayout::Skill));
if let Some(layout) = tree_layout {
markdown::split::write_tree(
cli.output.as_ref().expect("clap requires --output"),
input,
&api_doc,
&config,
layout,
cli.overview_max_tokens,
)?;
warn_filtered_out_selectors(&api_doc, &config);
return Ok(ExitCode::SUCCESS);
}
if let Some(output_path) = &cli.output {
let mut writer = BufWriter::new(create_output_file(output_path)?);
write_output(&mut writer, &api_doc, &config)?;
info!("Documentation written to: {:?}", output_path);
} else {
let mut writer = stdout();
write_output(&mut writer, &api_doc, &config)?;
}
warn_filtered_out_selectors(&api_doc, &config);
Ok(ExitCode::SUCCESS)
}
fn main() -> ExitCode {
match run() {
Ok(code) => code,
Err(err) => {
eprintln!("Error: {:#}", err);
ExitCode::from(1)
}
}
}