Skip to main content

apigate_macros/
lib.rs

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