mod endpoint;
mod examples;
mod schema;
pub(crate) mod schema_selection;
#[cfg_attr(not(feature = "cli"), allow(dead_code))]
pub(crate) mod split;
mod views;
use std::io::Write;
use anyhow::Result;
use indexmap::IndexMap;
use crate::models::{ApiDocumentation, DetailLevel, DocConfig, GroupBy};
pub(crate) use views::{removing_filters, service_is_visible, visible_endpoints};
pub(crate) use schema::response_schema;
pub(crate) use schema::SchemaUse;
#[cfg(feature = "cli")]
pub(crate) use schema::{
definition_section_tokens, inline_expansion_tokens, short_schema_reference,
};
#[derive(Debug, Clone, Default)]
pub(crate) struct ViewRender {
pub omissions: Vec<String>,
#[cfg_attr(not(feature = "cli"), allow(dead_code))]
pub schema_uses: IndexMap<String, SchemaUse>,
}
pub fn generate_markdown<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<()> {
generate_markdown_with_notices(writer, doc, config, &mut |_| {})
}
pub fn generate_markdown_with_notices<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
notice: &mut dyn FnMut(&str),
) -> Result<()> {
crate::selection::validate(doc, config)?;
match config.max_tokens {
Some(budget) => generate_within_budget(writer, doc, config, budget, notice),
None => {
let rendered = render(writer, doc, config)?;
for omission in &rendered.omissions {
notice(omission);
}
Ok(())
}
}
}
pub(crate) fn render<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
) -> Result<ViewRender> {
if schema_selection::active(config) {
let omissions = schema_selection::render(writer, doc, config)?;
return Ok(ViewRender {
omissions,
schema_uses: IndexMap::new(),
});
}
if config.detail_level == DetailLevel::Summary {
views::generate_summary(writer, doc, config)?;
Ok(ViewRender::default())
} else {
match config.group_by {
GroupBy::Service => views::generate_by_service(writer, doc, config),
GroupBy::Method => views::generate_by_method(writer, doc, config),
GroupBy::Path => views::generate_by_path(writer, doc, config),
GroupBy::Flat => views::generate_flat(writer, doc, config),
}
}
}
const DETAIL_LADDER: [DetailLevel; 4] = [
DetailLevel::Full,
DetailLevel::Standard,
DetailLevel::Basic,
DetailLevel::Summary,
];
fn generate_within_budget<W: Write>(
writer: &mut W,
doc: &ApiDocumentation,
config: &DocConfig,
budget: usize,
notice: &mut dyn FnMut(&str),
) -> Result<()> {
if schema_selection::active(config) {
let mut buffer = Vec::new();
let rendered = render(&mut buffer, doc, config)?;
let tokens = estimate_tokens(&buffer);
if tokens > budget {
let budget_notice = format!(
"> Selected schemas exceed the approximate {budget}-token budget (~{tokens} tokens before this notice). Selection and metadata are preserved; narrow --schema-field or explicitly set --schema-depth.\n\n"
);
notice(&format!(
"vimanam: selected schemas exceed approximate {budget}-token budget (~{tokens} tokens); preserving requested selection and metadata"
));
writer.write_all(budget_notice.as_bytes())?;
}
writer.write_all(&buffer)?;
for omission in &rendered.omissions {
notice(omission);
}
return Ok(());
}
let start = DETAIL_LADDER
.iter()
.position(|level| *level == config.detail_level)
.unwrap_or(0);
let mut chosen: Option<(DetailLevel, Vec<u8>, usize, ViewRender)> = None;
for level in &DETAIL_LADDER[start..] {
let mut trial_config = config.clone();
trial_config.detail_level = level.clone();
let mut buffer = Vec::new();
let rendered = render(&mut buffer, doc, &trial_config)?;
let tokens = estimate_tokens(&buffer);
let fits = tokens <= budget;
chosen = Some((level.clone(), buffer, tokens, rendered));
if fits {
break;
}
}
let (level, buffer, tokens, rendered) = chosen.expect("at least one detail level is rendered");
if level != config.detail_level {
notice(&format!(
"vimanam: output exceeded the {budget}-token budget at --detail {}; \
reduced to --detail {} (~{tokens} tokens).",
detail_level_name(&config.detail_level),
detail_level_name(&level),
));
} else if tokens > budget {
notice(&format!(
"vimanam: output is ~{tokens} tokens, over the {budget}-token budget; \
already at the lowest detail level (--detail {}).",
detail_level_name(&level),
));
}
writer.write_all(&buffer)?;
for omission in &rendered.omissions {
notice(omission);
}
Ok(())
}
pub fn estimate_tokens(rendered: &[u8]) -> usize {
String::from_utf8_lossy(rendered)
.chars()
.count()
.div_ceil(4)
}
pub(crate) fn detail_level_name(level: &DetailLevel) -> &'static str {
match level {
DetailLevel::Summary => "summary",
DetailLevel::Basic => "basic",
DetailLevel::Standard => "standard",
DetailLevel::Full => "full",
}
}