1mod codegen;
2mod expand;
3mod parse;
4mod route;
5mod service;
6mod template;
7
8use proc_macro::TokenStream;
9use proc_macro2::{Span, TokenStream as TokenStream2};
10use quote::quote;
11use syn::{Item, ItemMod, LitStr, parse_macro_input};
12
13use expand::expand_fn_params;
14use route::expand_route_from_fn;
15use service::ServiceArgs;
16
17#[proc_macro_attribute]
18pub fn service(args: TokenStream, input: TokenStream) -> TokenStream {
19 expand_service(args, input)
20 .unwrap_or_else(syn::Error::into_compile_error)
21 .into()
22}
23
24fn expand_service(args: TokenStream, input: TokenStream) -> syn::Result<TokenStream2> {
27 let args = syn::parse::<ServiceArgs>(args)?;
28 let ServiceArgs {
29 name,
30 prefix,
31 policy,
32 } = args;
33 let prefix = prefix.unwrap_or_else(|| LitStr::new("", Span::call_site()));
34
35 let mut module = syn::parse::<ItemMod>(input)?;
36 let apigate_path = apigate_crate_path()?;
37
38 let Some((_, items)) = module.content.as_mut() else {
39 return Err(syn::Error::new_spanned(
40 &module,
41 "#[apigate::service] requires an inline module body: `mod x { ... }`",
42 ));
43 };
44
45 let mut route_defs = Vec::new();
46 let mut generated_items = Vec::new();
47
48 for item in items.iter_mut() {
49 if let Item::Fn(f) = item {
50 if let Some(extracted) = expand_route_from_fn(&apigate_path, f)? {
51 route_defs.push(extracted.route_def);
52 generated_items.extend(extracted.generated_items);
53 }
54 }
55 }
56
57 let routes_ident = syn::Ident::new("__APIGATE_ROUTES", Span::call_site());
60
61 let service_policy = match &policy {
62 None => quote!(None),
63 Some(p) => quote!(Some(#p)),
64 };
65
66 items.extend(generated_items);
67 items.push(syn::parse_quote! {
68 #[doc(hidden)]
69 pub const #routes_ident: &'static [#apigate_path::RouteDef] = &[
70 #(#route_defs),*
71 ];
72 });
73
74 items.push(syn::parse_quote! {
75 pub fn routes() -> #apigate_path::Routes {
76 #apigate_path::Routes {
77 service: #name,
78 prefix: #prefix,
79 policy: #service_policy,
80 routes: #routes_ident,
81 }
82 }
83 });
84
85 Ok(quote!(#module))
86}
87
88#[proc_macro_attribute]
89pub fn hook(_args: TokenStream, input: TokenStream) -> TokenStream {
90 expand_fn_params(input, "hook", false)
91 .unwrap_or_else(syn::Error::into_compile_error)
92 .into()
93}
94
95#[proc_macro_attribute]
96pub fn map(_args: TokenStream, input: TokenStream) -> TokenStream {
97 expand_fn_params(input, "map", true)
98 .unwrap_or_else(syn::Error::into_compile_error)
99 .into()
100}
101
102pub(crate) fn apigate_crate_path() -> Result<TokenStream2, syn::Error> {
104 use proc_macro_crate::{FoundCrate, crate_name};
105
106 match crate_name("apigate") {
107 Ok(FoundCrate::Itself) => Ok(quote!(::apigate)),
108 Ok(FoundCrate::Name(n)) => {
109 let ident = syn::Ident::new(&n, Span::call_site());
110 Ok(quote!(::#ident))
111 }
112 Err(_) => Ok(quote!(::apigate)),
113 }
114}
115
116macro_rules! route_stub {
117 ($name:ident) => {
118 #[proc_macro_attribute]
119 pub fn $name(_args: TokenStream, input: TokenStream) -> TokenStream {
120 let item = parse_macro_input!(input as syn::Item);
121 syn::Error::new_spanned(
122 item,
123 concat!(
124 "`#[apigate::",
125 stringify!($name),
126 "]` must be used inside a `#[apigate::service] mod ... {}` module"
127 ),
128 )
129 .to_compile_error()
130 .into()
131 }
132 };
133}
134
135route_stub!(get);
136route_stub!(post);
137route_stub!(put);
138route_stub!(delete);
139route_stub!(patch);
140route_stub!(head);
141route_stub!(options);