Skip to main content

apigate_macros/
lib.rs

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