use std::collections::{HashMap, HashSet};
use std::io::Write;
use anyhow::Result;
use crate::models::{ApiDocumentation, DetailLevel, DocConfig, Endpoint, GroupBy};
use crate::utils::{clean_for_id, extract_content_type};
pub fn generate_markdown<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
if config.detail_level == DetailLevel::Summary {
generate_summary(writer, doc, config)
} else {
match config.group_by {
GroupBy::Service => generate_by_service(writer, doc, config),
GroupBy::Method => generate_by_method(writer, doc, config),
GroupBy::Flat => generate_flat(writer, doc, config),
}
}
}
fn generate_summary<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
writeln!(writer, "# {}", doc.title)?;
if let Some(description) = &doc.description {
writeln!(writer, "\n{}\n", description)?;
}
writeln!(writer, "API Version: {}\n", doc.version)?;
if !doc.servers.is_empty() && config.include_auth {
writeln!(writer, "## Server URLs")?;
for server in &doc.servers {
writeln!(writer, "* {}", server)?;
}
writeln!(writer)?;
}
if !doc.security_schemes.is_empty() && config.include_auth {
writeln!(writer, "## Authentication")?;
for (name, desc) in &doc.security_schemes {
writeln!(writer, "* **{}**: {}", name, desc)?;
}
writeln!(writer)?;
}
let services = if let Some(filter) = &config.service_filter {
let filter_set: HashSet<_> = filter.iter().collect();
doc.services
.iter()
.filter(|s| filter_set.contains(&s.name))
.collect::<Vec<_>>()
} else {
doc.services.iter().collect()
};
let mut service_endpoints: HashMap<&str, Vec<&Endpoint>> = HashMap::new();
for endpoint in &doc.endpoints {
if config.exclude_deprecated && endpoint.deprecated {
continue;
}
if let Some(methods) = &config.method_filter {
if !methods.contains(&endpoint.method) {
continue;
}
}
if let Some(path_pattern) = &config.path_filter {
if !endpoint.path.contains(path_pattern) {
continue;
}
}
for service_name in &endpoint.services {
service_endpoints
.entry(service_name)
.or_default()
.push(endpoint);
}
}
writeln!(writer, "## Services")?;
for service in &services {
writeln!(writer, "- {}", service.name)?;
if let Some(endpoints) = service_endpoints.get(&service.name as &str) {
let mut sorted_ops = endpoints.clone();
match config.sort_method {
crate::models::SortMethod::Alphabetical => {
sorted_ops.sort_by(|a, b| {
a.operation_id
.clone()
.unwrap_or_default()
.cmp(&b.operation_id.clone().unwrap_or_default())
});
}
crate::models::SortMethod::PathLength => {
sorted_ops.sort_by_key(|a| a.path.len());
}
crate::models::SortMethod::None => {}
}
for endpoint in sorted_ops {
let op_name = if let Some(operation_id) = &endpoint.operation_id {
if operation_id.starts_with(&format!("{}_", service.name)) {
operation_id.replacen(&format!("{}_", service.name), "", 1)
} else {
operation_id.clone()
}
} else {
format!("{} {}", endpoint.method, endpoint.path)
};
writeln!(writer, " * {}", op_name)?;
}
}
}
Ok(())
}
fn generate_by_service<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
writeln!(writer, "# {}", doc.title)?;
if let Some(description) = &doc.description {
writeln!(writer, "\n{}\n", description)?;
}
writeln!(writer, "API Version: {}\n", doc.version)?;
if !doc.servers.is_empty() && config.include_auth {
writeln!(writer, "## Server URLs")?;
for server in &doc.servers {
writeln!(writer, "* {}", server)?;
}
writeln!(writer)?;
}
if !doc.security_schemes.is_empty() && config.include_auth {
writeln!(writer, "## Authentication")?;
for (name, desc) in &doc.security_schemes {
writeln!(writer, "* **{}**: {}", name, desc)?;
}
writeln!(writer)?;
}
let services = if let Some(filter) = &config.service_filter {
let filter_set: HashSet<_> = filter.iter().collect();
doc.services
.iter()
.filter(|s| filter_set.contains(&s.name))
.collect::<Vec<_>>()
} else {
doc.services.iter().collect()
};
let mut service_endpoints: HashMap<&str, Vec<&Endpoint>> = HashMap::new();
for endpoint in &doc.endpoints {
if config.exclude_deprecated && endpoint.deprecated {
continue;
}
if let Some(methods) = &config.method_filter {
if !methods.contains(&endpoint.method) {
continue;
}
}
if let Some(path_pattern) = &config.path_filter {
if !endpoint.path.contains(path_pattern) {
continue;
}
}
for service_name in &endpoint.services {
service_endpoints
.entry(service_name)
.or_default()
.push(endpoint);
}
}
if config.include_toc {
writeln!(writer, "## Services\n")?;
for service in &services {
let anchor = clean_for_id(&service.name);
writeln!(writer, "- [{}](#{anchor})", service.name)?;
if let Some(endpoints) = service_endpoints.get(&service.name as &str) {
let mut sorted_ops = endpoints.clone();
match config.sort_method {
crate::models::SortMethod::Alphabetical => {
sorted_ops.sort_by(|a, b| {
a.summary
.clone()
.unwrap_or_default()
.cmp(&b.summary.clone().unwrap_or_default())
});
}
crate::models::SortMethod::PathLength => {
sorted_ops.sort_by_key(|a| a.path.len());
}
crate::models::SortMethod::None => {}
}
for endpoint in sorted_ops {
let op_title = get_short_title(endpoint);
let op_anchor = clean_for_id(&op_title);
writeln!(writer, " * [{}](#{op_anchor})", op_title)?;
}
}
}
writeln!(writer)?;
}
for service in &services {
let anchor = clean_for_id(&service.name);
writeln!(writer, "## {} {{#{}}}", service.name, anchor)?;
if let Some(description) = &service.description {
writeln!(writer, "\n{}", description)?;
}
if let Some(endpoints) = service_endpoints.get(&service.name as &str) {
let mut sorted_endpoints = endpoints.clone();
match config.sort_method {
crate::models::SortMethod::Alphabetical => {
sorted_endpoints.sort_by(|a, b| a.path.cmp(&b.path));
}
crate::models::SortMethod::PathLength => {
sorted_endpoints.sort_by_key(|a| a.path.len());
}
crate::models::SortMethod::None => {}
}
for endpoint in sorted_endpoints {
write_endpoint(writer, endpoint, config, true)?;
}
} else {
writeln!(writer, "\nNo endpoints found for this service.\n")?;
}
}
Ok(())
}
fn generate_by_method<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
writeln!(writer, "# {}", doc.title)?;
if let Some(description) = &doc.description {
writeln!(writer, "\n{}\n", description)?;
}
writeln!(writer, "API Version: {}\n", doc.version)?;
if !doc.servers.is_empty() && config.include_auth {
writeln!(writer, "## Server URLs")?;
for server in &doc.servers {
writeln!(writer, "* {}", server)?;
}
writeln!(writer)?;
}
if !doc.security_schemes.is_empty() && config.include_auth {
writeln!(writer, "## Authentication")?;
for (name, desc) in &doc.security_schemes {
writeln!(writer, "* **{}**: {}", name, desc)?;
}
writeln!(writer)?;
}
let mut method_endpoints: HashMap<&str, Vec<&Endpoint>> = HashMap::new();
for endpoint in &doc.endpoints {
if config.exclude_deprecated && endpoint.deprecated {
continue;
}
if let Some(services) = &config.service_filter {
if !endpoint.services.iter().any(|s| services.contains(s)) {
continue;
}
}
if let Some(path_pattern) = &config.path_filter {
if !endpoint.path.contains(path_pattern) {
continue;
}
}
if let Some(methods) = &config.method_filter {
if !methods.contains(&endpoint.method) {
continue;
}
}
method_endpoints
.entry(&endpoint.method)
.or_default()
.push(endpoint);
}
if config.include_toc {
writeln!(writer, "## HTTP Methods\n")?;
for method in [
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE",
] {
if let Some(endpoints) = method_endpoints.get(method) {
if !endpoints.is_empty() {
let anchor = clean_for_id(method);
writeln!(writer, "- [{}](#{anchor})", method)?;
}
}
}
writeln!(writer)?;
}
for method in [
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE",
] {
if let Some(endpoints) = method_endpoints.get(method) {
if !endpoints.is_empty() {
let anchor = clean_for_id(method);
writeln!(writer, "## {} {{#{}}}", method, anchor)?;
let mut sorted_endpoints = endpoints.clone();
match config.sort_method {
crate::models::SortMethod::Alphabetical => {
sorted_endpoints.sort_by(|a, b| a.path.cmp(&b.path));
}
crate::models::SortMethod::PathLength => {
sorted_endpoints.sort_by_key(|a| a.path.len());
}
crate::models::SortMethod::None => {}
}
for endpoint in sorted_endpoints {
write_endpoint(writer, endpoint, config, true)?;
}
}
}
}
Ok(())
}
fn generate_flat<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
writeln!(writer, "# {}", doc.title)?;
if let Some(description) = &doc.description {
writeln!(writer, "\n{}\n", description)?;
}
writeln!(writer, "API Version: {}\n", doc.version)?;
if !doc.servers.is_empty() && config.include_auth {
writeln!(writer, "## Server URLs")?;
for server in &doc.servers {
writeln!(writer, "* {}", server)?;
}
writeln!(writer)?;
}
if !doc.security_schemes.is_empty() && config.include_auth {
writeln!(writer, "## Authentication")?;
for (name, desc) in &doc.security_schemes {
writeln!(writer, "* **{}**: {}", name, desc)?;
}
writeln!(writer)?;
}
let mut endpoints: Vec<&Endpoint> = doc
.endpoints
.iter()
.filter(|endpoint| {
if config.exclude_deprecated && endpoint.deprecated {
return false;
}
if let Some(services) = &config.service_filter {
if !endpoint.services.iter().any(|s| services.contains(s)) {
return false;
}
}
if let Some(methods) = &config.method_filter {
if !methods.contains(&endpoint.method) {
return false;
}
}
if let Some(path_pattern) = &config.path_filter {
if !endpoint.path.contains(path_pattern) {
return false;
}
}
true
})
.collect();
match config.sort_method {
crate::models::SortMethod::Alphabetical => {
endpoints.sort_by(|a, b| a.path.cmp(&b.path).then(a.method.cmp(&b.method)));
}
crate::models::SortMethod::PathLength => {
endpoints.sort_by_key(|a| a.path.len());
}
crate::models::SortMethod::None => {}
}
writeln!(writer, "## Endpoints\n")?;
for endpoint in endpoints {
write_endpoint(writer, endpoint, config, true)?;
}
Ok(())
}
fn write_endpoint<W: Write>(
writer: &mut W,
endpoint: &Endpoint,
config: &DocConfig,
include_heading: bool,
) -> Result<()> {
let title = get_short_title(endpoint);
if include_heading {
let anchor = clean_for_id(&title);
writeln!(writer, "### {} {{#{}}}", title, anchor)?;
} else {
writeln!(writer, "**{}**", title)?;
}
writeln!(
writer,
"**Operation:** {} {}",
endpoint.method, endpoint.path
)?;
if let Some(description) = &endpoint.description {
writeln!(writer, "**Description:** {}", description)?;
} else if let Some(summary) = &endpoint.summary {
writeln!(writer, "**Description:** {}", summary)?;
}
if endpoint.deprecated {
writeln!(writer, "\n> **Deprecated**: This endpoint is deprecated.")?;
}
if let Some(operation_id) = &endpoint.operation_id {
writeln!(writer, "**Operation ID:** `{}`", operation_id)?;
}
if config.detail_level != DetailLevel::Basic {
if !endpoint.parameters.is_empty() {
writeln!(writer, "\n#### Parameters")?;
writeln!(writer, "| Name | In | Required | Description |")?;
writeln!(writer, "|------|----|---------:|-------------|")?;
for param in &endpoint.parameters {
if let Some(required) = param.required {
if !required && config.required_only {
continue;
}
}
let required_str = if let Some(req) = param.required {
if req {
"Yes"
} else {
"No"
}
} else {
"No"
};
let desc = param.description.as_deref().unwrap_or("-");
writeln!(
writer,
"| `{}` | {} | {} | {} |",
param.name, param.parameter_in, required_str, desc
)?;
}
}
writeln!(writer, "\n#### Responses")?;
writeln!(writer, "| Code | Type | Description |")?;
writeln!(writer, "|------|------|-------------|")?;
for (code, response) in &endpoint.responses {
let desc = response.description.as_deref().unwrap_or("-");
let content_type = extract_content_type(response).unwrap_or_default();
writeln!(writer, "| {} | {} | {} |", code, content_type, desc)?;
}
if config.include_schemas && config.detail_level == DetailLevel::Full {
writeln!(writer, "\n#### Request Schema")?;
let body_param = endpoint
.parameters
.iter()
.find(|p| p.parameter_in == "body" && p.schema.is_some());
if let Some(param) = body_param {
if let Some(schema) = ¶m.schema {
if let Some(schema_type) = &schema.schema_type {
writeln!(writer, "```json\n// Schema type: {}\n```", schema_type)?;
} else if let Some(ref_val) = &schema.reference {
writeln!(writer, "```json\n// Reference: {}\n```", ref_val)?;
}
}
} else {
writeln!(writer, "*No request schema available*")?;
}
writeln!(writer, "\n#### Response Schema")?;
if let Some((_, response)) = endpoint
.responses
.iter()
.find(|(code, _)| code.starts_with('2'))
{
if let Some(schema) = &response.schema {
if let Some(schema_type) = &schema.schema_type {
writeln!(writer, "```json\n// Schema type: {}\n```", schema_type)?;
} else if let Some(ref_val) = &schema.reference {
writeln!(writer, "```json\n// Reference: {}\n```", ref_val)?;
}
} else if let Some(content) = &response.content {
if let Some((content_type, media_type)) = content.iter().next() {
if media_type.schema.is_some() {
writeln!(writer, "```json\n// Content type: {}\n```", content_type)?;
}
}
} else {
writeln!(writer, "*No response schema available*")?;
}
} else {
writeln!(writer, "*No success response schema available*")?;
}
}
if config.include_examples && config.detail_level == DetailLevel::Full {
writeln!(writer, "\n#### Examples")?;
writeln!(writer, "*Examples would be included here if available*")?;
}
}
writeln!(writer)?; Ok(())
}
fn get_short_title(endpoint: &Endpoint) -> String {
if let Some(operation_id) = &endpoint.operation_id {
return operation_id.clone();
} else if let Some(summary) = &endpoint.summary {
if let Some(first_word) = summary.split_whitespace().next() {
if first_word.chars().any(|c| c.is_uppercase()) {
return first_word.to_string();
}
}
return summary.clone();
}
format!("{} {}", endpoint.method, endpoint.path)
}