Skip to main content

apigate_macros/
lib.rs

1//! Procedural macros for `apigate`.
2//!
3//! This crate is normally used through the `apigate` facade crate and provides:
4//! service modules, route attributes, request hooks, and request maps.
5#![warn(missing_docs)]
6
7extern crate core;
8
9mod codegen;
10mod expand;
11mod parse;
12mod route;
13mod service;
14mod template;
15
16use proc_macro::TokenStream;
17use proc_macro2::{Span, TokenStream as TokenStream2};
18use quote::quote;
19use syn::{Item, ItemMod, LitStr, parse_macro_input};
20
21use expand::{ExpansionMode, expand_fn_params};
22use route::expand_route_from_fn;
23use service::ServiceArgs;
24
25/// Defines an apigate service module.
26///
27/// The macro scans route attributes inside the inline module, generates a
28/// static route table, and injects a `routes()` function returning
29/// `apigate::Routes`.
30///
31/// Supported module arguments:
32/// - `name = "service_name"`: overrides the module name as service name.
33/// - `prefix = "/path"`: mounts all routes under the prefix.
34/// - `policy = "policy_name"`: applies a named policy to all routes.
35///
36/// Route attributes such as `#[apigate::get(...)]` must be used inside this
37/// module.
38#[proc_macro_attribute]
39pub fn service(args: TokenStream, input: TokenStream) -> TokenStream {
40    expand_service(args, input)
41        .unwrap_or_else(syn::Error::into_compile_error)
42        .into()
43}
44
45/// Expands `#[apigate::service(name = "...", prefix = "...")]` on a module:
46/// iterates functions, expands routes, and injects a `routes()` entrypoint.
47fn expand_service(args: TokenStream, input: TokenStream) -> syn::Result<TokenStream2> {
48    let args = syn::parse::<ServiceArgs>(args)?;
49    let ServiceArgs {
50        name,
51        prefix,
52        policy,
53    } = args;
54    let prefix = prefix.unwrap_or_else(|| LitStr::new("", Span::call_site()));
55
56    let mut module = syn::parse::<ItemMod>(input)?;
57    let name = name.unwrap_or_else(|| LitStr::new(&module.ident.to_string(), module.ident.span()));
58    let apigate_path = apigate_crate_path()?;
59
60    let Some((_, items)) = module.content.as_mut() else {
61        return Err(syn::Error::new_spanned(
62            &module,
63            "#[apigate::service] requires an inline module body: `mod x { ... }`",
64        ));
65    };
66
67    let mut route_defs = Vec::new();
68    let mut generated_items = Vec::new();
69
70    for item in items.iter_mut() {
71        if let Item::Fn(f) = item
72            && let Some(extracted) = expand_route_from_fn(&apigate_path, f)?
73        {
74            route_defs.push(extracted.route_def);
75            generated_items.extend(extracted.generated_items);
76        }
77    }
78
79    // NOTE: We intentionally generate a hidden const with all routes,
80    // so it can be referenced without recomputing at runtime
81    let routes_ident = syn::Ident::new("__APIGATE_ROUTES", Span::call_site());
82
83    let service_policy = match &policy {
84        None => quote!(None),
85        Some(p) => quote!(Some(#p)),
86    };
87
88    items.extend(generated_items);
89    items.push(syn::parse_quote! {
90        #[doc(hidden)]
91        pub const #routes_ident: &'static [#apigate_path::RouteDef] = &[
92            #(#route_defs),*
93        ];
94    });
95
96    items.push(syn::parse_quote! {
97        pub fn routes() -> #apigate_path::Routes {
98            #apigate_path::Routes {
99                service: #name,
100                prefix: #prefix,
101                policy: #service_policy,
102                routes: #routes_ident,
103            }
104        }
105    });
106
107    Ok(quote!(#module))
108}
109
110/// Marks an async function as a request hook.
111///
112/// Hooks can inspect and mutate request parts through `PartsCtx`, use
113/// `RequestScope` for shared/per-request state, and return
114/// `apigate::HookResult`.
115///
116/// The generated wrapper normalizes supported parameters into
117/// `(&mut PartsCtx, &mut RequestScope)` so route pipelines can call hooks
118/// cheaply.
119#[proc_macro_attribute]
120pub fn hook(_args: TokenStream, input: TokenStream) -> TokenStream {
121    expand_fn_params(input, ExpansionMode::Hook)
122        .unwrap_or_else(syn::Error::into_compile_error)
123        .into()
124}
125
126/// Marks an async function as a request mapper.
127///
128/// Maps transform typed `query`, `json`, or `form` inputs into a new serialized
129/// upstream request payload or query string. The first owned parameter is kept
130/// as the typed map input; other supported parameters are extracted from
131/// `RequestScope` or request parts.
132#[proc_macro_attribute]
133pub fn map(_args: TokenStream, input: TokenStream) -> TokenStream {
134    expand_fn_params(input, ExpansionMode::Map)
135        .unwrap_or_else(syn::Error::into_compile_error)
136        .into()
137}
138
139/// Resolves the path to the `apigate` crate for use in generated code.
140pub(crate) fn apigate_crate_path() -> Result<TokenStream2, syn::Error> {
141    use proc_macro_crate::{FoundCrate, crate_name};
142
143    match crate_name("apigate") {
144        Ok(FoundCrate::Itself) => Ok(quote!(::apigate)),
145        Ok(FoundCrate::Name(n)) => {
146            let ident = syn::Ident::new(&n, Span::call_site());
147            Ok(quote!(::#ident))
148        }
149        Err(_) => Ok(quote!(::apigate)),
150    }
151}
152
153macro_rules! route_stub {
154    ($name:ident) => {
155        /// Declares a route inside an `#[apigate::service]` module.
156        ///
157        /// This attribute is only expanded by `#[apigate::service]`; using it
158        /// directly outside a service module produces a compile error.
159        #[proc_macro_attribute]
160        pub fn $name(_args: TokenStream, input: TokenStream) -> TokenStream {
161            let item = parse_macro_input!(input as syn::Item);
162            syn::Error::new_spanned(
163                item,
164                concat!(
165                    "`#[apigate::",
166                    stringify!($name),
167                    "]` must be used inside a `#[apigate::service] mod ... {}` module"
168                ),
169            )
170            .to_compile_error()
171            .into()
172        }
173    };
174}
175
176route_stub!(get);
177route_stub!(post);
178route_stub!(put);
179route_stub!(delete);
180route_stub!(patch);
181route_stub!(head);
182route_stub!(options);