1use proc_macro::TokenStream;
2use proc_macro2::{Delimiter::Parenthesis, Group, Ident, Literal, Punct, Spacing, Span};
3use quote::*;
4use regex::Regex;
5use std::{fs::read_to_string, path::Path};
6
7mod shared;
8
9use shared::{append_module_as_str, walk_through_dir};
10
11fn http_specs(module_path: &String) -> [String; 2] {
12 let module_pattern =
13 Regex::new("crate::app::(\\w+)::api::(\\w+)").expect("Is not possible to mount regexp");
14 let mut spec = [String::from("get"), String::from("")];
15 let caps = module_pattern.captures(&module_path).expect(&format!(
16 "The module `{}` don't follow the convetion",
17 module_path
18 ));
19 let module_domain = inflector::string::pluralize::to_plural(caps.get(1).unwrap().as_str());
20 let module_name = caps.get(2).unwrap().as_str();
21
22 if !module_name.starts_with("get") && !module_name.starts_with("post") {
23 panic!(
24 "{} - invalid module name, api modules should starts with `post` or `get`",
25 module_path
26 )
27 }
28
29 let mut module_pieces = module_name.split("_");
30 spec[0] = module_pieces.next().unwrap().to_string();
31 spec[1] = format!("/{}", module_domain);
32
33 let mut set_param = false;
34 while let Some(piece) = module_pieces.next() {
35 if spec[1].ends_with(piece) {
39 continue;
40 }
41
42 if piece.to_uppercase() == piece || piece == "id" {
45 spec[1] = format!("{}/:{}", spec[1], piece.to_lowercase());
46 set_param = true;
48 continue;
49 }
50
51 if set_param {
53 spec[1] += "/";
54 set_param = false;
55 }
56
57 if spec[1].ends_with(&module_domain) {
58 spec[1] += "/";
59 }
60
61 if spec[1].ends_with("/") {
62 spec[1] = format!("{}{}", spec[1], piece);
63 continue;
64 }
65
66 spec[1] = format!("{}-{}", spec[1], piece);
69 }
70
71 spec
72}
73
74#[derive(Debug)]
75struct RouteToken {
76 method: String,
77 endpoint: String,
78 handle: String,
79}
80
81impl RouteToken {
82 fn new(module_path: String, fn_name: Option<&str>) -> Self {
83 let spec = http_specs(&module_path);
84 RouteToken {
85 method: spec[0].clone(),
86 endpoint: spec[1].clone(),
87 handle: module_path + "::" + fn_name.unwrap_or("handle"),
88 }
89 }
90}
91
92impl ToTokens for RouteToken {
93 fn to_tokens(&self, tokens: &mut __private::TokenStream) {
94 let mut handle_token = proc_macro2::TokenStream::new();
95 let mut route_call_tokens = proc_macro2::TokenStream::new();
96 route_call_tokens.append(Literal::string(&self.endpoint));
98 route_call_tokens.append(Punct::new(',', Spacing::Alone));
99 append_module_as_str(
101 format!("axum::routing::{}", self.method).as_str(),
102 &mut route_call_tokens,
103 );
104 append_module_as_str(&self.handle, &mut handle_token);
106 route_call_tokens.append(Group::new(Parenthesis, handle_token));
107
108 tokens.append(Punct::new('.', Spacing::Alone));
109 tokens.append(Ident::new("route", Span::call_site()));
110 tokens.append(Group::new(Parenthesis, route_call_tokens));
111 }
112}
113
114fn read_code(path: &str) {
115 let tokens: proc_macro2::TokenStream = read_to_string(path).unwrap().parse().unwrap();
116 }
130
131fn write_configure_router_function(
132 container: &str,
133 input: proc_macro2::TokenStream,
134 routes: &Vec<RouteToken>,
135) -> proc_macro2::TokenStream {
136 let mut fn_name = String::new();
137
138 if container.starts_with("crate::") && container.starts_with("super::") {
139 panic!("The first attribute needs to have fully namespace, like crate::Container or super::Container.");
140 }
141
142 for token in input.clone() {
143 match token {
144 proc_macro2::TokenTree::Ident(ident) => {
145 if !["pub", "async", "fn"].contains(&ident.to_string().as_str()) {
146 fn_name = ident.to_string();
147 break;
148 }
149 }
150 _ => {}
151 }
152 }
153
154 if fn_name.is_empty() {
155 panic!("Cannot get name from token {:?}", input.to_string());
156 }
157
158 let founded_routes = routes.len();
159 let ident = format_ident!("{}", fn_name);
160 let mut container_type = quote! {};
161 append_module_as_str(container, &mut container_type);
162 quote! {
163 pub struct Request {}
164
165 pub fn #ident() -> axum::Router<#container_type> {
166 println!("{} configured routes.", #founded_routes);
167 let mut router = axum::Router::<#container_type>::new()
168 #(#routes)*;
169 router
170 }
171 }
172}
173
174#[proc_macro_attribute]
186pub fn configure_routes(input: TokenStream, body: TokenStream) -> TokenStream {
187 let attrs_string = input.to_string().replace("\"", "");
188 let (pattern, container) = attrs_string
189 .split_once(",")
190 .expect("This macro needs two attributes: file tree pattern and container mod.");
191
192 if !pattern.ends_with(".rs") {
193 panic!(
194 "The provided: {} pattern needs to ends with `.rs` file extension.",
195 pattern
196 );
197 }
198
199 let root_directory = pattern
200 .split_once("*")
201 .expect("only `/src/*/path/*.rs` pattern is acceptable.")
202 .0;
203
204 let path = Path::new(root_directory);
205 let regexp = Regex::new(&pattern.replace("*", "\\w+")).expect(&format!(
206 "Impossibly to build the provided pattern {}",
207 pattern
208 ));
209
210 if !path.is_dir() {
211 panic!(
212 "Directory {} not found.",
213 path.as_os_str().to_str().unwrap()
214 );
215 }
216
217 let mut routes = vec![];
218 walk_through_dir(path, &mut |_, entry| {
219 let str = entry.to_str().unwrap().replace("\\", "/");
220 if regexp.is_match(&str) && !str.ends_with("mod.rs") {
222 read_code(&str);
223 let ident = format!(
224 "crate::{}",
225 str.trim_start_matches("./src/")
226 .trim_end_matches(".rs")
227 .replace("/", "::")
228 );
229
230 routes.push(RouteToken::new(ident, None));
231 }
232 });
233
234 write_configure_router_function(container, body.into(), &routes).into()
235}
236
237#[cfg(test)]
238mod tests {
239 use std::str::FromStr;
240
241 use proc_macro::TokenStream;
242 use quote::*;
243
244 use crate::{configure_routes, http_specs, write_configure_router_function, RouteToken};
245
246 #[test]
247 fn generate_routes() {
248 let route_tokens = vec![
249 RouteToken::new("crate::app::products::api::get_products".to_owned(), None),
250 RouteToken::new("crate::app::products::api::get_id".to_owned(), None),
251 RouteToken::new("crate::app::products::api::post_products".to_owned(), None),
252 ];
253
254 let expected = quote! {
255 let mut router = Router::<Container>::new()
256 .route("/products", axum::routing::get(crate::app::products::api::get_products::handle))
257 .route("/products/:id", axum::routing::get(crate::app::products::api::get_id::handle))
258 .route("/products", axum::routing::post(crate::app::products::api::post_products::handle));
259 };
260
261 let tokens = quote! {
262 let mut router = Router::<Container>::new()
263 #(#route_tokens)*;
264 };
265
266 assert_eq!(tokens.to_string(), expected.to_string())
267 }
268
269 #[test]
270 fn route_token_to_token_stream() {
271 let token = RouteToken::new("crate::app::products::api::get_products".to_owned(), None);
272 let comp = quote! { .route("/products", axum::routing::get(crate::app::products::api::get_products::handle)) };
273 assert_eq!(token.into_token_stream().to_string(), comp.to_string());
274 }
275
276 #[test]
277 fn test_http_specs() {
278 let expected = [String::from("get"), String::from("/products")];
279 assert_eq!(
280 expected,
281 http_specs(&String::from("crate::app::products::api::get_products"))
282 );
283 }
284
285 #[test]
286 fn test_http_specs_with_id() {
287 let expected = [String::from("get"), String::from("/products/:id")];
288 assert_eq!(
289 expected,
290 http_specs(&String::from("crate::app::products::api::get_id"))
291 );
292 }
293
294 #[test]
295 fn test_http_specs_multiple_params() {
296 let expected = [
297 String::from("get"),
298 String::from("/products/:id/order/:order"),
299 ];
300 assert_eq!(
301 expected,
302 http_specs(&String::from(
303 "crate::app::products::api::get_id_order_ORDER"
304 ))
305 );
306 }
307
308 #[test]
309 fn test_http_specs_post_method() {
310 let expected = [String::from("post"), String::from("/products")];
311 assert_eq!(
312 expected,
313 http_specs(&String::from("crate::app::products::api::post_products"))
314 );
315 }
316
317 #[test]
318 fn test_http_specs_only_module_method() {
319 let expected = [String::from("post"), String::from("/products")];
320 assert_eq!(
321 expected,
322 http_specs(&String::from("crate::app::products::api::post"))
323 );
324 }
325
326 #[test]
327 fn test_transform_configure_routes_fn() {
328 let routes = vec![];
330
331 let input = quote! {
332 pub fn config_routes() {}
333 };
334
335 let generated =
336 write_configure_router_function("crate::Container", input, &routes).to_string();
337
338 let expected = quote! {
339 pub fn config_routes() -> axum::Router<crate::Container> {
340 println!("{} configured routes.", 0usize);
341 let mut router = axum::Router::<crate::Container>::new();
342 router
343 }
344 };
345
346 assert_eq!(generated, expected.to_string());
347 }
348}