1#![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::expand_fn_params;
22use route::expand_route_from_fn;
23use service::ServiceArgs;
24
25#[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
45fn 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 if let Some(extracted) = expand_route_from_fn(&apigate_path, f)? {
73 route_defs.push(extracted.route_def);
74 generated_items.extend(extracted.generated_items);
75 }
76 }
77 }
78
79 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#[proc_macro_attribute]
120pub fn hook(_args: TokenStream, input: TokenStream) -> TokenStream {
121 expand_fn_params(input, "hook", false)
122 .unwrap_or_else(syn::Error::into_compile_error)
123 .into()
124}
125
126#[proc_macro_attribute]
133pub fn map(_args: TokenStream, input: TokenStream) -> TokenStream {
134 expand_fn_params(input, "map", true)
135 .unwrap_or_else(syn::Error::into_compile_error)
136 .into()
137}
138
139pub(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 #[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);