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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#![feature(proc_macro_quote)]
mod utils;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{quote, format_ident};
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,
	is_nested: bool,
}

// 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![];

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

	// Get every nested dir and append them to the route_dirs aray
	get_all_dirs(parsed_path, &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<_>>();


	// 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("/"),
			is_nested: false
		};

		// 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))
		}
	}

	for nested_file_path in route_dirs {
		// Get all the files from the base specified path
		let route_files = read_dir(&nested_file_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 cleaned_route_path = base_file_name(&nested_file_path);

		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();
			// Get the files contents...
			file.read_to_string(&mut file_contents).unwrap();

			// Construct our handler
			let handler = Handler {
				name: file_name,
				path: cleaned_route_path.clone(),
				is_nested: true
			};

			// 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 = {
					if route_handler.is_nested {
						format!("{}/{}", route_handler.path, route_handler.name)
					} else {
						format!("{}{}", route_handler.path, route_handler.name)
					}
				};
				quote!(.route(#path, web::get().to(#handler::get)))
			}
			RouteHandler::Post(route_handler) => {
				let handler = Ident::new(&route_handler.name, Span::call_site());
				let path = {
					if route_handler.is_nested {
						format!("{}/{}", route_handler.path, route_handler.name)
					} else {
						format!("{}{}", route_handler.path, route_handler.name)
					}
				};
				quote!(.route(#path, web::post().to(#handler::post)))
			}
			RouteHandler::Delete(route_handler) => {
				let handler = Ident::new(&route_handler.name, Span::call_site());
				let path = {
					if route_handler.is_nested {
						format!("{}/{}", route_handler.path, route_handler.name)
					} else {
						format!("{}{}", route_handler.path, route_handler.name)
					}
				};
				quote!(.route(#path, web::delete().to(#handler::delete)))
			}
			RouteHandler::Put(route_handler) => {
				let handler = Ident::new(&route_handler.name, Span::call_site());
				let path = {
					if route_handler.is_nested {
						format!("{}/{}", route_handler.path, route_handler.name)
					} else {
						format!("{}{}", route_handler.path, route_handler.name)
					}
				};
				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 handler
#[proc_macro]
pub fn rapid_configure(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	let path_string = if let proc_macro::TokenTree::Literal(literal) = item.into_iter().next().unwrap() {
		literal.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 mut route_dirs: Vec<PathBuf> = vec![];

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

	let base_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<_>>();


	let mut nested_idents: Vec<TokenStream2> = Vec::new();

	for dir in route_dirs {
		let string = dir.into_os_string().into_string().unwrap();

		let mod_name = format!("{}", string.replace("src/", "").replace("/", "::"));
		let tokens: proc_macro2::TokenStream = mod_name.parse().unwrap();
		nested_idents.push(quote! { pub use #tokens::*; });
	}

	proc_macro::TokenStream::from(quote!(
		use include_dir::{include_dir, Dir};
		mod #module_name { #(pub mod #base_idents;)* }
		pub use #module_name::{
			#(#base_idents,)*
		};
		#(#nested_idents)*
		const ROUTES_DIR: Dir = include_dir!(#path); // Including the entire routes dir here is what provides the "hot-reload" effec to the config macro
	))
}