#![allow(
clippy::doc_markdown,
clippy::option_if_let_else,
clippy::single_match_else,
clippy::use_self
)]
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Attribute, Expr, ExprLit, Ident, Lit, LitBool, LitStr, Token};
pub use autumn_macros_support::schema::{
last_segment_name, primitive_json_type, unwrap_single_generic,
};
#[allow(clippy::struct_excessive_bools)]
#[derive(Default)]
pub struct ApiDocAttr {
pub summary: Option<LitStr>,
pub description: Option<LitStr>,
pub tags: Vec<LitStr>,
pub operation_id: Option<LitStr>,
pub status: Option<u16>,
pub hidden: bool,
pub mcp_tool: bool,
pub mcp_exclude: bool,
pub mcp_stream: bool,
pub mcp_task: bool,
pub mcp_core: bool,
pub webmcp_tool: bool,
pub webmcp_exclude: bool,
}
enum KeyValue {
Summary(LitStr),
Description(LitStr),
Tag(LitStr),
Tags(Vec<LitStr>),
OperationId(LitStr),
Status(u16),
Hidden,
Mcp(bool),
Stream,
Task,
Core,
WebMcp(bool),
}
impl Parse for KeyValue {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == "hidden" {
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
let value: LitBool = input.parse()?;
return Ok(if value.value {
KeyValue::Hidden
} else {
KeyValue::Tags(Vec::new())
});
}
return Ok(KeyValue::Hidden);
}
if key_str == "mcp" {
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
let value: LitBool = input.parse()?;
return Ok(KeyValue::Mcp(value.value));
}
return Ok(KeyValue::Mcp(true));
}
if key_str == "webmcp" {
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
let value: LitBool = input.parse()?;
return Ok(KeyValue::WebMcp(value.value));
}
return Ok(KeyValue::WebMcp(true));
}
if key_str == "stream" {
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
let value: LitBool = input.parse()?;
return Ok(if value.value {
KeyValue::Stream
} else {
KeyValue::Tags(Vec::new())
});
}
return Ok(KeyValue::Stream);
}
if key_str == "task" {
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
let value: LitBool = input.parse()?;
return Ok(if value.value {
KeyValue::Task
} else {
KeyValue::Tags(Vec::new())
});
}
return Ok(KeyValue::Task);
}
if key_str == "core" {
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
let value: LitBool = input.parse()?;
return Ok(if value.value {
KeyValue::Core
} else {
KeyValue::Tags(Vec::new())
});
}
return Ok(KeyValue::Core);
}
let _eq: Token![=] = input.parse()?;
match key_str.as_str() {
"summary" => Ok(KeyValue::Summary(input.parse()?)),
"description" => Ok(KeyValue::Description(input.parse()?)),
"tag" => Ok(KeyValue::Tag(input.parse()?)),
"tags" => {
let content;
syn::bracketed!(content in input);
let items =
syn::punctuated::Punctuated::<LitStr, Token![,]>::parse_terminated(&content)?;
Ok(KeyValue::Tags(items.into_iter().collect()))
}
"operation_id" => Ok(KeyValue::OperationId(input.parse()?)),
"status" => {
let value: Expr = input.parse()?;
let n = expect_u16(&value)?;
Ok(KeyValue::Status(n))
}
other => Err(syn::Error::new(
key.span(),
format!(
"unknown key `{other}` in `#[api_doc(...)]`. \
Supported keys: summary, description, tag, tags, operation_id, status, hidden, mcp, stream, task, core, webmcp."
),
)),
}
}
}
fn expect_u16(expr: &Expr) -> syn::Result<u16> {
if let Expr::Lit(ExprLit {
lit: Lit::Int(int), ..
}) = expr
{
int.base10_parse::<u16>()
} else {
Err(syn::Error::new_spanned(
expr,
"expected an integer HTTP status code (e.g. `status = 201`)",
))
}
}
impl ApiDocAttr {
fn merge(&mut self, kv: KeyValue) {
match kv {
KeyValue::Summary(v) => self.summary = Some(v),
KeyValue::Description(v) => self.description = Some(v),
KeyValue::Tag(v) => self.tags = vec![v],
KeyValue::Tags(v) if !v.is_empty() => self.tags = v,
KeyValue::Tags(_) => {}
KeyValue::OperationId(v) => self.operation_id = Some(v),
KeyValue::Status(n) => self.status = Some(n),
KeyValue::Hidden => self.hidden = true,
KeyValue::Mcp(true) => self.mcp_tool = true,
KeyValue::Mcp(false) => self.mcp_exclude = true,
KeyValue::Stream => self.mcp_stream = true,
KeyValue::Task => self.mcp_task = true,
KeyValue::Core => self.mcp_core = true,
KeyValue::WebMcp(true) => self.webmcp_tool = true,
KeyValue::WebMcp(false) => self.webmcp_exclude = true,
}
}
}
impl Parse for ApiDocAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let items = syn::punctuated::Punctuated::<KeyValue, Token![,]>::parse_terminated(input)?;
let mut out = ApiDocAttr::default();
for kv in items {
out.merge(kv);
}
Ok(out)
}
}
pub fn extract(attrs: &mut Vec<Attribute>) -> Result<ApiDocAttr, TokenStream> {
let mut collected = ApiDocAttr::default();
let mut error: Option<TokenStream> = None;
attrs.retain(|attr| {
if !attr.path().is_ident("api_doc") {
return true;
}
let parsed: syn::Result<ApiDocAttr> = match &attr.meta {
syn::Meta::Path(_) => Ok(ApiDocAttr::default()),
syn::Meta::List(list) => syn::parse2(list.tokens.clone()),
syn::Meta::NameValue(nv) => Err(syn::Error::new_spanned(
nv,
"expected `#[api_doc(...)]`, not `#[api_doc = ...]`",
)),
};
match parsed {
Ok(parsed) => {
collected.absorb(parsed);
}
Err(err) => {
if error.is_none() {
error = Some(err.to_compile_error());
}
}
}
false
});
if let Some(err) = error {
return Err(err);
}
Ok(collected)
}
impl ApiDocAttr {
fn absorb(&mut self, other: ApiDocAttr) {
if other.summary.is_some() {
self.summary = other.summary;
}
if other.description.is_some() {
self.description = other.description;
}
if !other.tags.is_empty() {
self.tags = other.tags;
}
if other.operation_id.is_some() {
self.operation_id = other.operation_id;
}
if other.status.is_some() {
self.status = other.status;
}
if other.hidden {
self.hidden = true;
}
if other.mcp_tool {
self.mcp_tool = true;
}
if other.mcp_exclude {
self.mcp_exclude = true;
}
if other.mcp_stream {
self.mcp_stream = true;
}
if other.mcp_task {
self.mcp_task = true;
}
if other.mcp_core {
self.mcp_core = true;
}
if other.webmcp_tool {
self.webmcp_tool = true;
}
if other.webmcp_exclude {
self.webmcp_exclude = true;
}
}
pub fn emit_ident_fields(&self, default_operation_id: &Ident) -> TokenStream {
let summary = option_str(self.summary.as_ref());
let description = option_str(self.description.as_ref());
let tags = slice_str(&self.tags);
let op_id = if let Some(id) = &self.operation_id {
quote! { #id }
} else {
quote! { ::core::stringify!(#default_operation_id) }
};
let status = self.status.unwrap_or(200);
let hidden = self.hidden;
let mcp_tool = self.mcp_tool;
let mcp_exclude = self.mcp_exclude;
let mcp_stream = self.mcp_stream;
let mcp_task = self.mcp_task;
let mcp_core = self.mcp_core;
let webmcp_tool = self.webmcp_tool;
let webmcp_exclude = self.webmcp_exclude;
quote! {
operation_id: #op_id,
summary: #summary,
description: #description,
tags: #tags,
success_status: #status,
hidden: #hidden,
mcp_tool: #mcp_tool,
mcp_exclude: #mcp_exclude,
mcp_stream: #mcp_stream,
mcp_task: #mcp_task,
mcp_core: #mcp_core,
webmcp_tool: #webmcp_tool,
webmcp_exclude: #webmcp_exclude,
}
}
}
fn option_str(lit: Option<&LitStr>) -> TokenStream {
match lit {
Some(v) => quote! { ::core::option::Option::Some(#v) },
None => quote! { ::core::option::Option::None },
}
}
fn slice_str(items: &[LitStr]) -> TokenStream {
if items.is_empty() {
quote! { &[] }
} else {
let literals: Vec<_> = items.iter().map(|s| quote! { #s }).collect();
quote! { &[#(#literals),*] }
}
}
pub fn extract_path_params(path: &str) -> Vec<String> {
let mut out = Vec::new();
let mut remaining = path;
while let Some(start) = remaining.find('{') {
let after_brace = &remaining[start + 1..];
if let Some(rest) = after_brace.strip_prefix('{') {
remaining = rest;
continue;
}
let Some(end_rel) = after_brace.find('}') else {
break;
};
let inner = &after_brace[..end_rel];
let name = inner.split(':').next().unwrap_or(inner).trim();
if !name.is_empty() {
out.push(name.to_owned());
}
remaining = &after_brace[end_rel + 1..];
}
out
}
pub fn emit_path_param_slice(params: &[String]) -> TokenStream {
if params.is_empty() {
quote! { &[] }
} else {
let literals: Vec<_> = params
.iter()
.map(|p| LitStr::new(p, Span::call_site()))
.collect();
quote! { &[#(#literals),*] }
}
}
pub fn infer_request_body(input_fn: &syn::ItemFn) -> Option<TokenStream> {
for arg in &input_fn.sig.inputs {
let syn::FnArg::Typed(pat) = arg else {
continue;
};
if let Some(inner) = unwrap_json_body(&pat.ty) {
return Some(schema_entry_for_type(&inner));
}
}
None
}
pub fn unwrap_json_body(ty: &syn::Type) -> Option<syn::Type> {
if let Some(inner) = unwrap_single_generic(ty, "Json") {
return Some(inner);
}
if let Some(inner) = unwrap_single_generic(ty, "Valid")
&& let Some(payload) = unwrap_single_generic(&inner, "Json")
{
return Some(payload);
}
None
}
pub fn infer_response_body(input_fn: &syn::ItemFn) -> Option<TokenStream> {
let markerless_gate_budget = ["__AutumnStepUpGate_", "__AutumnThrottleGate_"]
.into_iter()
.filter(|prefix| crate::param_helpers::has_guard_gate_param_with_prefix(input_fn, prefix))
.count();
let ty = recover_guarded_return_type(&input_fn.block, markerless_gate_budget)
.or_else(|| sig_output_type(input_fn))?;
let ty = unwrap_result_ok(&ty).unwrap_or(ty);
find_json_in_type(&ty).map(|inner| schema_entry_for_type(&inner))
}
pub fn sig_output_type(input_fn: &syn::ItemFn) -> Option<syn::Type> {
let syn::ReturnType::Type(_, ty) = &input_fn.sig.output else {
return None;
};
Some((**ty).clone())
}
fn recover_guarded_return_type(
block: &syn::Block,
markerless_gate_budget: usize,
) -> Option<syn::Type> {
let (ty, init_expr, remaining_budget) =
crate::idempotency_guard::generated_inner_response_binding(block, markerless_gate_budget)?;
let nested = crate::idempotency_guard::expr_nested_async_body(init_expr)
.and_then(|block| recover_guarded_return_type(block, remaining_budget));
Some(nested.unwrap_or_else(|| ty.clone()))
}
pub fn find_json_in_type(ty: &syn::Type) -> Option<syn::Type> {
if let Some(inner) = unwrap_single_generic(ty, "Json") {
return Some(inner);
}
if let syn::Type::Tuple(tup) = ty {
for elem in &tup.elems {
if let Some(inner) = unwrap_single_generic(elem, "Json") {
return Some(inner);
}
}
}
None
}
pub fn unwrap_result_ok(ty: &syn::Type) -> Option<syn::Type> {
let path = match ty {
syn::Type::Path(p) => &p.path,
_ => return None,
};
let last = path.segments.last()?;
let name = last.ident.to_string();
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
match name.as_str() {
"Result" => args.args.iter().find_map(|arg| match arg {
syn::GenericArgument::Type(t) => Some(t.clone()),
_ => None,
}),
"AutumnResult" => args.args.iter().find_map(|arg| match arg {
syn::GenericArgument::Type(t) => Some(t.clone()),
_ => None,
}),
_ => None,
}
}
fn schema_entry_for_type(ty: &syn::Type) -> TokenStream {
let wrapper_name = LitStr::new(
&last_segment_name(ty).unwrap_or_else(|| "Schema".to_owned()),
Span::call_site(),
);
if let Some(inner) = unwrap_single_generic(ty, "Vec") {
let inner_tokens = schema_entry_for_type(&inner);
return quote! {
::autumn_web::openapi::SchemaEntry {
name: #wrapper_name,
kind: ::autumn_web::openapi::SchemaKind::Array(&#inner_tokens),
identity: ::core::option::Option::Some(
::autumn_web::openapi::type_name_of::<#ty>
),
}
};
}
if let Some(inner) = unwrap_single_generic(ty, "Option") {
let inner_tokens = schema_entry_for_type(&inner);
return quote! {
::autumn_web::openapi::SchemaEntry {
name: #wrapper_name,
kind: ::autumn_web::openapi::SchemaKind::Nullable(&#inner_tokens),
identity: ::core::option::Option::Some(
::autumn_web::openapi::type_name_of::<#ty>
),
}
};
}
let name = last_segment_name(ty).unwrap_or_else(|| "Schema".to_owned());
let name_lit = LitStr::new(&name, Span::call_site());
if let Some(json_type) = primitive_json_type(&name) {
let json_lit = LitStr::new(json_type, Span::call_site());
quote! {
::autumn_web::openapi::SchemaEntry {
name: #name_lit,
kind: ::autumn_web::openapi::SchemaKind::Primitive(#json_lit),
identity: ::core::option::Option::None,
}
}
} else {
quote! {
::autumn_web::openapi::SchemaEntry {
name: #name_lit,
kind: ::autumn_web::openapi::SchemaKind::Ref,
identity: ::core::option::Option::Some(
::autumn_web::openapi::type_name_of::<#ty>
),
}
}
}
}
pub fn infer_query_params(input_fn: &syn::ItemFn) -> Option<TokenStream> {
for arg in &input_fn.sig.inputs {
let syn::FnArg::Typed(pat) = arg else {
continue;
};
if let Some(inner) = unwrap_single_generic(&pat.ty, "Query") {
return Some(schema_entry_for_type(&inner));
}
}
None
}
pub fn has_policy_check_in_stmts(stmts: &[syn::Stmt]) -> bool {
for stmt in stmts {
let s = quote::quote!(#stmt).to_string();
if s.contains("__check_policy") {
return true;
}
}
false
}
pub fn extract_secured_info(input_fn: &syn::ItemFn) -> (bool, TokenStream, TokenStream) {
for attr in &input_fn.attrs {
if attr.path().is_ident("secured")
|| attr
.path()
.segments
.last()
.is_some_and(|s| s.ident == "secured")
{
let roles = extract_secured_roles(attr);
let scopes = extract_secured_scopes(attr);
return (
true,
emit_static_str_slice(&roles),
emit_static_str_slice(&scopes),
);
}
}
if let Some(roles) = extract_secured_roles_marker(input_fn) {
let scopes = extract_secured_scopes_marker(input_fn).unwrap_or_default();
return (
true,
emit_static_str_slice(&roles),
emit_static_str_slice(&scopes),
);
}
for attr in &input_fn.attrs {
if attr.path().is_ident("authorize")
|| attr
.path()
.segments
.last()
.is_some_and(|s| s.ident == "authorize")
{
return (true, quote! { &[] }, quote! { &[] });
}
}
if has_policy_check_in_stmts(&input_fn.block.stmts) {
return (true, quote! { &[] }, quote! { &[] });
}
let has_session = input_fn.sig.inputs.iter().any(|param| {
if let syn::FnArg::Typed(pt) = param
&& let syn::Pat::Ident(pi) = pt.pat.as_ref()
{
return pi.ident == "__autumn_session";
}
false
});
if has_session {
return (true, quote! { &[] }, quote! { &[] });
}
(false, quote! { &[] }, quote! { &[] })
}
pub fn is_public(input_fn: &syn::ItemFn) -> bool {
for attr in &input_fn.attrs {
if attr.path().is_ident("public")
|| attr
.path()
.segments
.last()
.is_some_and(|s| s.ident == "public")
{
return true;
}
}
has_public_marker_in_stmts(&input_fn.block.stmts)
}
const AGENT_OPERABLE_MARKER: &str = "__AUTUMN_AGENT_OPERABLE";
pub fn extract_agent_authority(input_fn: &syn::ItemFn) -> bool {
let has_attr = input_fn.attrs.iter().any(|attr| {
attr.path().is_ident("agent_operable")
|| attr
.path()
.segments
.last()
.is_some_and(|s| s.ident == "agent_operable")
});
has_attr || crate::edge::stmts_have_marker(&input_fn.block.stmts, AGENT_OPERABLE_MARKER)
}
fn has_public_marker_in_stmts(stmts: &[syn::Stmt]) -> bool {
stmts.iter().any(has_public_marker_in_stmt)
}
fn has_public_marker_in_stmt(stmt: &syn::Stmt) -> bool {
match stmt {
syn::Stmt::Item(syn::Item::Const(item_const)) => item_const.ident == "__AUTUMN_PUBLIC",
syn::Stmt::Expr(expr, _) => has_public_marker_in_expr(expr),
syn::Stmt::Local(local) => local
.init
.as_ref()
.is_some_and(|init| has_public_marker_in_expr(&init.expr)),
_ => false,
}
}
fn has_public_marker_in_expr(expr: &syn::Expr) -> bool {
match expr {
syn::Expr::Block(block) => has_public_marker_in_stmts(&block.block.stmts),
syn::Expr::Unsafe(block) => has_public_marker_in_stmts(&block.block.stmts),
_ => crate::idempotency_guard::expr_nested_async_body(expr)
.is_some_and(|block| has_public_marker_in_stmts(&block.stmts)),
}
}
fn extract_secured_roles_marker(input_fn: &syn::ItemFn) -> Option<Vec<String>> {
extract_secured_roles_marker_from_stmts(&input_fn.block.stmts)
}
fn extract_secured_roles_marker_from_stmts(stmts: &[syn::Stmt]) -> Option<Vec<String>> {
stmts
.iter()
.find_map(extract_secured_roles_marker_from_stmt)
}
fn extract_secured_roles_marker_from_stmt(stmt: &syn::Stmt) -> Option<Vec<String>> {
match stmt {
syn::Stmt::Item(syn::Item::Const(item_const))
if item_const.ident == "__AUTUMN_SECURED_ROLES" =>
{
extract_roles_from_marker_expr(&item_const.expr)
}
syn::Stmt::Expr(expr, _) => extract_secured_roles_marker_from_expr(expr),
syn::Stmt::Local(local) => local
.init
.as_ref()
.and_then(|init| extract_secured_roles_marker_from_expr(&init.expr)),
_ => None,
}
}
fn extract_secured_roles_marker_from_expr(expr: &syn::Expr) -> Option<Vec<String>> {
match expr {
syn::Expr::Block(block) => extract_secured_roles_marker_from_stmts(&block.block.stmts),
syn::Expr::Unsafe(block) => extract_secured_roles_marker_from_stmts(&block.block.stmts),
_ => crate::idempotency_guard::expr_nested_async_body(expr)
.and_then(|block| extract_secured_roles_marker_from_stmts(&block.stmts)),
}
}
fn extract_roles_from_marker_expr(expr: &syn::Expr) -> Option<Vec<String>> {
let syn::Expr::Reference(reference) = expr else {
return None;
};
let syn::Expr::Array(array) = reference.expr.as_ref() else {
return None;
};
let mut roles = Vec::with_capacity(array.elems.len());
for elem in &array.elems {
let syn::Expr::Lit(lit) = elem else {
return None;
};
let syn::Lit::Str(role) = &lit.lit else {
return None;
};
roles.push(role.value());
}
Some(roles)
}
fn extract_secured_roles(attr: &syn::Attribute) -> Vec<String> {
use proc_macro2::TokenTree;
let syn::Meta::List(list) = &attr.meta else {
return Vec::new();
};
let mut roles = Vec::new();
let mut iter = list.tokens.clone().into_iter().peekable();
while let Some(TokenTree::Literal(lit)) = iter.peek() {
match syn::parse2::<syn::LitStr>(quote! { #lit }) {
Ok(s) => roles.push(s.value()),
Err(_) => break,
}
iter.next();
if let Some(TokenTree::Punct(p)) = iter.peek()
&& p.as_char() == ','
{
iter.next();
} else {
break;
}
}
roles
}
fn extract_secured_scopes(attr: &syn::Attribute) -> Vec<String> {
use proc_macro2::TokenTree;
let syn::Meta::List(list) = &attr.meta else {
return Vec::new();
};
let mut iter = list.tokens.clone().into_iter();
while let Some(tt) = iter.next() {
let TokenTree::Ident(ident) = tt else {
continue;
};
if ident != "scopes" {
continue;
}
let Some(TokenTree::Punct(p)) = iter.next() else {
continue;
};
if p.as_char() != '=' {
continue;
}
let Some(TokenTree::Group(group)) = iter.next() else {
continue;
};
let mut scopes = Vec::new();
for inner_tt in group.stream() {
if let TokenTree::Literal(lit) = inner_tt
&& let Ok(s) = syn::parse2::<syn::LitStr>(quote! { #lit })
{
scopes.push(s.value());
}
}
return scopes;
}
Vec::new()
}
fn extract_secured_scopes_marker(input_fn: &syn::ItemFn) -> Option<Vec<String>> {
extract_secured_scopes_marker_from_stmts(&input_fn.block.stmts)
}
fn extract_secured_scopes_marker_from_stmts(stmts: &[syn::Stmt]) -> Option<Vec<String>> {
stmts
.iter()
.find_map(extract_secured_scopes_marker_from_stmt)
}
fn extract_secured_scopes_marker_from_stmt(stmt: &syn::Stmt) -> Option<Vec<String>> {
match stmt {
syn::Stmt::Item(syn::Item::Const(item_const))
if item_const.ident == "__AUTUMN_SECURED_SCOPES" =>
{
extract_roles_from_marker_expr(&item_const.expr)
}
syn::Stmt::Expr(expr, _) => extract_secured_scopes_marker_from_expr(expr),
syn::Stmt::Local(local) => local
.init
.as_ref()
.and_then(|init| extract_secured_scopes_marker_from_expr(&init.expr)),
_ => None,
}
}
fn extract_secured_scopes_marker_from_expr(expr: &syn::Expr) -> Option<Vec<String>> {
match expr {
syn::Expr::Block(block) => extract_secured_scopes_marker_from_stmts(&block.block.stmts),
syn::Expr::Unsafe(block) => extract_secured_scopes_marker_from_stmts(&block.block.stmts),
_ => crate::idempotency_guard::expr_nested_async_body(expr)
.and_then(|block| extract_secured_scopes_marker_from_stmts(&block.stmts)),
}
}
const AUTHORIZE_BINDINGS_MARKER: &str = "__AUTUMN_AUTHORIZE_BINDINGS";
pub fn extract_authorize_bindings(input_fn: &syn::ItemFn) -> Vec<(String, String)> {
let mut bindings = Vec::new();
collect_authorize_markers_in_stmts(&input_fn.block.stmts, &mut bindings);
for attr in &input_fn.attrs {
if crate::authorize::attr_is_authorize_shaped(attr, input_fn) {
bindings.extend(authorize_binding_from_attr(attr));
}
}
bindings
}
fn authorize_binding_from_attr(attr: &syn::Attribute) -> Option<(String, String)> {
let syn::Meta::List(list) = &attr.meta else {
return None;
};
let args = crate::authorize::parse_with_leading_literal(list.tokens.clone()).ok()?;
Some((args.action?, args.resource?.to_string()))
}
fn collect_authorize_markers_in_stmts(stmts: &[syn::Stmt], out: &mut Vec<(String, String)>) {
for stmt in stmts {
match stmt {
syn::Stmt::Expr(expr, _) => collect_authorize_markers_in_expr(expr, out),
syn::Stmt::Local(local) => {
if let Some(init) = &local.init {
collect_authorize_markers_in_expr(&init.expr, out);
}
}
_ => {}
}
}
for stmt in stmts {
if let syn::Stmt::Item(syn::Item::Const(item_const)) = stmt
&& item_const.ident == AUTHORIZE_BINDINGS_MARKER
{
collect_authorize_bindings_from_marker_expr(&item_const.expr, out);
}
}
}
fn collect_authorize_markers_in_expr(expr: &syn::Expr, out: &mut Vec<(String, String)>) {
match expr {
syn::Expr::Block(block) => collect_authorize_markers_in_stmts(&block.block.stmts, out),
syn::Expr::Unsafe(block) => collect_authorize_markers_in_stmts(&block.block.stmts, out),
_ => {
if let Some(block) = crate::idempotency_guard::expr_nested_async_body(expr) {
collect_authorize_markers_in_stmts(&block.stmts, out);
}
}
}
}
fn collect_authorize_bindings_from_marker_expr(expr: &syn::Expr, out: &mut Vec<(String, String)>) {
let syn::Expr::Reference(reference) = expr else {
return;
};
let syn::Expr::Array(array) = reference.expr.as_ref() else {
return;
};
let mut decoded = Vec::with_capacity(array.elems.len());
for elem in &array.elems {
let syn::Expr::Tuple(tuple) = elem else {
return;
};
let [action, resource] = tuple.elems.iter().collect::<Vec<_>>()[..] else {
return;
};
let (Some(action), Some(resource)) =
(string_literal_value(action), string_literal_value(resource))
else {
return;
};
decoded.push((action, resource));
}
out.extend(decoded);
}
fn string_literal_value(expr: &syn::Expr) -> Option<String> {
match expr {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => Some(s.value()),
_ => None,
}
}
pub fn emit_authorize_binding_slice(bindings: &[(String, String)]) -> TokenStream {
if bindings.is_empty() {
return quote! { &[] };
}
let entries = bindings.iter().map(|(action, resource)| {
let action = LitStr::new(action, Span::call_site());
let resource = LitStr::new(resource, Span::call_site());
quote! {
::autumn_web::openapi::AuthorizeBinding { action: #action, resource: #resource }
}
});
quote! { &[#(#entries),*] }
}
fn emit_static_str_slice(items: &[String]) -> TokenStream {
if items.is_empty() {
quote! { &[] }
} else {
let lits: Vec<_> = items
.iter()
.map(|s| LitStr::new(s, Span::call_site()))
.collect();
quote! { &[#(#lits),*] }
}
}
pub fn schema_option(expr: Option<TokenStream>) -> TokenStream {
match expr {
Some(e) => quote! { ::core::option::Option::Some(#e) },
None => quote! { ::core::option::Option::None },
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_doc_core_flag_parses() {
let attr: ApiDocAttr = syn::parse_str("mcp, core").unwrap();
assert!(attr.mcp_tool && attr.mcp_core);
let attr: ApiDocAttr = syn::parse_str("mcp, core = true").unwrap();
assert!(attr.mcp_core);
let attr: ApiDocAttr = syn::parse_str("mcp, core = false").unwrap();
assert!(!attr.mcp_core);
}
#[test]
fn api_doc_core_alone_does_not_opt_in() {
let attr: ApiDocAttr = syn::parse_str("core").unwrap();
assert!(attr.mcp_core && !attr.mcp_tool);
}
#[test]
fn api_doc_core_survives_a_merge() {
let mut first: ApiDocAttr = syn::parse_str("mcp").unwrap();
let second: ApiDocAttr = syn::parse_str("core").unwrap();
first.absorb(second);
assert!(first.mcp_tool && first.mcp_core);
}
#[test]
fn api_doc_unknown_key_error_lists_core_and_webmcp() {
let err = syn::parse_str::<ApiDocAttr>("bogus = 1").err().unwrap();
assert!(err.to_string().contains("task, core, webmcp."), "{err}");
}
#[test]
fn api_doc_webmcp_flag_parses() {
let attr: ApiDocAttr = syn::parse_str("webmcp").unwrap();
assert!(attr.webmcp_tool && !attr.webmcp_exclude && !attr.mcp_tool);
let attr: ApiDocAttr = syn::parse_str("webmcp = true").unwrap();
assert!(attr.webmcp_tool);
let attr: ApiDocAttr = syn::parse_str("webmcp = false").unwrap();
assert!(attr.webmcp_exclude && !attr.webmcp_tool);
}
#[test]
fn api_doc_mcp_does_not_imply_webmcp() {
let attr: ApiDocAttr = syn::parse_str("mcp").unwrap();
assert!(attr.mcp_tool && !attr.webmcp_tool);
let attr: ApiDocAttr = syn::parse_str("mcp, webmcp").unwrap();
assert!(attr.mcp_tool && attr.webmcp_tool);
}
#[test]
fn api_doc_webmcp_survives_a_merge() {
let mut first: ApiDocAttr = syn::parse_str("mcp").unwrap();
first.absorb(syn::parse_str("webmcp").unwrap());
first.absorb(syn::parse_str("summary = \"x\"").unwrap());
assert!(first.webmcp_tool);
let mut other: ApiDocAttr = syn::parse_str("summary = \"x\"").unwrap();
other.absorb(syn::parse_str("webmcp = false").unwrap());
assert!(other.webmcp_exclude);
}
#[test]
fn api_doc_webmcp_fields_are_emitted() {
let attr: ApiDocAttr = syn::parse_str("webmcp").unwrap();
let ident = syn::Ident::new("handler", Span::call_site());
let out = attr.emit_ident_fields(&ident).to_string();
assert!(out.contains("webmcp_tool : true"), "{out}");
assert!(out.contains("webmcp_exclude : false"), "{out}");
}
#[test]
fn extract_path_params_handles_single() {
assert_eq!(extract_path_params("/users/{id}"), vec!["id".to_owned()]);
}
#[test]
fn extract_path_params_handles_multiple() {
assert_eq!(
extract_path_params("/posts/{year}/{slug}"),
vec!["year".to_owned(), "slug".to_owned()]
);
}
#[test]
fn extract_path_params_handles_regex_prefix() {
assert_eq!(
extract_path_params("/users/{id:[0-9]+}"),
vec!["id".to_owned()]
);
}
#[test]
fn extract_path_params_returns_empty_for_static() {
assert_eq!(extract_path_params("/hello"), [] as [String; 0]);
assert_eq!(extract_path_params("/"), [] as [String; 0]);
}
#[test]
fn extract_path_params_ignores_unclosed_braces() {
assert_eq!(extract_path_params("/oops/{broken"), [] as [String; 0]);
}
#[test]
fn extract_path_params_skips_escaped_braces() {
assert_eq!(extract_path_params("/{{hello}}"), [] as [String; 0]);
assert_eq!(
extract_path_params("/{{literal}}/{id}"),
vec!["id".to_owned()]
);
}
#[test]
fn secured_roles_marker_extracts_roles() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {
const __AUTUMN_SECURED_ROLES: &[&str] = &["admin", "editor"];
}
};
assert_eq!(
extract_secured_roles_marker(&input_fn),
Some(vec!["admin".to_owned(), "editor".to_owned()])
);
}
#[test]
fn secured_roles_marker_extracts_empty_roles() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {
const __AUTUMN_SECURED_ROLES: &[&str] = &[];
}
};
assert_eq!(extract_secured_roles_marker(&input_fn), Some(Vec::new()));
}
#[test]
fn secured_roles_marker_extracts_nested_roles() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {
{
const __AUTUMN_SECURED_ROLES: &[&str] = &["admin"];
}
}
};
assert_eq!(
extract_secured_roles_marker(&input_fn),
Some(vec!["admin".to_owned()])
);
}
#[test]
fn extract_secured_scopes_from_attribute_scopes_only() {
let input_fn: syn::ItemFn = syn::parse_quote! {
#[secured(scopes = ["posts:read", "posts:write"])]
async fn handler() {}
};
let attr = input_fn
.attrs
.iter()
.find(|a| a.path().is_ident("secured"))
.unwrap();
assert_eq!(
extract_secured_scopes(attr),
vec!["posts:read".to_owned(), "posts:write".to_owned()]
);
}
#[test]
fn extract_secured_scopes_from_attribute_roles_and_scopes() {
let input_fn: syn::ItemFn = syn::parse_quote! {
#[secured("admin", scopes = ["posts:write"])]
async fn handler() {}
};
let attr = input_fn
.attrs
.iter()
.find(|a| a.path().is_ident("secured"))
.unwrap();
assert_eq!(extract_secured_scopes(attr), vec!["posts:write".to_owned()]);
}
#[test]
fn extract_secured_scopes_returns_empty_for_roles_only() {
let input_fn: syn::ItemFn = syn::parse_quote! {
#[secured("admin")]
async fn handler() {}
};
let attr = input_fn
.attrs
.iter()
.find(|a| a.path().is_ident("secured"))
.unwrap();
assert_eq!(extract_secured_scopes(attr), [] as [String; 0]);
}
#[test]
fn scopes_marker_extracted_from_handler_body() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {
const __AUTUMN_SECURED_SCOPES: &[&str] = &["posts:write", "posts:delete"];
}
};
assert_eq!(
extract_secured_scopes_marker(&input_fn),
Some(vec!["posts:write".to_owned(), "posts:delete".to_owned()])
);
}
#[test]
fn scopes_marker_empty_array_returns_some_empty_vec() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {
const __AUTUMN_SECURED_SCOPES: &[&str] = &[];
}
};
assert_eq!(
extract_secured_scopes_marker(&input_fn),
Some(Vec::<String>::new())
);
}
#[test]
fn scopes_marker_absent_returns_none() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {}
};
assert_eq!(extract_secured_scopes_marker(&input_fn), None);
}
#[test]
fn is_public_detects_attribute() {
let input_fn: syn::ItemFn = syn::parse_quote! {
#[public]
async fn handler() {}
};
assert!(is_public(&input_fn));
}
#[test]
fn is_public_detects_body_marker() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {
const __AUTUMN_PUBLIC: () = ();
}
};
assert!(is_public(&input_fn));
}
#[test]
fn is_public_false_for_unannotated() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {}
};
assert!(!is_public(&input_fn));
}
#[test]
fn extract_authorize_bindings_from_attribute() {
let input_fn: syn::ItemFn = syn::parse_quote! {
#[authorize("update", resource = Note)]
async fn handler(note: Note) {}
};
assert_eq!(
extract_authorize_bindings(&input_fn),
vec![("update".to_owned(), "Note".to_owned())]
);
}
#[test]
fn extract_authorize_bindings_from_marker() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {
const __AUTUMN_AUTHORIZE_BINDINGS: &[(&str, &str)] = &[("update", "Note")];
}
};
assert_eq!(
extract_authorize_bindings(&input_fn),
vec![("update".to_owned(), "Note".to_owned())]
);
}
#[test]
fn extract_authorize_bindings_from_nested_marker() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
let __autumn_inner: ::autumn_web::reexports::axum::response::Response =
(async move {
const __AUTUMN_AUTHORIZE_BINDINGS: &[(&str, &str)] = &[("update", "Note")];
})
.await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
assert_eq!(
extract_authorize_bindings(&input_fn),
vec![("update".to_owned(), "Note".to_owned())]
);
}
#[test]
fn extract_authorize_bindings_is_empty_without_authorize() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {}
};
assert_eq!(
extract_authorize_bindings(&input_fn),
Vec::<(String, String)>::new()
);
}
#[test]
fn extract_authorize_bindings_handles_string_literal_resource() {
let input_fn: syn::ItemFn = syn::parse_quote! {
#[authorize("update", resource = "Note")]
async fn handler(note: Note) {}
};
assert_eq!(
extract_authorize_bindings(&input_fn),
vec![("update".to_owned(), "Note".to_owned())]
);
}
#[test]
fn extract_authorize_bindings_unions_attr_and_marker() {
let input_fn: syn::ItemFn = syn::parse_quote! {
#[authorize("publish", resource = Note)]
async fn handler(note: Note) {
const __AUTUMN_AUTHORIZE_BINDINGS: &[(&str, &str)] = &[("update", "Note")];
}
};
assert_eq!(
extract_authorize_bindings(&input_fn),
vec![
("update".to_owned(), "Note".to_owned()),
("publish".to_owned(), "Note".to_owned()),
]
);
}
#[test]
fn extract_authorize_bindings_orders_stacked_markers_by_source() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
const __AUTUMN_AUTHORIZE_BINDINGS: &[(&str, &str)] = &[("second", "Note")];
let __autumn_inner: ::autumn_web::reexports::axum::response::Response =
(async move {
const __AUTUMN_AUTHORIZE_BINDINGS: &[(&str, &str)] =
&[("first", "Note")];
})
.await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
assert_eq!(
extract_authorize_bindings(&input_fn),
vec![
("first".to_owned(), "Note".to_owned()),
("second".to_owned(), "Note".to_owned()),
]
);
}
#[test]
fn secured_markers_survive_live_authorize_attribute() {
let input_fn: syn::ItemFn = syn::parse_quote! {
#[authorize("update", resource = Note)]
async fn handler(note: Note) {
const __AUTUMN_SECURED_ROLES: &[&str] = &["admin"];
const __AUTUMN_SECURED_SCOPES: &[&str] = &["notes:write"];
}
};
let (secured, roles, scopes) = extract_secured_info(&input_fn);
assert!(secured);
assert!(
roles.to_string().contains("\"admin\""),
"roles from an expanded #[secured] must survive a live #[authorize] attribute: {roles}"
);
assert!(
scopes.to_string().contains("\"notes:write\""),
"scopes must survive alongside the roles: {scopes}"
);
}
#[test]
fn public_marker_survives_generated_wrapper() {
let public = crate::public::public_macro(
quote::quote! {},
quote::quote! { async fn h() -> &'static str { "ok" } },
);
let throttled =
crate::throttle::throttle_macro(quote::quote! { limit = 5, per = "1m" }, public);
let parsed = crate::param_helpers::extract_fn_item(throttled, "h");
assert!(
is_public(&parsed),
"the public marker must survive a #[throttle] wrapper"
);
}
#[test]
fn secured_roles_survive_authorize_wrapper() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
const __AUTUMN_AUTHORIZE_BINDINGS: &[(&str, &str)] = &[("update", "Note")];
let __autumn_inner: ::autumn_web::reexports::axum::response::Response =
(async move {
const __AUTUMN_SECURED_ROLES: &[&str] = &["admin"];
const __AUTUMN_SECURED_SCOPES: &[&str] = &["notes:write"];
})
.await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
let (secured, roles, scopes) = extract_secured_info(&input_fn);
assert!(secured);
assert!(
roles.to_string().contains("\"admin\""),
"roles buried under a generated wrapper must be recovered: {roles}"
);
assert!(
scopes.to_string().contains("\"notes:write\""),
"scopes buried under a generated wrapper must be recovered: {scopes}"
);
}
#[test]
fn extract_authorize_bindings_ignores_malformed_marker() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() {
const __AUTUMN_AUTHORIZE_BINDINGS: &[(&str, u32)] = &[("update", 1)];
}
};
assert_eq!(
extract_authorize_bindings(&input_fn),
Vec::<(String, String)>::new()
);
}
#[test]
fn infer_response_body_reads_sig_output_when_no_guard_marker_present() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::Json<Created> { todo!() }
};
let schema = infer_response_body(&input_fn)
.expect("a bare Json<T> return type must produce a response schema");
assert!(
schema.to_string().contains("\"Created\""),
"schema should name the Created type: {schema}"
);
}
#[test]
fn infer_response_body_recovers_type_under_single_guard_wrapper() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
const __AUTUMN_THROTTLE_ROUTE_ID: &str = "handler";
let __autumn_inner: ::autumn_web::reexports::axum::Json<Created> =
(async move { todo!() }).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
let schema = infer_response_body(&input_fn).expect(
"a Json<T> return type rewritten to Response by a single guard must still be \
recoverable from the __autumn_inner marker",
);
assert!(
schema.to_string().contains("\"Created\""),
"recovered schema should name the Created type: {schema}"
);
}
#[test]
fn infer_response_body_recovers_innermost_type_under_stacked_guards() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
const __AUTUMN_SECURED_ROLES: &[&str] = &[];
let __autumn_inner: ::autumn_web::reexports::axum::response::Response = (async move {
const __AUTUMN_THROTTLE_ROUTE_ID: &str = "handler";
let __autumn_inner: ::autumn_web::reexports::axum::Json<Created> =
(async move { todo!() }).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
})
.await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
let schema = infer_response_body(&input_fn).expect(
"the innermost __autumn_inner binding must be recovered even under a second, \
outer guard wrapper",
);
assert!(
schema.to_string().contains("\"Created\""),
"recovered schema should name the innermost Created type, not Response: {schema}"
);
}
#[test]
fn infer_response_body_none_when_guard_wraps_unit_return() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
const __AUTUMN_THROTTLE_ROUTE_ID: &str = "handler";
let __autumn_inner: () = (async move { todo!() }).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
assert!(infer_response_body(&input_fn).is_none());
}
#[test]
fn infer_response_body_recovers_result_wrapped_json_under_guard() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
const __AUTUMN_THROTTLE_ROUTE_ID: &str = "handler";
let __autumn_inner: AutumnResult<::autumn_web::reexports::axum::Json<Created>> =
(async move { todo!() }).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
let schema = infer_response_body(&input_fn)
.expect("Result-wrapped Json<T> recovered from the marker must still be inferred");
assert!(
schema.to_string().contains("\"Created\""),
"recovered schema should name the Created type: {schema}"
);
}
#[test]
fn infer_response_body_ignores_unrelated_local_named_autumn_inner() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::Json<Real> {
let __autumn_inner: ::autumn_web::reexports::axum::Json<Fake> =
(async move { compute() }).await;
do_something(__autumn_inner);
::autumn_web::reexports::axum::Json(Real::default())
}
};
let schema = infer_response_body(&input_fn)
.expect("the correctly-declared sig.output must still be inferred");
let rendered = schema.to_string();
assert!(
rendered.contains("\"Real\""),
"must recover the handler's real declared return type, not the incidental local: \
{rendered}"
);
assert!(
!rendered.contains("\"Fake\""),
"must not be fooled by an unrelated local that merely shares the marker's name: \
{rendered}"
);
}
#[test]
fn infer_response_body_ignores_position_and_tail_match_without_a_guard_marker() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::Json<Real> {
let __autumn_inner: ::autumn_web::reexports::axum::Json<Fake> =
(async move { compute() }).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
let schema = infer_response_body(&input_fn)
.expect("the correctly-declared sig.output must still be inferred");
let rendered = schema.to_string();
assert!(
rendered.contains("\"Real\""),
"must recover the handler's real declared return type: {rendered}"
);
assert!(
!rendered.contains("\"Fake\""),
"must not be fooled by a coincidental position+tail match with no guard marker: \
{rendered}"
);
}
#[test]
fn infer_response_body_ignores_a_nested_coincidental_match_with_no_marker_of_its_own() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
const __AUTUMN_THROTTLE_ROUTE_ID: &str = "handler";
let __autumn_inner: ::autumn_web::reexports::axum::Json<Real> = (async move {
let __autumn_inner: ::autumn_web::reexports::axum::Json<Fake> =
(async move { compute() }).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
})
.await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
let schema = infer_response_body(&input_fn)
.expect("the real guard's own outer binding must still be recovered");
let rendered = schema.to_string();
assert!(
rendered.contains("\"Real\""),
"must recover the real guard's own type, not the nested coincidental one: {rendered}"
);
assert!(
!rendered.contains("\"Fake\""),
"must not descend into a nested block that lacks its own guard marker: {rendered}"
);
}
#[test]
fn infer_response_body_ignores_a_nested_coincidental_match_under_a_markerless_gate() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler(
_: __AutumnThrottleGate_handler,
) -> ::autumn_web::reexports::axum::response::Response {
let __autumn_inner: ::autumn_web::reexports::axum::Json<Real> = (async move {
let __autumn_inner: ::autumn_web::reexports::axum::Json<Fake> =
(async move { compute() }).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
})
.await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
let schema = infer_response_body(&input_fn)
.expect("the real gate's own outer binding must still be recovered");
let rendered = schema.to_string();
assert!(
rendered.contains("\"Real\""),
"must recover the real gate's own type, not the nested coincidental one: {rendered}"
);
assert!(
!rendered.contains("\"Fake\""),
"must not spend the same gate parameter's budget twice on an unrelated nested \
coincidence: {rendered}"
);
}
#[test]
fn infer_response_body_recovers_innermost_type_under_triple_stacked_guards() {
let input_fn: syn::ItemFn = syn::parse_quote! {
async fn handler() -> ::autumn_web::reexports::axum::response::Response {
const __AUTUMN_SECURED_ROLES: &[&str] = &[];
let __autumn_inner: ::autumn_web::reexports::axum::response::Response = (async move {
const __AUTUMN_AUTHORIZE_BINDINGS: &[(&str, &str)] = &[];
let __autumn_inner: ::autumn_web::reexports::axum::response::Response = (async move {
const __AUTUMN_THROTTLE_ROUTE_ID: &str = "handler";
let __autumn_inner: ::autumn_web::reexports::axum::Json<Created> =
(async move { todo!() }).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
})
.await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
})
.await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
}
};
let schema = infer_response_body(&input_fn).expect(
"the innermost __autumn_inner binding must be recovered through three levels of \
guard wrapping",
);
assert!(
schema.to_string().contains("\"Created\""),
"recovered schema should name the innermost Created type: {schema}"
);
}
}