use crate::app::extract_app_meta;
use crate::server_attrs::{has_server_hidden, has_server_skip, validate_server_attrs};
use heck::ToTitleCase;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use server_less_parse::{
MethodInfo, ParamInfo, extract_groups, extract_methods, get_impl_name, resolve_method_group,
};
use syn::{ItemImpl, Token, parse::Parse};
#[derive(Default)]
pub(crate) struct MarkdownArgs {
pub title: Option<String>,
pub types: bool,
}
impl Parse for MarkdownArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut args = MarkdownArgs {
title: None,
types: true,
};
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
match ident.to_string().as_str() {
"name" | "title" => {
input.parse::<Token![=]>()?;
let lit: syn::LitStr = input.parse()?;
args.title = Some(lit.value());
}
"types" => {
input.parse::<Token![=]>()?;
let lit: syn::LitBool = input.parse()?;
args.types = lit.value();
}
other => {
const VALID: &[&str] = &["title", "types"];
let suggestion = crate::did_you_mean(other, VALID)
.map(|s| format!(" — did you mean `{s}`?"))
.unwrap_or_default();
return Err(syn::Error::new(
ident.span(),
format!(
"unknown argument `{other}`{suggestion}. Valid arguments: name, types"
),
));
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(args)
}
}
pub(crate) fn expand_markdown(
args: MarkdownArgs,
mut impl_block: ItemImpl,
) -> syn::Result<TokenStream2> {
crate::reject_generic_impl(&impl_block)?;
let app_meta = extract_app_meta(&mut impl_block.attrs);
let struct_name = get_impl_name(&impl_block)?;
let generics_clone = impl_block.generics.clone();
let (impl_generics, _ty_generics, where_clause) = generics_clone.split_for_impl();
let self_ty = impl_block.self_ty.clone();
let struct_name_str = struct_name.to_string();
let all_methods = extract_methods(&impl_block)?;
for m in &all_methods {
validate_server_attrs(m)?;
}
let methods: Vec<_> = all_methods
.into_iter()
.filter(|m| !has_server_skip(m) && !has_server_hidden(m))
.collect();
let title = args
.title
.or(app_meta.name.map(|n| format!("{} API", n)))
.unwrap_or_else(|| format!("{} API", struct_name_str));
let show_types = args.types;
let group_registry = extract_groups(&impl_block)?;
let groups = partition_by_group(&methods, &group_registry)?;
let mut methods_section = String::new();
for (group_name, group_methods) in &groups {
if let Some(name) = group_name {
methods_section.push_str(&format!("## {}\n\n", name));
} else {
methods_section.push_str("## Methods\n\n");
}
let docs: Vec<String> = group_methods
.iter()
.map(|m| generate_method_doc(m, show_types))
.collect();
methods_section.push_str(&docs.join("\n---\n\n"));
}
let markdown = format!(
"# {}\n\n{}\n\n{}",
title,
generate_overview(&struct_name_str, &methods, &groups),
methods_section,
);
let mut clean_impl = impl_block;
clean_impl
.attrs
.retain(|attr| !attr.path().is_ident("server"));
Ok(quote! {
#clean_impl
impl #impl_generics #self_ty #where_clause {
pub fn markdown_docs() -> &'static str {
#markdown
}
pub fn write_markdown(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::fs::write(path, Self::markdown_docs())
}
}
})
}
fn partition_by_group<'a>(
methods: &'a [MethodInfo],
registry: &Option<server_less_parse::GroupRegistry>,
) -> syn::Result<Vec<(Option<String>, Vec<&'a MethodInfo>)>> {
let mut groups: Vec<(Option<String>, Vec<&MethodInfo>)> = Vec::new();
for method in methods {
let group = resolve_method_group(method, registry)?;
if let Some(pos) = groups.iter().position(|(g, _)| *g == group) {
groups[pos].1.push(method);
} else {
groups.push((group, vec![method]));
}
}
if let Some(pos) = groups.iter().position(|(g, _)| g.is_none())
&& pos != 0
{
let ungrouped = groups.remove(pos);
groups.insert(0, ungrouped);
}
Ok(groups)
}
fn generate_overview(
name: &str,
methods: &[MethodInfo],
groups: &[(Option<String>, Vec<&MethodInfo>)],
) -> String {
let method_count = methods.len();
let has_async = methods.iter().any(|m| m.is_async);
let mut overview = format!(
"**{}** provides {} method{}.",
name,
method_count,
if method_count == 1 { "" } else { "s" }
);
if has_async {
overview.push_str(" Some methods are async.");
}
let named_groups: Vec<&str> = groups
.iter()
.filter_map(|(g, _)| g.as_deref())
.collect();
if !named_groups.is_empty() {
overview.push_str(&format!(
" Organized into {} group{}: {}.",
named_groups.len(),
if named_groups.len() == 1 { "" } else { "s" },
named_groups.join(", "),
));
}
overview
}
fn generate_method_doc(method: &MethodInfo, show_types: bool) -> String {
let name = method.name_str();
let title = name.replace('_', " ").to_title_case();
let mut doc = format!("### {}\n\n", title);
if let Some(desc) = &method.docs {
doc.push_str(desc);
doc.push_str("\n\n");
}
if method.is_async {
doc.push_str("*async*\n\n");
}
doc.push_str("```\n");
doc.push_str(&name);
doc.push('(');
let params: Vec<String> = method
.params
.iter()
.map(|p| format_param(p, show_types))
.collect();
doc.push_str(¶ms.join(", "));
doc.push(')');
if let Some(ty) = &method.return_info.ty
&& show_types
&& !method.return_info.is_unit
{
let type_str = quote::quote!(#ty).to_string();
doc.push_str(&format!(" -> {}", simplify_type(&type_str)));
}
doc.push_str("\n```\n\n");
if !method.params.is_empty() {
doc.push_str("**Parameters:**\n\n");
for param in &method.params {
doc.push_str(&format!(
"- `{}`: {}{}\n",
param.name,
if show_types {
let ty = ¶m.ty;
let ty = quote::quote!(#ty).to_string();
format!("*{}* ", simplify_type(&ty))
} else {
String::new()
},
if param.is_optional { "(optional)" } else { "" }
));
}
doc.push('\n');
}
if !method.return_info.is_unit {
doc.push_str("**Returns:** ");
if let Some(ty) = &method.return_info.ty {
let type_str = quote::quote!(#ty).to_string();
doc.push_str(&describe_return_type(&type_str, &method.return_info));
}
doc.push_str("\n\n");
}
doc
}
fn format_param(param: &ParamInfo, show_types: bool) -> String {
let name = param.name_str();
if show_types {
let ty = ¶m.ty;
let ty_str = quote::quote!(#ty).to_string();
format!("{}: {}", name, simplify_type(&ty_str))
} else {
name
}
}
fn simplify_type(ty: &str) -> String {
ty.replace(" < ", "<")
.replace(" > ", ">")
.replace(" , ", ", ")
.replace("& str", "&str")
.replace(":: ", "::")
}
fn describe_return_type(ty: &str, info: &server_less_parse::ReturnInfo) -> String {
if info.is_result {
"Result (success or error)".to_string()
} else if info.is_option {
"Optional value (may be null)".to_string()
} else if ty.contains("Vec") {
"Array of values".to_string()
} else if ty.contains("String") || ty.contains("str") {
"String".to_string()
} else if ty.contains("bool") {
"Boolean".to_string()
} else if ty.contains("i32") || ty.contains("i64") || ty.contains("u32") || ty.contains("u64") {
"Integer".to_string()
} else if ty.contains("f32") || ty.contains("f64") {
"Number".to_string()
} else {
simplify_type(ty)
}
}