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
26fn 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 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
105pub(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);