use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, GenericArgument, ItemFn, PathArguments, Type};
fn extractor_inner_type(ty: &Type) -> Option<(String, &Type)> {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
let name = segment.ident.to_string();
if matches!(name.as_str(), "Json" | "ValidatedJson" | "Path" | "Query") {
if let PathArguments::AngleBracketed(ref args) = segment.arguments {
if let Some(GenericArgument::Type(inner)) = args.args.first() {
return Some((name, inner));
}
}
}
}
}
None
}
struct RouteArgs {
path: syn::LitStr,
registry: Option<syn::Path>,
}
impl syn::parse::Parse for RouteArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let path: syn::LitStr = input.parse()?;
let mut registry = None;
if input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
input.parse::<syn::Token![=]>()?;
if ident == "registry" {
registry = Some(input.parse()?);
} else {
let _: syn::Expr = input.parse()?;
}
if input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
} else {
break;
}
}
}
Ok(RouteArgs { path, registry })
}
}
fn route_macro(args: TokenStream, input: TokenStream, method: &str) -> TokenStream {
let route_args = parse_macro_input!(args as RouteArgs);
let path_lit = &route_args.path;
let func = parse_macro_input!(input as ItemFn);
let fn_name = &func.sig.ident;
let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
let method_ident = syn::Ident::new(method, fn_name.span());
let vis = &func.vis;
let attrs = &func.attrs;
let sig = &func.sig;
let block = &func.block;
let state_ty = func
.sig
.inputs
.iter()
.find_map(|input| {
if let syn::FnArg::Typed(pat_type) = input {
if let Type::Path(type_path) = &*pat_type.ty {
if let Some(segment) = type_path.path.segments.last() {
if segment.ident == "State" {
if let PathArguments::AngleBracketed(ref args) = segment.arguments {
if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
return Some(quote! { #inner_ty });
}
}
}
}
}
}
None
})
.unwrap_or_else(|| quote! { () });
let req_schema = func.sig.inputs.iter().find_map(|input| {
if let syn::FnArg::Typed(pat_type) = input {
if let Some((name, inner)) = extractor_inner_type(&pat_type.ty) {
if name == "Json" || name == "ValidatedJson" {
return Some(
quote! { .with_request_schema(::ukiapi::schema_for::<#inner>()) },
);
}
}
}
None
});
let query_schema = func.sig.inputs.iter().find_map(|input| {
if let syn::FnArg::Typed(pat_type) = input {
if let Some((name, inner)) = extractor_inner_type(&pat_type.ty) {
if name == "Query" {
return Some(quote! { .with_query_schema(::ukiapi::schema_for::<#inner>()) });
}
}
}
None
});
let res_schema = match &func.sig.output {
syn::ReturnType::Type(_, ret_ty) => {
if let Some((name, inner)) = extractor_inner_type(ret_ty.as_ref()) {
if name == "Json" {
Some(quote! { .with_response_schema(::ukiapi::schema_for::<#inner>()) })
} else {
None
}
} else {
None
}
}
_ => None,
};
let registry_submit = if let Some(reg) = &route_args.registry {
if state_ty.to_string() == "()" {
quote! { ::ukiapi::submit_route!(#route_fn_name, #reg, stateless); }
} else {
quote! { ::ukiapi::submit_route!(#route_fn_name, #reg, stateful); }
}
} else if state_ty.to_string() == "()" {
quote! { ::ukiapi::submit_route!(#route_fn_name); }
} else {
quote! {}
};
let expanded = quote! {
#(#attrs)*
#vis #sig #block
#[doc(hidden)]
pub fn #route_fn_name() -> ::ukiapi::Route<#state_ty> {
::ukiapi::Route::#method_ident(#path_lit, #fn_name)
#req_schema
#res_schema
#query_schema
}
#registry_submit
};
expanded.into()
}
#[proc_macro_attribute]
pub fn model(_args: TokenStream, input: TokenStream) -> TokenStream {
let item: proc_macro2::TokenStream = input.into();
let expanded = quote! {
#[derive(::ukiapi::Serialize, ::ukiapi::Deserialize, ::ukiapi::JsonSchema, ::validator::Validate, Clone, ::ukiapi::ts_rs::TS)]
#[ts(export, crate = "::ukiapi::ts_rs")]
#item
};
expanded.into()
}
#[proc_macro_attribute]
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
route_macro(args, input, "get")
}
#[proc_macro_attribute]
pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
route_macro(args, input, "post")
}
#[proc_macro_attribute]
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
route_macro(args, input, "put")
}
#[proc_macro_attribute]
pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
route_macro(args, input, "delete")
}
#[proc_macro_attribute]
pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
route_macro(args, input, "patch")
}
#[proc_macro_attribute]
pub fn websocket(args: TokenStream, input: TokenStream) -> TokenStream {
route_macro(args, input, "websocket")
}
#[proc_macro_attribute]
pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
let func = parse_macro_input!(input as ItemFn);
let block = &func.block;
let sig = &func.sig;
let attrs = &func.attrs;
let vis = &func.vis;
let expanded = quote! {
#[tokio::main]
#(#attrs)*
#vis #sig {
if std::env::var("UKIAPI_HOST").is_err() {
std::env::set_var("UKIAPI_HOST", "127.0.0.1");
}
if std::env::var("UKIAPI_PORT").is_err() {
std::env::set_var("UKIAPI_PORT", "3000");
}
env_logger::init();
#block
}
};
expanded.into()
}