rapid_web_codegen/
lib.rs

1mod utils;
2use proc_macro::TokenStream;
3use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
4use quote::quote;
5use regex::Regex;
6use std::{
7	fs::{read_dir, File},
8	io::prelude::*,
9	path::PathBuf,
10};
11use utils::{
12	base_file_name, get_all_dirs, get_all_middleware, parse_handler_path, parse_route_path, reverse_route_path, validate_route_handler,
13	NEXTJS_ROUTE_PATH, REMIX_ROUTE_PATH,
14};
15
16// TODO: some of this could leverage tokio
17
18/// Inits a traditional actix-web server entrypoint
19/// Note: this is only being done because we need to re-route the macro to point at rapid_web instead of actix
20///
21/// # Examples
22/// ```
23/// #[rapid_web::main]
24/// async fn main() {
25///     async { println!("Hello world"); }.await
26/// }
27/// ```
28#[proc_macro_attribute]
29pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
30	let mut output: TokenStream = (quote! {
31		#[::rapid_web::actix::rt::main(system = "::rapid_web::actix::rt::System")]
32	})
33	.into();
34
35	output.extend(item);
36	output
37}
38
39// A macro for flagging functions as route handlers for type generation
40// Note: this macro does nothing other than serving as a flag for the rapid compiler/file-parser
41#[proc_macro_attribute]
42pub fn rapid_handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
43	item
44}
45
46struct Handler {
47	path: String,
48	absolute_path: String,
49	name: String,
50	is_nested: bool,
51}
52
53// Currently, the rapid file-based router will only support GET, POST, DELETE, and PUT request formats (we could support patch if needed)
54enum RouteHandler {
55	Query(Handler),
56	Mutation(Handler),
57	Get(Handler),
58	Post(Handler),
59	Delete(Handler),
60	Put(Handler),
61	Patch(Handler),
62}
63
64/// Macro for generated rapid route handlers based on the file system
65///
66/// This macro will look through the specified path and codegen route handlers for each one
67/// Currently, there is only logic inplace to support GET, POST, DELETE, and PUT requests as well as middleware via a "_middleware.rs" file
68///
69/// * `item` - A string slice that holds the path to the file system routes root directory (ex: "src/routes")
70/// # Examples
71/// ```
72/// routes!("src/routes")
73/// ```
74#[proc_macro]
75pub fn routes(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
76	// Get the rapid server config file and check for the routes path (we want to panic otherwise)
77	let routes_path = if let proc_macro::TokenTree::Literal(literal) = item.into_iter().next().unwrap() {
78		literal.to_string()
79	} else {
80		panic!("Error: Invalid routes path!")
81	};
82
83	// If the users routes path is not nested we want to panic
84	if routes_path == "/" {
85		panic!("The 'routes_directory' variable cannot be set to a base path. Please use something nested!");
86	}
87
88	// Remove string quotes on start and end of path
89	let parsed_path = &routes_path[1..routes_path.len() - 1];
90
91	// Directories with routes in them
92	let mut route_dirs: Vec<PathBuf> = vec![];
93	// Base handlers
94	let mut route_handlers: Vec<RouteHandler> = vec![];
95	// Check if there is base middleware
96	let mut has_root_middleware = false;
97
98	// Get every nested dir and append them to the route_dirs aray
99	get_all_dirs(parsed_path, &mut route_dirs);
100
101	// Get all the files from the base specified path
102	let route_files = read_dir(parsed_path)
103		.unwrap()
104		.map(|path| {
105			let path = path.unwrap().path();
106			path
107		})
108		.filter(|item| {
109			if item.is_dir() {
110				return false;
111			}
112
113			let file_name = item.file_name().unwrap();
114
115			if file_name == "_middleware.rs" {
116				has_root_middleware = true;
117			}
118
119			file_name != "mod"
120		})
121		.collect::<Vec<_>>();
122
123	// Go through each file path and generate route handlers for each one (this is only for the base dir '/')
124	for file_path in route_files {
125		// Open the route file
126		let mut file = File::open(&file_path).unwrap();
127		// Get the name of the file (this drives the route path)
128		// Index.rs will generate a '/' route and anything else simply is generated based on the file name (stem without ".rs")
129		let file_name = file_path.file_stem().unwrap().to_string_lossy().to_string();
130		// Save the file contents to a variable
131		let mut file_contents = String::new();
132		file.read_to_string(&mut file_contents).unwrap();
133
134		// Check for dynamic routes
135		let dynamic_route_regex = Regex::new(r"_.*?_").unwrap();
136
137		let is_dynamic_route = dynamic_route_regex.is_match(&file_name);
138
139		let parsed_name = match is_dynamic_route {
140			true => file_name.replacen("_", r"{", 1).replacen("_", "}", 1),
141			false => file_name,
142		};
143
144		// Construct our handler
145		let handler = Handler {
146			name: parsed_name,
147			path: String::from("/"),
148			absolute_path: parsed_path.to_string(),
149			is_nested: false,
150		};
151
152		// Check if the contents contain a valid rapid_web route and append them to the route handlers vec
153		// Routes are determined valid if they contain a function that starts with "async fn" and contains the "rapid_handler" attribute macro
154		parse_handlers(&mut route_handlers, file_contents, handler);
155	}
156
157	for nested_file_path in route_dirs {
158		// Get all the files from the base specified path
159		let route_files = read_dir(&nested_file_path)
160			.unwrap()
161			.map(|path| {
162				let path = path.unwrap().path();
163				path
164			})
165			.filter(|item| {
166				if item.is_dir() {
167					return false;
168				}
169
170				let file_name = item.file_name().unwrap();
171
172				file_name != "mod"
173			})
174			.collect::<Vec<_>>();
175
176		// Get the base file path from the long "src/routes/etc/etc" path
177		let cleaned_route_path = base_file_name(&nested_file_path, parsed_path);
178
179		for file_path in route_files {
180			// Open the route file
181			let mut file = File::open(&file_path).unwrap();
182			// Get the name of the file (this drives the route path)
183			// Index.rs will generate a '/' route and anything else
184			let file_name = file_path.file_stem().unwrap().to_string_lossy().to_string();
185
186			// Check if there is middleware in the current path (this is done via checking for a "_middleware" filename)
187			// We do not want to register any middleware handlers so trigger an early exit
188			if file_name == String::from("_middleware") {
189				continue;
190			}
191
192			let dynamic_route_regex = Regex::new(r"_.*?_").unwrap();
193
194			let is_dynamic_route = dynamic_route_regex.is_match(&file_name);
195
196			let parsed_name = match is_dynamic_route {
197				true => file_name.replacen("_", r"{", 1).replacen("_", "}", 1),
198				false => file_name,
199			};
200
201			// Save the file contents to a variable
202			let mut file_contents = String::new();
203			// Get the files contents...
204			file.read_to_string(&mut file_contents).unwrap();
205
206			// Construct our handler
207			let handler = Handler {
208				name: parsed_name,
209				path: parse_route_path(cleaned_route_path.clone()),
210				absolute_path: nested_file_path.as_os_str().to_str().unwrap().to_string(),
211				is_nested: true,
212			};
213
214			// Check if the contents contain a valid rapid_web route and append them to the route handlers vec
215			// TODO: we should consider supporting HEAD requests here as well (currently, we require that this be done through a traditional .route() call on the route builder function)
216			parse_handlers(&mut route_handlers, file_contents, handler);
217		}
218	}
219
220	// Generate the token indents that we will pass into the actix-web router
221	let idents = route_handlers
222		.into_iter()
223		.map(|it| match it {
224			RouteHandler::Get(route_handler) => generate_handler_tokens(route_handler, parsed_path, "get"),
225			RouteHandler::Post(route_handler) => generate_handler_tokens(route_handler, parsed_path, "post"),
226			RouteHandler::Delete(route_handler) => generate_handler_tokens(route_handler, parsed_path, "delete"),
227			RouteHandler::Put(route_handler) => generate_handler_tokens(route_handler, parsed_path, "put"),
228			RouteHandler::Patch(route_handler) => generate_handler_tokens(route_handler, parsed_path, "patch"),
229			RouteHandler::Query(route_handler) => generate_handler_tokens(route_handler, parsed_path, "query"),
230			RouteHandler::Mutation(route_handler) => generate_handler_tokens(route_handler, parsed_path, "mutation"),
231		})
232		.collect::<Vec<_>>();
233
234	proc_macro::TokenStream::from(quote!(
235		web::scope("")
236			#(#idents)*
237	))
238}
239
240/// A macro for generating imports for every route handler (used in rapid file based routing)
241///
242/// This macro must be used before any other code runs.
243/// # Arguments
244///
245/// * `item` - A string slice that holds the path to the file system routes root directory
246///
247/// # Examples
248/// ```
249/// rapid_configure!("src/routes")
250/// ```
251#[proc_macro]
252pub fn rapid_configure(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
253	let path_string = if let proc_macro::TokenTree::Literal(literal) = item.into_iter().next().unwrap() {
254		literal.to_string()
255	} else {
256		panic!("Error: Invalid routes path!")
257	};
258	let path = &path_string[1..path_string.len() - 1];
259	let module_name = Ident::new(&path[path.find("/").map(|it| it + 1).unwrap_or(0)..], Span::call_site());
260
261	let mut route_dirs: Vec<PathBuf> = vec![];
262
263	// Get every nested dir and append them to the route_dirs array
264	get_all_dirs(path, &mut route_dirs);
265
266	// Grab all of the base idents that we need to power the base "/" handler
267	let base_idents = std::fs::read_dir(path)
268		.unwrap()
269		.map(|it| {
270			let path = it.unwrap().path();
271			let name = path.file_stem().unwrap().to_string_lossy();
272			Ident::new(&name, Span::call_site())
273		})
274		.filter(|it| it.to_string() != "mod")
275		.collect::<Vec<_>>();
276
277	let mut nested_idents: Vec<TokenStream2> = Vec::new();
278
279	for dir in route_dirs {
280		let string = dir.into_os_string().into_string().unwrap();
281
282		let mod_name = format!("{}", string.replace("src/", "").replace("/", "::"));
283		let tokens: proc_macro2::TokenStream = mod_name.parse().unwrap();
284		nested_idents.push(quote! { pub use #tokens::*; });
285	}
286
287	proc_macro::TokenStream::from(quote!(
288		use include_dir::{include_dir, Dir};
289		use rapid_web::actix::web;
290		mod #module_name { #(pub mod #base_idents;)* }
291		pub use #module_name::{
292			#(#base_idents,)*
293		};
294		#(#nested_idents)*
295		#[cfg(debug_assertions)] // Only run this in debug mode (having extra code in main.rs file makes binary way larger)
296		const ROUTES_DIR: Dir = include_dir!(#path); // Including the entire routes dir here is what provides the "hot-reload" effect to the config macro
297	))
298}
299
300/// A macro for generating imports for every route handler in a Rapid NextJS application
301///
302/// This macro must be used before any other code runs within your `root.rs` file.
303///
304/// # Examples
305/// ```
306/// rapid_configure_nextjs!()
307/// ```
308#[proc_macro]
309pub fn rapid_configure_nextjs(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
310	generate_route_imports(tokens, NEXTJS_ROUTE_PATH)
311}
312
313/// A macro for generating imports for every route handler in a Rapid Remix application
314///
315/// This macro must be used before any other code runs within your `root.rs` file.
316///
317/// # Examples
318/// ```
319/// rapid_configure_remix!()
320/// ```
321#[proc_macro]
322pub fn rapid_configure_remix(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
323	generate_route_imports(tokens, REMIX_ROUTE_PATH)
324}
325
326/// Function that generates handler tokens from a Handler type
327/// Note: this currently depends on the assumption that each dir only has 1 _middleware.rs file which we always be the case since dupe file names is not allowed
328fn generate_handler_tokens(route_handler: Handler, parsed_path: &str, handler_type: &str) -> proc_macro2::TokenStream {
329	let parsed_handler_type: proc_macro2::TokenStream = handler_type.parse().unwrap();
330
331	let mut middleware_paths: Vec<PathBuf> = Vec::new();
332	get_all_middleware(&route_handler.absolute_path, parsed_path, &mut middleware_paths);
333
334	let middleware_idents = middleware_paths
335		.into_iter()
336		.map(|middleware| {
337			let base = base_file_name(&middleware.as_path(), parsed_path);
338
339			// Trigger an early exit here if we find that we were in the base dir anyway
340			if base == "" {
341				return quote!(.wrap(_middleware::Middleware));
342			}
343			// Parse the module name string and remove all the slashes (ex: "/job" endpoint)
344			let mod_name = format!("{}", base.replacen("/", "", 1)).replace("/", "::");
345
346			let parsed_mod: proc_macro2::TokenStream = mod_name.parse().unwrap();
347
348			quote!(.wrap(#parsed_mod::_middleware::Middleware))
349		})
350		.collect::<Vec<_>>();
351
352	// We want all "index.rs" files to auto map to "/"
353	let name = match route_handler.name.as_str() {
354		"index" => String::from(""),
355		_ => format!("/{}", route_handler.name),
356	};
357
358	let parsed_path = match route_handler.path.as_str() {
359		"/" => "",
360		_ => route_handler.path.as_str(),
361	};
362
363	// Parse the module name string and remove all the slashes (ex: "/job" endpoint)
364	let handler_mod_name = format!(
365		"{}",
366		parse_handler_path(&format!("{}/{}", reverse_route_path(parsed_path.to_string()), route_handler.name)).replacen("/", "", 1)
367	)
368	.replace("/", "::");
369	let handler: proc_macro2::TokenStream = handler_mod_name.parse().unwrap();
370
371	// This is the path string that gets passed to the ".route(path)" function
372	let rapid_routes_path = {
373		if route_handler.is_nested {
374			format!("{}{}", parsed_path, name)
375		} else {
376			// If the router is not nested then we want to set the name to a slash
377			let parsed_name = match name.as_str() {
378				"" => "/",
379				_ => name.as_str(),
380			};
381
382			format!("{}{}", parsed_path, parsed_name)
383		}
384	};
385
386	// Output our idents based on the handler types
387	match handler_type {
388		// Check if we got a query or mutation..
389		"query" => {
390			// If we got a query type we want to generate routes for `get` request types (`delete` could get moved to here too...?)
391			quote!(
392				.route(#rapid_routes_path, web::get().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
393			)
394		}
395		"mutation" => {
396			// If we got a mutation type we want to generate routes for each of the following (all at the same path):
397			// `post`, `put`, `patch`, `delete`
398			quote!(
399				.route(#rapid_routes_path, web::post().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
400				.route(#rapid_routes_path, web::put().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
401				.route(#rapid_routes_path, web::patch().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
402				.route(#rapid_routes_path, web::delete().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
403			)
404		}
405		// Currently we still support declaring handlers with a very specific HTTP type (ex: `get` or `post` etc)
406		// ^^^ Eventually, what was described above should get deprecated
407		_ => quote!(.route(#rapid_routes_path, web::#parsed_handler_type().to(#handler::#parsed_handler_type)#(#middleware_idents)*)),
408	}
409}
410
411/// Function for parsing a route file and making sure it contains a valid handler
412/// If it does, we want to push the valid handler to the handlers array
413/// Note: no need to support HEAD and OPTIONS requests
414fn parse_handlers(route_handlers: &mut Vec<RouteHandler>, file_contents: String, handler: Handler) {
415	// TODO: we need to depricate everything except for `query` and `mutation`
416	if file_contents.contains("async fn get") && validate_route_handler(&file_contents) {
417		route_handlers.push(RouteHandler::Get(handler))
418	} else if file_contents.contains("async fn post") && validate_route_handler(&file_contents) {
419		route_handlers.push(RouteHandler::Post(handler))
420	} else if file_contents.contains("async fn delete") && validate_route_handler(&file_contents) {
421		route_handlers.push(RouteHandler::Delete(handler))
422	} else if file_contents.contains("async fn put") && validate_route_handler(&file_contents) {
423		route_handlers.push(RouteHandler::Put(handler))
424	} else if file_contents.contains("async fn patch") && validate_route_handler(&file_contents) {
425		route_handlers.push(RouteHandler::Patch(handler))
426	} else if file_contents.contains("async fn query") && validate_route_handler(&file_contents) {
427		route_handlers.push(RouteHandler::Query(handler))
428	} else if file_contents.contains("async fn mutation") && validate_route_handler(&file_contents) {
429		route_handlers.push(RouteHandler::Mutation(handler))
430	}
431}
432
433/// Generates route imports based on a framework specific base routes directory
434fn generate_route_imports(tokens: proc_macro::TokenStream, routes_directory: &str) -> proc_macro::TokenStream {
435	// Parse the tokens and check if they did not contain anything...
436	let path = if tokens.is_empty() {
437		// Output the default remix routes path if we did not find any tokens passed in by the user
438		routes_directory.to_string()
439	} else {
440		// If we found any tokens we want to output them and use them as the routes path
441		let path_string = if let proc_macro::TokenTree::Literal(literal) = tokens.into_iter().next().unwrap() {
442			let raw_path = literal.to_string();
443			raw_path[1..raw_path.len() - 1].to_string()
444		} else {
445			routes_directory.to_string()
446		};
447		path_string.to_string()
448	};
449
450	let module_name = Ident::new("routes", Span::call_site());
451
452	let mut route_dirs: Vec<PathBuf> = vec![];
453
454	// Get every nested dir and append them to the route_dirs array
455	get_all_dirs(&path, &mut route_dirs);
456
457	// Grab all of the base idents that we need to power the base "/" handler
458	let base_idents = std::fs::read_dir(&path)
459		.unwrap()
460		.map(|it| {
461			let path = it.unwrap().path();
462			let name = path.file_stem().unwrap().to_string_lossy();
463			Ident::new(&name, Span::call_site())
464		})
465		.filter(|it| it.to_string() != "mod")
466		.collect::<Vec<_>>();
467
468	let mut nested_idents: Vec<TokenStream2> = Vec::new();
469
470	for dir in route_dirs {
471		let string = dir.into_os_string().into_string().unwrap();
472		let delimiter = "routes";
473		let start_index = match string.find(delimiter) {
474			Some(index) => index + delimiter.len(),
475			None => {
476				panic!("Invalid route directory!");
477			}
478		};
479		let mod_name = format!("routes{}", &string[start_index..].replace("/", "::"));
480		let tokens: proc_macro2::TokenStream = mod_name.parse().unwrap();
481		nested_idents.push(quote! { pub use #tokens::*; });
482	}
483
484	// If the user wanted to use their own path we should force them to import their routes dir
485	if path != routes_directory {
486		return proc_macro::TokenStream::from(quote!(
487			use include_dir::{include_dir, Dir};
488			use rapid_web::actix::web;
489			pub use #module_name::{
490				#(#base_idents,)*
491			};
492			#(#nested_idents)*
493			#[cfg(debug_assertions)] // Only run this in debug mode when the user actually wants hot-reload (having extra code in main.rs file makes binary way larger)
494			const ROUTES_DIR: Dir = include_dir!(#path); // Including the entire routes dir here is what provides the "hot-reload" effect to the config macro
495		));
496	} else {
497		// Since the user did not specify a custom path we can just assume they used the `REMIX_ROUTE_PATH`
498		return proc_macro::TokenStream::from(quote!(
499			use include_dir::{include_dir, Dir};
500			use rapid_web::actix::web;
501			mod #module_name { #(pub mod #base_idents;)* }
502			pub use #module_name::{
503				#(#base_idents,)*
504			};
505			#(#nested_idents)*
506			#[cfg(debug_assertions)] // Only run this in debug mode (having extra code in main.rs file makes binary way larger)
507			const ROUTES_DIR: Dir = include_dir!(#path); // Including the entire routes dir here is what provides the "hot-reload" effect to the config macro
508		));
509	}
510}