use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::punctuated::Punctuated;
use syn::{
Attribute, Expr, FnArg, ImplItem, ItemImpl, LitStr, Meta, Path, ReturnType, Token, Type,
parse_macro_input,
};
use nest_rs_codegen::{
forwarded_arg_idents, impl_self_ident, injected_method_with_layers, layer_inject_keys,
nth_generic_type,
};
use crate::attr::{expr_str, opt_str, take_flag_attr, take_use_attr};
type RouteHandler = (
syn::Ident,
syn::Ident,
Vec<Path>,
Vec<Path>,
Vec<Path>,
Option<Type>,
Vec<Expr>,
bool,
bool,
Vec<Path>,
Vec<Path>,
Vec<Path>,
);
type RoutesByPath = Vec<(LitStr, Vec<RouteHandler>)>;
pub(crate) fn routes(_args: TokenStream, input: TokenStream) -> TokenStream {
let mut item = parse_macro_input!(input as ItemImpl);
let self_ty = item.self_ty.clone();
let ctrl_name = match impl_self_ident(&self_ty, "routes") {
Ok(name) => name,
Err(err) => return err.to_compile_error().into(),
};
let ctrl_tag = LitStr::new(&ctrl_name.to_string(), ctrl_name.span());
let mut wrappers: Vec<TokenStream2> = Vec::new();
let mut routes_by_path: RoutesByPath = Vec::new();
let mut route_metas: Vec<TokenStream2> = Vec::new();
for impl_item in item.items.iter_mut() {
let ImplItem::Fn(method) = impl_item else {
continue;
};
let verb_idx = method.attrs.iter().position(|attr| {
["get", "post", "put", "delete", "patch"]
.iter()
.any(|v| attr.path().is_ident(v))
});
let Some(idx) = verb_idx else { continue };
let attr = method.attrs.remove(idx);
let verb_ident = attr
.path()
.get_ident()
.expect("verb attribute has an ident")
.clone();
let route_path: LitStr = match attr.parse_args() {
Ok(p) => p,
Err(err) => return err.to_compile_error().into(),
};
let method_name = method.sig.ident.clone();
let method_name_lit = method_name.to_string();
let wrapper_name = format_ident!("__nestrs_route_{}", method_name);
let inputs: Vec<FnArg> = method.sig.inputs.iter().skip(1).cloned().collect();
let arg_idents = match forwarded_arg_idents(&method.sig) {
Ok(idents) => idents,
Err(err) => return err.to_compile_error().into(),
};
let return_type = match &method.sig.output {
ReturnType::Default => quote! { () },
ReturnType::Type(_, ty) => quote! { #ty },
};
let extra_inputs = if inputs.is_empty() {
quote! {}
} else {
quote! { , #(#inputs),* }
};
let guards = match take_use_attr(&mut method.attrs, "use_guards") {
Ok(paths) => paths,
Err(err) => return err.to_compile_error().into(),
};
let force_guards = match take_use_attr(&mut method.attrs, "force_guards") {
Ok(paths) => paths,
Err(err) => return err.to_compile_error().into(),
};
let filters = match take_use_attr(&mut method.attrs, "use_filters") {
Ok(paths) => paths,
Err(err) => return err.to_compile_error().into(),
};
let interceptors = match take_use_attr(&mut method.attrs, "use_interceptors") {
Ok(paths) => paths,
Err(err) => return err.to_compile_error().into(),
};
let method_pipes = match take_use_attr(&mut method.attrs, "use_pipes") {
Ok(paths) => paths,
Err(err) => return err.to_compile_error().into(),
};
let method_exception_filters =
match take_use_attr(&mut method.attrs, "use_exception_filters") {
Ok(paths) => paths,
Err(err) => return err.to_compile_error().into(),
};
let is_public = take_flag_attr(&mut method.attrs, "public");
let no_pipes = take_flag_attr(&mut method.attrs, "no_pipes");
let response_shapers = match crate::response::take_response_shapers(
&mut method.attrs,
&method.block,
) {
Ok(d) => d,
Err(err) => return err.to_compile_error().into(),
};
let call_expr = quote! { __ctrl.#method_name(#(#arg_idents),*).await };
let returns_result = match &method.sig.output {
ReturnType::Type(_, ty) => result_inner(ty).is_some(),
ReturnType::Default => false,
};
let (wrapper_return_type, wrapper_body) = if response_shapers.is_empty() {
(return_type.clone(), call_expr)
} else {
let mut wrapper_args: Vec<syn::Ident> = Vec::with_capacity(arg_idents.len() + 1);
wrapper_args.push(syn::Ident::new("__ctrl", proc_macro2::Span::call_site()));
wrapper_args.extend(arg_idents.iter().cloned());
let body = crate::response::apply_response_shapers(
&response_shapers,
call_expr,
&wrapper_args,
returns_result,
);
(quote! { ::poem::Result<::poem::Response> }, body)
};
wrappers.push(quote! {
#[::poem::handler]
async fn #wrapper_name(
::poem::web::Data(__ctrl): ::poem::web::Data<&::std::sync::Arc<#self_ty>>
#extra_inputs
) -> #wrapper_return_type {
#wrapper_body
}
});
let mut metas: Vec<Expr> = Vec::new();
while let Some(m_idx) = method.attrs.iter().position(|a| a.path().is_ident("meta")) {
let m_attr = method.attrs.remove(m_idx);
match m_attr.parse_args::<Expr>() {
Ok(expr) => metas.push(expr),
Err(err) => return err.to_compile_error().into(),
}
}
let shaper = shaper_type(&inputs);
let handler = (
verb_ident.clone(),
wrapper_name.clone(),
guards,
filters,
interceptors,
shaper.clone(),
metas,
is_public,
no_pipes,
force_guards,
method_pipes,
method_exception_filters,
);
match routes_by_path
.iter_mut()
.find(|(path, _)| path.value() == route_path.value())
{
Some((_, handlers)) => handlers.push(handler),
None => routes_by_path.push((route_path.clone(), vec![handler])),
}
let verb_variant = match verb_ident.to_string().as_str() {
"get" => quote!(::nest_rs_http::HttpVerb::Get),
"post" => quote!(::nest_rs_http::HttpVerb::Post),
"put" => quote!(::nest_rs_http::HttpVerb::Put),
"delete" => quote!(::nest_rs_http::HttpVerb::Delete),
"patch" => quote!(::nest_rs_http::HttpVerb::Patch),
_ => unreachable!("verb_ident filtered above"),
};
let api = match method.attrs.iter().position(|a| a.path().is_ident("api")) {
Some(a_idx) => {
let a_attr = method.attrs.remove(a_idx);
match parse_api_attr(&a_attr) {
Ok(api) => api,
Err(err) => return err.to_compile_error().into(),
}
}
None => ApiMeta::default(),
};
let summary = opt_str(&api.summary);
let description = opt_str(&api.description);
let tags = if api.tags.is_empty() {
quote! { &[#ctrl_tag] }
} else {
let tags = &api.tags;
quote! { &[#(#tags),*] }
};
let request_body = match request_payload(&inputs) {
Some(ty) => quote! {
::core::option::Option::Some(::nest_rs_http::schema_of::<#ty> as ::nest_rs_http::SchemaFn)
},
None => quote! { ::core::option::Option::None },
};
let response = match (shaper.is_some(), response_payload(&method.sig.output)) {
(false, Some(ty)) => quote! {
::core::option::Option::Some(::nest_rs_http::schema_of::<#ty> as ::nest_rs_http::SchemaFn)
},
_ => quote! { ::core::option::Option::None },
};
route_metas.push(quote! {
::nest_rs_http::HttpRouteMeta {
verb: #verb_variant,
path: #route_path,
handler: #method_name_lit,
summary: #summary,
description: #description,
tags: #tags,
request_body: #request_body,
response: #response,
}
});
}
let route_layer_keys = layer_inject_keys(
routes_by_path
.iter()
.flat_map(|(_, handlers)| handlers.iter())
.flat_map(
|(
_,
_,
guards,
filters,
interceptors,
_,
_,
_,
_,
force_guards,
pipes,
exception_filters,
)| {
guards
.iter()
.chain(filters)
.chain(interceptors)
.chain(force_guards)
.chain(pipes)
.chain(exception_filters)
},
),
);
let injected_method = injected_method_with_layers(&self_ty, &route_layer_keys);
let route_entries: Vec<TokenStream2> = routes_by_path
.iter()
.map(|(path, handlers)| {
let mut iter = handlers.iter();
let first = iter.next().expect("each path has at least one verb");
let first_label = format!("{} {}", first.0, path.value());
let first_ep = guarded_handler(first, &first_label, &self_ty);
let first_verb = &first.0;
let mut method = quote! { ::poem::#first_verb(#first_ep) };
for handler in iter {
let label = format!("{} {}", handler.0, path.value());
let ep = guarded_handler(handler, &label, &self_ty);
let verb = &handler.0;
method = quote! { #method.#verb(#ep) };
}
quote! { .at(#path, #method) }
})
.collect();
quote! {
#item
#(#wrappers)*
impl ::nest_rs_http::Controller for #self_ty {
fn mount(
container: &::nest_rs_core::Container,
route: ::poem::Route,
) -> ::poem::Route {
use ::poem::EndpointExt;
let __ctrl = ::std::sync::Arc::new(<#self_ty>::from_container(container));
let __sub = ::poem::Route::new()
#(#route_entries)*
.data(__ctrl);
let __sub = <#self_ty>::__nestrs_controller_layers(container, __sub);
let __prefix = ::nest_rs_http::version_path(<#self_ty>::VERSION, <#self_ty>::PATH);
route.nest(__prefix.as_str(), __sub)
}
}
impl ::nest_rs_core::Discoverable for #self_ty {
#injected_method
fn register(
builder: ::nest_rs_core::ContainerBuilder,
) -> ::nest_rs_core::ContainerBuilder {
let __meta = ::nest_rs_http::HttpControllerMeta::new(
<#self_ty>::PATH,
<#self_ty>::VERSION,
::std::vec![#(#route_metas),*],
|__c, __r| <#self_ty as ::nest_rs_http::Controller>::mount(__c, __r),
);
builder.attach_meta::<#self_ty, ::nest_rs_http::HttpControllerMeta>(__meta)
}
}
}
.into()
}
fn shaper_type(inputs: &[FnArg]) -> Option<Type> {
inputs.iter().find_map(|arg| {
let FnArg::Typed(pt) = arg else { return None };
let Type::Path(tp) = pt.ty.as_ref() else {
return None;
};
let last = tp.path.segments.last()?;
match last.ident == "Authorize"
&& matches!(last.arguments, syn::PathArguments::AngleBracketed(_))
{
true => Some((*pt.ty).clone()),
false => None,
}
})
}
fn guarded_handler(handler: &RouteHandler, route_label: &str, self_ty: &Type) -> TokenStream2 {
let (
_verb,
wrapper,
guards,
filters,
interceptors,
shaper,
metas,
is_public,
no_pipes,
force_guards,
method_pipes,
method_exception_filters,
) = handler;
let mut expr = match shaper {
Some(ty) => quote! {
::nest_rs_http::shaped(#wrapper, ::core::marker::PhantomData::<#ty>)
},
None => quote! { #wrapper },
};
expr = wrap_interceptors(expr, interceptors);
expr = wrap_filters(expr, filters);
let route_label_lit = LitStr::new(route_label, proc_macro2::Span::call_site());
let method_guard_specs = guard_specs(guards);
let force_guard_typeids = force_guard_typeids(force_guards);
let method_pipe_specs = pipe_specs(method_pipes);
let method_exception_filter_specs = exception_filter_specs(method_exception_filters);
let no_pipes_flag = if *no_pipes {
quote!(true)
} else {
quote!(false)
};
expr = quote! {
::nest_rs_interceptors::InterceptorExt::interceptor(
#expr,
::std::sync::Arc::new(
::nest_rs_guards::RouteShaper::new(
#route_label_lit,
<#self_ty>::__nestrs_controller_guard_specs(),
#method_guard_specs,
#force_guard_typeids,
<#self_ty>::__nestrs_controller_pipe_specs(),
#method_pipe_specs,
#no_pipes_flag,
<#self_ty>::__nestrs_controller_exception_filter_specs(),
#method_exception_filter_specs,
)
),
)
};
for m in metas {
expr = quote! { ::poem::EndpointExt::data(#expr, #m) };
}
if *is_public {
expr = quote! {
::poem::EndpointExt::data(#expr, ::nest_rs_core::Public)
};
}
expr
}
fn guard_specs(paths: &[Path]) -> TokenStream2 {
if paths.is_empty() {
return quote! { ::std::vec![] };
}
let entries = paths.iter().map(|p| {
quote! {
::nest_rs_guards::dispatch::ScopedLayerSpec {
type_id: ::core::any::TypeId::of::<#p>(),
name: ::core::any::type_name::<#p>(),
resolve: |__c| ::nest_rs_core::Container::get::<#p>(__c)
.map(|__arc| __arc as ::std::sync::Arc<dyn ::nest_rs_guards::Guard>),
}
}
});
quote! { ::std::vec![#(#entries),*] }
}
fn pipe_specs(paths: &[Path]) -> TokenStream2 {
if paths.is_empty() {
return quote! { ::std::vec![] };
}
let entries = paths.iter().map(|p| {
quote! {
::nest_rs_guards::dispatch::ScopedLayerSpec {
type_id: ::core::any::TypeId::of::<#p>(),
name: ::core::any::type_name::<#p>(),
resolve: |__c| ::nest_rs_core::Container::get::<#p>(__c)
.map(|__arc| __arc as ::std::sync::Arc<dyn ::nest_rs_pipes::GlobalPipe>),
}
}
});
quote! { ::std::vec![#(#entries),*] }
}
fn exception_filter_specs(paths: &[Path]) -> TokenStream2 {
if paths.is_empty() {
return quote! { ::std::vec![] };
}
let entries = paths.iter().map(|p| {
quote! {
::nest_rs_guards::dispatch::ScopedLayerSpec {
type_id: ::core::any::TypeId::of::<#p>(),
name: ::core::any::type_name::<#p>(),
resolve: |__c| ::nest_rs_core::Container::get::<#p>(__c)
.map(|__arc| __arc as ::std::sync::Arc<dyn ::nest_rs_exception_filters::ExceptionFilterErased>),
}
}
});
quote! { ::std::vec![#(#entries),*] }
}
fn force_guard_typeids(paths: &[Path]) -> TokenStream2 {
if paths.is_empty() {
return quote! { ::std::vec![] };
}
let entries = paths.iter().map(|p| {
quote! { ::core::any::TypeId::of::<#p>() }
});
quote! { ::std::vec![#(#entries),*] }
}
fn wrap_interceptors(mut expr: TokenStream2, paths: &[Path]) -> TokenStream2 {
for p in paths.iter().rev() {
expr = quote! {
{
let __ep = ::poem::EndpointExt::boxed(
::poem::EndpointExt::map_to_response(#expr),
);
let __type_id = ::core::any::TypeId::of::<#p>();
let __is_global = ::nest_rs_core::Container::get::<
::nest_rs_interceptors::InterceptorSpecs,
>(container)
.is_some_and(|__specs| __specs.0.iter().any(|__s| __s.type_id == __type_id));
if __is_global {
::tracing::warn!(
target: "nest_rs::layers",
layer = ::core::any::type_name::<#p>(),
scope = "method",
"interceptor declared at multiple scopes — broadest (global) wins, this scope skipped",
);
__ep
} else {
::poem::EndpointExt::boxed(::poem::EndpointExt::map_to_response(
::nest_rs_interceptors::InterceptorExt::interceptor(
__ep,
::nest_rs_core::Container::get::<#p>(container).expect(concat!(
"#[use_interceptors] interceptor `",
stringify!(#p),
"` is not registered — add it to a module's providers"
)),
),
))
}
}
};
}
expr
}
fn wrap_filters(mut expr: TokenStream2, paths: &[Path]) -> TokenStream2 {
for p in paths.iter().rev() {
expr = quote! {
{
let __ep = ::poem::EndpointExt::boxed(
::poem::EndpointExt::map_to_response(#expr),
);
let __type_id = ::core::any::TypeId::of::<#p>();
let __is_global = ::nest_rs_core::Container::get::<
::nest_rs_filters::FilterSpecs,
>(container)
.is_some_and(|__specs| __specs.0.iter().any(|__s| __s.type_id == __type_id));
if __is_global {
::tracing::warn!(
target: "nest_rs::layers",
layer = ::core::any::type_name::<#p>(),
scope = "method",
"filter declared at multiple scopes — broadest (global) wins, this scope skipped",
);
__ep
} else {
::poem::EndpointExt::boxed(::poem::EndpointExt::map_to_response(
::nest_rs_filters::FilterExt::filter(
__ep,
::nest_rs_core::Container::get::<#p>(container).expect(concat!(
"#[use_filters] filter `",
stringify!(#p),
"` is not registered — add it to a module's providers"
)),
),
))
}
}
};
}
expr
}
#[derive(Default)]
struct ApiMeta {
summary: Option<LitStr>,
description: Option<LitStr>,
tags: Vec<LitStr>,
}
fn parse_api_attr(attr: &Attribute) -> syn::Result<ApiMeta> {
let mut out = ApiMeta::default();
let metas = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
for meta in metas {
match meta {
Meta::NameValue(nv) if nv.path.is_ident("summary") => {
out.summary = Some(expr_str(&nv.value)?);
}
Meta::NameValue(nv) if nv.path.is_ident("description") => {
out.description = Some(expr_str(&nv.value)?);
}
Meta::List(list) if list.path.is_ident("tags") => {
out.tags = list
.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)?
.into_iter()
.collect();
}
other => {
return Err(syn::Error::new_spanned(
other,
"#[api] accepts `summary = \"...\"`, `description = \"...\"`, and \
`tags(\"a\", \"b\")`",
));
}
}
}
Ok(out)
}
fn json_payload(ty: &Type) -> Option<Type> {
if let Some(t) = nth_generic_type(ty, "Json", 0) {
return Some(t.clone());
}
if let Some(inner) = nth_generic_type(ty, "Valid", 0) {
return json_payload(inner);
}
if let Some(inner) = nth_generic_type(ty, "Piped", 1) {
return json_payload(inner);
}
None
}
fn request_payload(inputs: &[FnArg]) -> Option<Type> {
inputs.iter().find_map(|arg| match arg {
FnArg::Typed(pt) => json_payload(&pt.ty),
_ => None,
})
}
pub(crate) fn result_inner(ty: &Type) -> Option<&Type> {
nth_generic_type(ty, "Result", 0)
}
fn response_payload(output: &ReturnType) -> Option<Type> {
let ReturnType::Type(_, ty) = output else {
return None;
};
let inner = result_inner(ty).unwrap_or(ty);
nth_generic_type(inner, "Json", 0).cloned()
}