use crate::utoipa::config::{OpenApiConfig, ParamConfig, ParamLocation, ResponseConfig};
use syn::*;
pub fn extract_doc_comments(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
let mut lines = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc")
&& let Ok(meta) = attr.meta.require_name_value()
&& let Expr::Lit(ExprLit {
lit: Lit::Str(lit_str),
..
}) = &meta.value
{
let line = lit_str.value().trim().to_string();
if !line.is_empty() {
lines.push(line);
}
}
}
if lines.is_empty() {
return (None, None);
}
let summary = lines.first().cloned();
let description = if lines.len() > 1 {
Some(lines[1..].join("\n"))
} else {
None
};
(summary, description)
}
pub fn infer_params_from_fn_args(
inputs: &punctuated::Punctuated<FnArg, token::Comma>,
) -> (
Vec<ParamConfig>,
Option<crate::utoipa::config::RequestBodyConfig>,
) {
let mut params = Vec::new();
let mut request_body = None;
for arg in inputs {
if let FnArg::Typed(pat_type) = arg {
if has_attr(&pat_type.attrs, "dep") || has_attr(&pat_type.attrs, "config") {
continue;
}
if has_attr(&pat_type.attrs, "body") {
let ty = (*pat_type.ty).clone();
let description = extract_desc_from_attrs(&pat_type.attrs);
let mut content_type = "application/json".to_string();
use crate::toolkit::attr::StrAttrMap;
for attr in &pat_type.attrs {
if attr.path().is_ident("body")
&& let Meta::List(list) = &attr.meta
&& let Ok(sam) = syn::parse2::<StrAttrMap>(list.tokens.clone())
&& sam.map.contains_key("str")
{
content_type = "text/plain".to_string();
}
}
request_body = Some(crate::utoipa::config::RequestBodyConfig {
ty,
description,
required: true,
content_type,
});
continue;
}
let (extractor_info, inner_type) = analyze_extractor_type(&pat_type.ty);
if let Type::Path(type_path) = &*pat_type.ty
&& let Some(last_segment) = type_path.path.segments.last()
{
let type_name = last_segment.ident.to_string();
if matches!(type_name.as_str(), "Json" | "Form") {
let description = extract_desc_from_attrs(&pat_type.attrs);
let content_type = if type_name.as_str() == "Form" {
"application/x-www-form-urlencoded".to_string()
} else {
"application/json".to_string()
};
request_body = Some(crate::utoipa::config::RequestBodyConfig {
ty: inner_type.unwrap_or_else(|| (*pat_type.ty).clone()),
description,
required: true,
content_type,
});
continue;
} else if matches!(type_name.as_str(), "Multipart" | "MultipartResult") {
let description = extract_desc_from_attrs(&pat_type.attrs);
request_body = Some(crate::utoipa::config::RequestBodyConfig {
ty: parse_quote!(::miko::serde_json::Value),
description,
required: true,
content_type: "multipart/form-data".to_string(),
});
continue;
}
}
let location = determine_param_location(&pat_type.attrs).or(extractor_info);
if let Some(loc) = location {
if let Some(param_name) = extract_extractor_ident(&pat_type.pat) {
let description = extract_desc_from_attrs(&pat_type.attrs);
let base_type = inner_type.unwrap_or_else(|| (*pat_type.ty).clone());
let is_optional = is_option_type(&base_type);
params.push(ParamConfig {
name: param_name,
ty: base_type, location: loc,
description,
required: !is_optional, deprecated: false,
example: None,
});
}
}
}
}
if request_body.is_none()
&& let Some(last_arg) = inputs.iter().rev().find(|arg| {
let FnArg::Typed(pat_type) = arg else {
return false;
};
!has_attr(&pat_type.attrs, "dep") && !has_attr(&pat_type.attrs, "config")
})
&& let FnArg::Typed(pat_type) = last_arg
&& pat_type.attrs.is_empty()
{
let ty = (*pat_type.ty).clone();
if is_string_type(&ty) {
request_body = Some(crate::utoipa::config::RequestBodyConfig {
ty,
description: None,
required: true,
content_type: "text/plain".to_string(),
});
}
}
(params, request_body)
}
fn analyze_extractor_type(ty: &Type) -> (Option<ParamLocation>, Option<Type>) {
if let Type::Path(type_path) = ty
&& let Some(last_segment) = type_path.path.segments.last()
{
let extractor_name = last_segment.ident.to_string();
let location = match extractor_name.as_str() {
"Path" => Some(ParamLocation::Path),
"Query" => Some(ParamLocation::Query),
"Json" | "Form" => None, "State" | "Extension" | "Extensions" | "Method" | "Uri" => None, _ => return (None, None), };
let inner_type = if let PathArguments::AngleBracketed(args) = &last_segment.arguments {
args.args.first().and_then(|arg| {
if let GenericArgument::Type(inner) = arg {
Some(inner.clone())
} else {
None
}
})
} else {
None
};
if matches!(extractor_name.as_str(), "Json" | "Form") {
return (None, inner_type);
}
return (location, inner_type);
}
(None, None)
}
fn has_attr(attrs: &[Attribute], name: &str) -> bool {
attrs.iter().any(|attr| attr.path().is_ident(name))
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty
&& let Some(last_segment) = type_path.path.segments.last()
{
return last_segment.ident == "Option";
}
false
}
fn is_string_type(ty: &Type) -> bool {
match ty {
Type::Path(path) => {
path.path
.segments
.last()
.map(|s| s.ident == "String")
.unwrap_or(false)
|| path.path.is_ident("String")
}
_ => false,
}
}
fn determine_param_location(attrs: &[Attribute]) -> Option<ParamLocation> {
for attr in attrs {
if attr.path().is_ident("path") {
return Some(ParamLocation::Path);
} else if attr.path().is_ident("query") {
return Some(ParamLocation::Query);
} else if attr.path().is_ident("header") {
return Some(ParamLocation::Header);
}
}
None
}
fn extract_desc_from_attrs(attrs: &[Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("desc")
&& let Ok(lit) = attr.parse_args::<LitStr>()
{
return Some(lit.value());
}
}
None
}
#[allow(unused_variables)]
pub fn infer_response_from_return_type(_output: &ReturnType) -> Option<ResponseConfig> {
None
}
#[allow(dead_code)]
fn extract_response_body_type(ty: &Type) -> Option<Type> {
if let Type::Path(type_path) = ty {
let last_segment = type_path.path.segments.last()?;
match last_segment.ident.to_string().as_str() {
"Result" => {
if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(GenericArgument::Type(ok_type)) = args.args.first()
{
return extract_response_body_type(ok_type);
}
}
"Json" => {
if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(GenericArgument::Type(inner_type)) = args.args.first()
{
return Some(inner_type.clone());
}
}
"Response" => {
if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(GenericArgument::Type(inner_type)) = args.args.first()
{
return Some(inner_type.clone());
}
}
_ => {}
}
}
None
}
#[allow(dead_code)]
pub fn extract_path_params(path: &str) -> Vec<String> {
let mut params = Vec::new();
let mut in_brace = false;
let mut current_param = String::new();
for ch in path.chars() {
match ch {
'{' => {
in_brace = true;
current_param.clear();
}
'}' => {
if in_brace && !current_param.is_empty() {
params.push(current_param.clone());
}
in_brace = false;
}
_ => {
if in_brace {
current_param.push(ch);
}
}
}
}
params
}
#[allow(dead_code)]
pub fn infer_path_from_fn_name(fn_name: &str) -> String {
let name = fn_name
.trim_start_matches("get_")
.trim_start_matches("post_")
.trim_start_matches("put_")
.trim_start_matches("delete_")
.trim_start_matches("patch_");
let path = name.replace('_', "/");
format!("/{}", path)
}
pub fn infer_openapi_config(
fn_attrs: &[Attribute],
fn_inputs: &punctuated::Punctuated<FnArg, token::Comma>,
fn_output: &ReturnType,
) -> OpenApiConfig {
let mut config = OpenApiConfig::new();
let (summary, description) = extract_doc_comments(fn_attrs);
config.auto_summary = summary;
config.auto_description = description;
let (params, request_body) = infer_params_from_fn_args(fn_inputs);
config.auto_params = params;
config.auto_request_body = request_body;
config.auto_response = infer_response_from_return_type(fn_output);
config
}
pub fn extract_extractor_ident(pat: &Pat) -> Option<String> {
match pat {
Pat::Ident(p) => Some(p.ident.to_string()),
Pat::TupleStruct(p) if p.elems.len() == 1 => {
if let Pat::Ident(inner_ident) = &p.elems[0] {
Some(inner_ident.ident.to_string())
} else {
None
}
}
_ => None,
}
}