use std::collections::HashMap;
use std::io::Write;
use anyhow::Result;
use indexmap::IndexMap;
use crate::models::{ApiDocumentation, DocConfig, Endpoint, SortMethod};
use crate::utils::clean_for_id;
use super::endpoint::{get_short_title, write_endpoint};
use super::schema::{SchemaContext, render_schema_definitions};
fn endpoint_anchor(prefix: Option<&str>, endpoint: &Endpoint) -> String {
match prefix {
Some(prefix) => clean_for_id(&format!("{} {} {}", prefix, endpoint.method, endpoint.path)),
None => clean_for_id(&format!("{} {}", endpoint.method, endpoint.path)),
}
}
fn sort_endpoints(endpoints: &mut [&Endpoint], sort_method: &SortMethod) {
match sort_method {
SortMethod::Alphabetical => {
endpoints.sort_by(|a, b| a.path.cmp(&b.path).then(a.method.cmp(&b.method)));
}
SortMethod::PathLength => {
endpoints.sort_by_key(|a| a.path.len());
}
SortMethod::None => {}
}
}
fn service_matches_filter(name: &str, filter: &[String]) -> bool {
filter.iter().any(|entry| entry.eq_ignore_ascii_case(name))
}
pub(crate) fn service_is_visible(name: &str, config: &DocConfig) -> bool {
match &config.service_filter {
Some(filter) => service_matches_filter(name, filter),
None => true,
}
}
pub(crate) fn visible_endpoints<'a>(
doc: &'a ApiDocumentation,
config: &DocConfig,
) -> Vec<&'a Endpoint> {
let mut endpoints: Vec<&Endpoint> = doc
.endpoints
.iter()
.filter(|endpoint| {
passes_filters(endpoint, config) && passes_service_filter(endpoint, config)
})
.collect();
sort_endpoints(&mut endpoints, &config.sort_method);
endpoints
}
fn passes_filters(endpoint: &Endpoint, config: &DocConfig) -> bool {
if config.exclude_deprecated && endpoint.deprecated {
return false;
}
if let Some(methods) = &config.method_filter
&& !methods.contains(&endpoint.method)
{
return false;
}
if let Some(path_pattern) = &config.path_filter
&& !endpoint.path.contains(path_pattern)
{
return false;
}
true
}
fn passes_service_filter(endpoint: &Endpoint, config: &DocConfig) -> bool {
match &config.service_filter {
Some(filter) => endpoint
.services
.iter()
.any(|s| service_matches_filter(s, filter)),
None => true,
}
}
fn write_preamble<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)?;
}
Ok(())
}
pub(super) fn generate_summary<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
write_preamble(writer, doc, config)?;
let services = if let Some(filter) = &config.service_filter {
doc.services
.iter()
.filter(|s| service_matches_filter(&s.name, filter))
.collect::<Vec<_>>()
} else {
doc.services.iter().collect()
};
let mut service_endpoints: HashMap<&str, Vec<&Endpoint>> = HashMap::new();
for endpoint in &doc.endpoints {
if !passes_filters(endpoint, config) {
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();
sort_endpoints(&mut sorted_ops, &config.sort_method);
for endpoint in sorted_ops {
let op_name = if let Some(operation_id) = &endpoint.operation_id {
operation_id
.strip_prefix(&format!("{}_", service.name))
.unwrap_or(operation_id)
.to_string()
} else {
format!("{} {}", endpoint.method, endpoint.path)
};
writeln!(writer, " * {}", op_name)?;
}
}
}
Ok(())
}
pub(super) fn generate_by_service<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
write_preamble(writer, doc, config)?;
let services = if let Some(filter) = &config.service_filter {
doc.services
.iter()
.filter(|s| service_matches_filter(&s.name, filter))
.collect::<Vec<_>>()
} else {
doc.services.iter().collect()
};
let mut service_endpoints: HashMap<&str, Vec<&Endpoint>> = HashMap::new();
for endpoint in &doc.endpoints {
if !passes_filters(endpoint, config) {
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();
sort_endpoints(&mut sorted_ops, &config.sort_method);
for endpoint in sorted_ops {
let op_title = get_short_title(endpoint);
let op_anchor = endpoint_anchor(Some(&service.name), endpoint);
writeln!(writer, " * [{}](#{op_anchor})", op_title)?;
}
}
}
writeln!(writer)?;
}
let mut schema_ctx = SchemaContext::new(doc, config.inline_schemas);
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();
sort_endpoints(&mut sorted_endpoints, &config.sort_method);
for endpoint in sorted_endpoints {
let anchor = endpoint_anchor(Some(&service.name), endpoint);
write_endpoint(writer, endpoint, config, Some(&anchor), &mut schema_ctx)?;
}
} else {
writeln!(writer, "\nNo endpoints found for this service.\n")?;
}
}
render_schema_definitions(writer, &mut schema_ctx)?;
Ok(())
}
pub(super) fn generate_by_method<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
write_preamble(writer, doc, config)?;
let mut method_endpoints: HashMap<&str, Vec<&Endpoint>> = HashMap::new();
for endpoint in &doc.endpoints {
if !passes_filters(endpoint, config) || !passes_service_filter(endpoint, config) {
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)
&& !endpoints.is_empty()
{
let anchor = clean_for_id(method);
writeln!(writer, "- [{}](#{anchor})", method)?;
}
}
writeln!(writer)?;
}
let mut schema_ctx = SchemaContext::new(doc, config.inline_schemas);
for method in [
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE",
] {
if let Some(endpoints) = method_endpoints.get(method)
&& !endpoints.is_empty()
{
let anchor = clean_for_id(method);
writeln!(writer, "## {} {{#{}}}", method, anchor)?;
let mut sorted_endpoints = endpoints.clone();
sort_endpoints(&mut sorted_endpoints, &config.sort_method);
for endpoint in sorted_endpoints {
let anchor = endpoint_anchor(None, endpoint);
write_endpoint(writer, endpoint, config, Some(&anchor), &mut schema_ctx)?;
}
}
}
render_schema_definitions(writer, &mut schema_ctx)?;
Ok(())
}
pub(super) fn generate_by_path<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
write_preamble(writer, doc, config)?;
let mut path_endpoints: IndexMap<&str, Vec<&Endpoint>> = IndexMap::new();
for endpoint in &doc.endpoints {
if !passes_filters(endpoint, config) || !passes_service_filter(endpoint, config) {
continue;
}
path_endpoints
.entry(endpoint.path.as_str())
.or_default()
.push(endpoint);
}
if config.include_toc {
writeln!(writer, "## Paths\n")?;
for (path, endpoints) in &path_endpoints {
let anchor = clean_for_id(path);
writeln!(writer, "- [{}](#{anchor})", path)?;
let mut sorted_ops = endpoints.clone();
sort_endpoints(&mut sorted_ops, &config.sort_method);
for endpoint in sorted_ops {
let op_title = get_short_title(endpoint);
let op_anchor = endpoint_anchor(None, endpoint);
writeln!(writer, " * [{}](#{op_anchor})", op_title)?;
}
}
writeln!(writer)?;
}
let mut schema_ctx = SchemaContext::new(doc, config.inline_schemas);
for (path, endpoints) in &path_endpoints {
let anchor = clean_for_id(path);
writeln!(writer, "## {} {{#{}}}", path, anchor)?;
let mut sorted_endpoints = endpoints.clone();
sort_endpoints(&mut sorted_endpoints, &config.sort_method);
for endpoint in sorted_endpoints {
let anchor = endpoint_anchor(None, endpoint);
write_endpoint(writer, endpoint, config, Some(&anchor), &mut schema_ctx)?;
}
}
render_schema_definitions(writer, &mut schema_ctx)?;
Ok(())
}
pub(super) fn generate_flat<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
write_preamble(writer, doc, config)?;
let endpoints = visible_endpoints(doc, config);
writeln!(writer, "## Endpoints\n")?;
let mut schema_ctx = SchemaContext::new(doc, config.inline_schemas);
for endpoint in endpoints {
let anchor = endpoint_anchor(None, endpoint);
write_endpoint(writer, endpoint, config, Some(&anchor), &mut schema_ctx)?;
}
render_schema_definitions(writer, &mut schema_ctx)?;
Ok(())
}