1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#![feature(proc_macro_quote)]
mod utils;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use std::{
	fs::{read_dir, File},
	io::prelude::*,
	path::PathBuf,
};
use utils::{get_all_dirs, base_file_name};

/// Inits a traditional actix-web server entrypoint
/// Note: this is only being done because we need to re-route the macro to point at rapid_web
///
/// # Examples
/// ```
/// #[rapid_web::main]
/// async fn main() {
///     async { println!("Hello world"); }.await
/// }
/// ```
#[proc_macro_attribute]
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
	let mut output: TokenStream = (quote! {
		#[::rapid_web::actix::rt::main(system = "::rapid_web::actix::rt::System")]
	})
	.into();

	output.extend(item);
	output
}

struct Handler {
	path: String,
	name: String,
}

// Currently, the rapid file-based router will only support GET, POST, DELETE, and PUT request formats
enum RouteHandler {
	Get(Handler),
	Post(Handler),
	Delete(Handler),
	Put(Handler),
}


#[proc_macro]
pub fn routes(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	// Parse the inputted routes path param
	let routes_path = if let proc_macro::TokenTree::Literal(literal) = item.into_iter().next().unwrap() {
		literal.to_string()
	} else {
		panic!("Error: Invalid routes path!")
	};
	// Remove string quotes on start and end of path
	let parsed_path = &routes_path[1..routes_path.len() - 1];

	let mut route_dirs: Vec<PathBuf> = vec![];

	// Get every nested dir and append them to the route_dirs aray
	get_all_dirs("src/routes", &mut route_dirs);

	// Get all the files from the base specified path
	let route_files = read_dir(parsed_path)
		.unwrap()
		.map(|path| {
			let path = path.unwrap().path();
			path
		})
		.filter(|item| {
			if item.is_dir() {
				return false;
			}
			item.file_name().unwrap() != "mod"
		})
		.collect::<Vec<_>>();

	let mut route_handlers: Vec<RouteHandler> = vec![];

	// Go through each file path and generate route handlers for each one (this is only for the base dir '/')
	for file_path in route_files {
		// Open the route file
		let mut file = File::open(&file_path).unwrap();
		// Get the name of the file (this drives the route path)
		// Index.rs will generate a '/' route and anything else
		let file_name = file_path.file_stem().unwrap().to_string_lossy().to_string();
		// Save the file contents to a variable
		let mut file_contents = String::new();
		file.read_to_string(&mut file_contents).unwrap();

		// Construct our handler
		let handler = Handler {
			name: file_name,
			path: String::from("/"),
		};

		// Check if the contents contain a valid rapid_web route and append them to the route handlers vec
		if file_contents.contains("async fn get") {
			route_handlers.push(RouteHandler::Get(handler))
		} else if file_contents.contains("async fn post") {
			route_handlers.push(RouteHandler::Post(handler))
		} else if file_contents.contains("async fn delete") {
			route_handlers.push(RouteHandler::Delete(handler))
		} else if file_contents.contains("async fn put") {
			route_handlers.push(RouteHandler::Put(handler))
		}
	}

	// Generate the token indents that we will pass into the actix-web router
	let idents = route_handlers
		.into_iter()
		.map(|it| match it {
			RouteHandler::Get(route_handler) => {
				let handler = Ident::new(&route_handler.name, Span::call_site());
				let path = format!("{}{}", route_handler.name, route_handler.path);
				quote!(.route(#path, web::get().to(#handler::get)))
			}
			RouteHandler::Post(route_handler) => {
				let handler = Ident::new(&route_handler.name, Span::call_site());
				let path = format!("{}{}", route_handler.name, route_handler.path);
				quote!(.route(#path, web::post().to(#handler::post)))
			}
			RouteHandler::Delete(route_handler) => {
				let handler = Ident::new(&route_handler.name, Span::call_site());
				let path = format!("{}{}", route_handler.name, route_handler.path);
				quote!(.route(#path, web::delete().to(#handler::delete)))
			}
			RouteHandler::Put(route_handler) => {
				let handler = Ident::new(&route_handler.name, Span::call_site());
				let path = format!("{}{}", route_handler.name, route_handler.path);
				quote!(.route(#path, web::put().to(#handler::put)))
			}
		})
		.collect::<Vec<_>>();

	proc_macro::TokenStream::from(quote!(
		web::scope("")
			#(#idents)*
	))
}

// This function will generate the imports needed for each route handlier
#[proc_macro]
pub fn rapid_configure(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	let path_string = if let proc_macro::TokenTree::Literal(lit) = item.into_iter().next().unwrap() {
		lit.to_string()
	} else {
		panic!("Error: Invalid routes path!")
	};
	let path = &path_string[1..path_string.len() - 1];
	let module_name = Ident::new(&path[path.find("/").map(|it| it + 1).unwrap_or(0)..], Span::call_site());

	let idents = std::fs::read_dir(path)
		.unwrap()
		.map(|it| {
			let path = it.unwrap().path();
			let name = path.file_stem().unwrap().to_string_lossy();
			Ident::new(&name, Span::call_site())
		})
		.filter(|it| it.to_string() != "mod")
		.collect::<Vec<_>>();

	proc_macro::TokenStream::from(quote!(
		mod #module_name { #(pub mod #idents;)* }
		pub use #module_name::{
			#(#idents,)*
		};
	))
}