Skip to main content

apigate_macros/
lib.rs

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
24/// Expands `#[apigate::service(name = "...", prefix = "...")]` on a module:
25/// iterates functions, expands routes, and injects a `routes()` entrypoint.
26fn 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 name = name.unwrap_or_else(|| LitStr::new(&module.ident.to_string(), module.ident.span()));
37    let apigate_path = apigate_crate_path()?;
38
39    let Some((_, items)) = module.content.as_mut() else {
40        return Err(syn::Error::new_spanned(
41            &module,
42            "#[apigate::service] requires an inline module body: `mod x { ... }`",
43        ));
44    };
45
46    let mut route_defs = Vec::new();
47    let mut generated_items = Vec::new();
48
49    for item in items.iter_mut() {
50        if let Item::Fn(f) = item {
51            if let Some(extracted) = expand_route_from_fn(&apigate_path, f)? {
52                route_defs.push(extracted.route_def);
53                generated_items.extend(extracted.generated_items);
54            }
55        }
56    }
57
58    // NOTE: We intentionally generate a hidden const with all routes,
59    // so it can be referenced without recomputing at runtime
60    let routes_ident = syn::Ident::new("__APIGATE_ROUTES", Span::call_site());
61
62    let service_policy = match &policy {
63        None => quote!(None),
64        Some(p) => quote!(Some(#p)),
65    };
66
67    items.extend(generated_items);
68    items.push(syn::parse_quote! {
69        #[doc(hidden)]
70        pub const #routes_ident: &'static [#apigate_path::RouteDef] = &[
71            #(#route_defs),*
72        ];
73    });
74
75    items.push(syn::parse_quote! {
76        pub fn routes() -> #apigate_path::Routes {
77            #apigate_path::Routes {
78                service: #name,
79                prefix: #prefix,
80                policy: #service_policy,
81                routes: #routes_ident,
82            }
83        }
84    });
85
86    Ok(quote!(#module))
87}
88
89#[proc_macro_attribute]
90pub fn hook(_args: TokenStream, input: TokenStream) -> TokenStream {
91    expand_fn_params(input, "hook", false)
92        .unwrap_or_else(syn::Error::into_compile_error)
93        .into()
94}
95
96#[proc_macro_attribute]
97pub fn map(_args: TokenStream, input: TokenStream) -> TokenStream {
98    expand_fn_params(input, "map", true)
99        .unwrap_or_else(syn::Error::into_compile_error)
100        .into()
101}
102
103/// Resolves the path to the `apigate` crate for use in generated code.
104pub(crate) fn apigate_crate_path() -> Result<TokenStream2, syn::Error> {
105    use proc_macro_crate::{FoundCrate, crate_name};
106
107    match crate_name("apigate") {
108        Ok(FoundCrate::Itself) => Ok(quote!(::apigate)),
109        Ok(FoundCrate::Name(n)) => {
110            let ident = syn::Ident::new(&n, Span::call_site());
111            Ok(quote!(::#ident))
112        }
113        Err(_) => Ok(quote!(::apigate)),
114    }
115}
116
117macro_rules! route_stub {
118    ($name:ident) => {
119        #[proc_macro_attribute]
120        pub fn $name(_args: TokenStream, input: TokenStream) -> TokenStream {
121            let item = parse_macro_input!(input as syn::Item);
122            syn::Error::new_spanned(
123                item,
124                concat!(
125                    "`#[apigate::",
126                    stringify!($name),
127                    "]` must be used inside a `#[apigate::service] mod ... {}` module"
128                ),
129            )
130            .to_compile_error()
131            .into()
132        }
133    };
134}
135
136route_stub!(get);
137route_stub!(post);
138route_stub!(put);
139route_stub!(delete);
140route_stub!(patch);
141route_stub!(head);
142route_stub!(options);