lenso_capability_http_endpoint_macros/
lib.rs1use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6 Attribute, Error, ImplItem, ItemImpl, LitStr, Result, Token,
7 parse::{Parse, ParseStream},
8 parse_macro_input,
9 punctuated::Punctuated,
10 spanned::Spanned,
11};
12
13#[proc_macro_attribute]
18pub fn endpoint(arguments: TokenStream, input: TokenStream) -> TokenStream {
19 if !arguments.is_empty() {
20 return Error::new(
21 proc_macro2::Span::call_site(),
22 "endpoint does not accept arguments",
23 )
24 .into_compile_error()
25 .into();
26 }
27
28 let implementation = parse_macro_input!(input as ItemImpl);
29 expand_endpoint(implementation)
30 .unwrap_or_else(Error::into_compile_error)
31 .into()
32}
33
34fn expand_endpoint(mut implementation: ItemImpl) -> Result<proc_macro2::TokenStream> {
35 if implementation.trait_.is_some() {
36 return Err(Error::new_spanned(
37 implementation.impl_token,
38 "endpoint can only be applied to an inherent impl block",
39 ));
40 }
41 if !implementation.generics.params.is_empty() {
42 return Err(Error::new_spanned(
43 &implementation.generics,
44 "endpoint does not support generic impl blocks",
45 ));
46 }
47
48 let provider = implementation.self_ty.clone();
49 let mut routes = Vec::new();
50 for item in &mut implementation.items {
51 let ImplItem::Fn(method) = item else {
52 continue;
53 };
54 let route = take_route(&mut method.attrs)?;
55 if let Some(route) = route {
56 routes.push((route, method.sig.ident.clone()));
57 }
58 }
59 if routes.is_empty() {
60 return Err(Error::new_spanned(
61 &implementation.self_ty,
62 "endpoint impl must declare at least one HTTP handler attribute",
63 ));
64 }
65
66 let route_ids = routes.iter().map(|(route, _)| &route.id);
67 let methods = routes.iter().map(|(route, _)| &route.method);
68 let paths = routes.iter().map(|(route, _)| &route.path);
69 let handlers = routes.iter().map(|(_, handler)| handler);
70
71 Ok(quote! {
72 #implementation
73
74 ::lenso_capability_http_endpoint::http_endpoint! {
75 impl #provider {
76 #(
77 #route_ids => (#methods, #paths) => #handlers,
78 )*
79 }
80 }
81 })
82}
83
84fn take_route(attributes: &mut Vec<Attribute>) -> Result<Option<Route>> {
85 let mut route = None;
86 let mut retained = Vec::with_capacity(attributes.len());
87 for attribute in attributes.drain(..) {
88 let Some(method) = http_method(&attribute) else {
89 retained.push(attribute);
90 continue;
91 };
92 if route.is_some() {
93 return Err(Error::new_spanned(
94 attribute,
95 "an endpoint handler may declare only one HTTP method",
96 ));
97 }
98 let arguments = attribute.parse_args::<RouteArguments>()?;
99 route = Some(Route {
100 method: LitStr::new(method, attribute.path().span()),
101 id: arguments.route_id,
102 path: arguments.path,
103 });
104 }
105 *attributes = retained;
106 Ok(route)
107}
108
109fn http_method(attribute: &Attribute) -> Option<&'static str> {
110 let path = attribute.path();
111 [
112 ("get", "GET"),
113 ("post", "POST"),
114 ("put", "PUT"),
115 ("patch", "PATCH"),
116 ("delete", "DELETE"),
117 ("head", "HEAD"),
118 ("options", "OPTIONS"),
119 ]
120 .into_iter()
121 .find_map(|(attribute, method)| path.is_ident(attribute).then_some(method))
122}
123
124struct RouteArguments {
125 route_id: LitStr,
126 path: LitStr,
127}
128
129impl Parse for RouteArguments {
130 fn parse(input: ParseStream<'_>) -> Result<Self> {
131 let arguments = Punctuated::<LitStr, Token![,]>::parse_terminated(input)?;
132 if arguments.len() != 2 {
133 return Err(Error::new(
134 input.span(),
135 "HTTP handler attributes require a route ID and path",
136 ));
137 }
138 let mut arguments = arguments.into_iter();
139 Ok(Self {
140 route_id: arguments.next().expect("length was checked"),
141 path: arguments.next().expect("length was checked"),
142 })
143 }
144}
145
146struct Route {
147 method: LitStr,
148 id: LitStr,
149 path: LitStr,
150}
151
152#[cfg(test)]
153mod tests {
154 use super::expand_endpoint;
155 use syn::parse_quote;
156
157 #[test]
158 fn expands_handler_attributes_into_the_static_route_table() {
159 let expanded = expand_endpoint(parse_quote! {
160 impl OrdersHttp {
161 #[get("orders.read", "/orders/{order_id}")]
162 async fn read(&self) {}
163 }
164 })
165 .unwrap()
166 .to_string();
167
168 assert!(expanded.contains("http_endpoint"));
169 assert!(expanded.contains("orders.read"));
170 assert!(expanded.contains("GET"));
171 assert!(expanded.contains("/orders/{order_id}"));
172 assert!(!expanded.contains("# [get"));
173 }
174
175 #[test]
176 fn rejects_handlers_with_multiple_http_methods() {
177 let error = expand_endpoint(parse_quote! {
178 impl OrdersHttp {
179 #[get("orders.read", "/orders/{order_id}")]
180 #[post("orders.read", "/orders/{order_id}")]
181 async fn read(&self) {}
182 }
183 })
184 .unwrap_err();
185
186 assert!(error.to_string().contains("only one HTTP method"));
187 }
188
189 #[test]
190 fn rejects_impls_without_handlers() {
191 let error = expand_endpoint(parse_quote! {
192 impl OrdersHttp {
193 fn helper(&self) {}
194 }
195 })
196 .unwrap_err();
197
198 assert!(error.to_string().contains("at least one HTTP handler"));
199 }
200}