rapid_web/shift/
generate.rs

1use super::{
2	convert::{convert_all_types_in_path, TypescriptConverter, TypescriptType},
3	util::{
4		extract_handler_types, get_handler_type, get_output_type_alias, get_route_key, is_dynamic_route, remove_last_occurrence, space,
5		HandlerRequestType, TypeClass, GENERATED_TS_FILE_MESSAGE,
6	},
7};
8use crate::util::validate_route_handler;
9use std::{
10	fs::{File, OpenOptions},
11	io::prelude::*,
12	path::PathBuf,
13};
14use walkdir::WalkDir;
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum Handler {
18	Query(TypedQueryHandler),
19	Mutation(TypedMutationHandler),
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct RouteKey {
24	pub key: String,
25	pub value: String,
26}
27
28#[derive(Debug, Clone, PartialEq)]
29pub struct TypedQueryHandler {
30	pub request_type: HandlerRequestType,
31	pub path: Option<TypescriptType>,
32	pub query_params: Option<TypescriptType>,
33	pub output_type: TypescriptType,
34	pub route_key: RouteKey,
35}
36
37#[derive(Debug, Clone, PartialEq)]
38pub struct TypedMutationHandler {
39	pub request_type: HandlerRequestType,
40	pub query_params: Option<TypescriptType>,
41	pub path: Option<TypescriptType>,
42	pub input_type: Option<TypescriptType>,
43	pub output_type: TypescriptType,
44	pub route_key: RouteKey,
45}
46
47/// Function for generating typescript types from a rapid routes directory
48pub fn generate_handler_types(routes_path: PathBuf, converter: &mut TypescriptConverter) -> Vec<Handler> {
49	let mut handlers: Vec<Handler> = Vec::new();
50
51	let routes_dir = routes_path;
52
53	for route_file in WalkDir::new(routes_dir.clone()) {
54		let entry = match route_file {
55			Ok(val) => val,
56			Err(e) => panic!("An error occurred what attempting to parse directory: {}", e),
57		};
58
59		// We only want to handle route files and no directories (the walkDir crate auto iterates through nested dirs)
60		if entry.path().is_dir() {
61			continue;
62		}
63
64		// We also want to make sure that we exit if we find a mod.rs file or a middleware file
65		let file_name = entry.file_name();
66
67		// Make sure we ignore middleware and mod files from route handler generation
68		if file_name == "_middleware.rs" || file_name == "mod.rs" {
69			continue;
70		}
71
72		// Create a reference to the current route file and grab its contents as a string
73		let mut file = File::open(&entry.path()).unwrap();
74		let mut route_file_contents = String::new();
75		file.read_to_string(&mut route_file_contents).unwrap();
76
77		// We also want to exit early if the route is invalid
78		if !validate_route_handler(&route_file_contents) {
79			continue;
80		}
81
82		let parsed_route_dir = entry
83			.path()
84			.to_str()
85			.unwrap_or("/")
86			.to_string()
87			.replace(routes_dir.to_str().unwrap_or("src/routes"), "")
88			.replace(".rs", "");
89
90		let route_key = RouteKey {
91			key: get_route_key(parsed_route_dir.clone(), &route_file_contents),
92			value: parsed_route_dir,
93		};
94
95		let handler_types = match extract_handler_types(&route_file_contents) {
96			Some(val) => {
97				if val.len() > 0 {
98					val
99				} else {
100					continue;
101				}
102			}
103			None => continue,
104		};
105
106		let mut query_params: Option<TypescriptType> = None;
107		let mut body_type: Option<TypescriptType> = None;
108		let mut path: Option<TypescriptType> = None;
109		let output_type: TypescriptType = get_output_type_alias(&route_file_contents);
110		let request_type = handler_types[0].as_ref().unwrap().handler_type.clone();
111
112		for typed in handler_types {
113			let rust_type = match typed {
114				Some(val) => val,
115				None => continue,
116			};
117
118			let converted_type = match rust_type.type_value {
119				Some(val) => converter.convert_primitive(val),
120				None => TypescriptType {
121					typescript_type: String::from("any"),
122					is_optional: false,
123				},
124			};
125
126			match rust_type.class {
127				Some(TypeClass::InputBody) => body_type = Some(converted_type),
128				Some(TypeClass::QueryParam) => query_params = Some(converted_type),
129				Some(TypeClass::Path) => path = Some(converted_type),
130				_ => continue,
131			}
132		}
133
134		match request_type {
135			HandlerRequestType::Get => {
136				handlers.push(Handler::Query(TypedQueryHandler {
137					request_type,
138					path,
139					query_params,
140					output_type,
141					route_key,
142				}));
143			}
144			HandlerRequestType::Query => {
145				handlers.push(Handler::Query(TypedQueryHandler {
146					request_type,
147					path,
148					query_params,
149					output_type,
150					route_key,
151				}));
152			}
153			_ => {
154				handlers.push(Handler::Mutation(TypedMutationHandler {
155					request_type,
156					query_params,
157					path,
158					input_type: body_type,
159					output_type,
160					route_key,
161				}));
162			}
163		}
164	}
165
166	handlers
167}
168
169pub fn create_typescript_types(out_dir: PathBuf, route_dir: PathBuf, type_generation_dir: PathBuf) {
170	// Create a new bindings.ts file to store all of our generated types
171	let file = OpenOptions::new()
172		.write(true)
173		.create(true)
174		.truncate(true)
175		.open(format!("{}/bindings.ts", out_dir.as_os_str().to_str().unwrap()))
176		.unwrap();
177
178	// Init our typescript converter
179	let mut converter = TypescriptConverter::new(true, "".to_string(), true, 4, file);
180
181	let handlers = generate_handler_types(route_dir.clone(), &mut converter);
182
183	// Early exit without doing anything if we did not detect any handlers
184	if handlers.len() < 1 {
185		return;
186	}
187
188	let routes = generate_routes(route_dir.to_str().unwrap());
189
190	// Init our a queries and mutations keys in the handlers interface
191	let mut queries_ts = String::from("{");
192	let mut mutations_ts = String::from("{");
193
194	// TODO: convert every type in the entire project to a typescript type
195	// Make sure that we skip the routes directory when converting types
196
197	// Convert every type in project to a typescript type (this is so that any used types in the route handlers generated above do not error out)
198	convert_all_types_in_path(type_generation_dir.to_str().unwrap(), &mut converter);
199
200	// Loop through every handler and generate the typescript type for it
201	for handler in handlers {
202		match handler {
203			Handler::Query(query) => {
204				let mut ts_type = format!("\n\t\t{}: {{\n", query.route_key.key);
205				let route_path = query.route_key.value;
206				let is_dynamic_route_path = is_dynamic_route(&route_path);
207
208				let spacing = space(2);
209				let request_type = match query.request_type {
210					HandlerRequestType::Post => "post",
211					HandlerRequestType::Put => "put",
212					HandlerRequestType::Delete => "delete",
213					HandlerRequestType::Get => "get",
214					HandlerRequestType::Patch => "patch",
215					HandlerRequestType::Query => "query",
216					HandlerRequestType::Mutation => "mutation",
217				};
218
219				if let Some(query_params_type) = query.query_params {
220					let query_type = query_params_type.typescript_type;
221					if converter.converted_types.contains(&query_type) {
222						let query_params = format!("\t\t\tquery_params: {}", query_type);
223						ts_type.push_str(&format!("{}{}\n", spacing, query_params));
224					} else {
225						let query_params = format!("\t\t\tquery_params: {}", "any");
226						ts_type.push_str(&format!("{}{}\n", spacing, query_params));
227					}
228				}
229
230				if let Some(dynamic_path_type) = query.path {
231					let path_type = dynamic_path_type.typescript_type;
232					// If we found a path then that means this route handler is dynamic so lets also push a isDynamic type to the handler schema
233					if converter.converted_types.contains(&path_type) {
234						let path = format!("\t\t\tpath: {}", path_type);
235						ts_type.push_str(&format!("{}{}\n", spacing, path));
236					} else {
237						let path = format!("\t\t\tpath: {}", "any");
238						ts_type.push_str(&format!("{}{}\n", spacing, path));
239					}
240				}
241
242				let output_body = format!("\t\t\toutput: {}", query.output_type.typescript_type);
243				ts_type.push_str(&format!("{}{}\n", spacing, output_body));
244
245				let request_type = format!("\t\t\ttype: '{}'", request_type);
246				ts_type.push_str(&format!("{}{}\n", spacing, request_type));
247
248				let dynamic_type = format!("\t\t\tisDynamic: {}", is_dynamic_route_path);
249				ts_type.push_str(&format!("{}{}\n", spacing, dynamic_type));
250
251				ts_type.push_str(&format!("\t\t}},\n"));
252
253				queries_ts.push_str(&ts_type);
254			}
255			Handler::Mutation(mutation) => {
256				let mut ts_type = format!("\n\t\t{}: {{\n", mutation.route_key.key);
257				let route_path = mutation.route_key.value;
258				let spacing = space(2);
259				let is_dynamic_route_path = is_dynamic_route(&route_path);
260				let request_type = match mutation.request_type {
261					HandlerRequestType::Post => "post",
262					HandlerRequestType::Put => "put",
263					HandlerRequestType::Delete => "delete",
264					HandlerRequestType::Get => "get",
265					HandlerRequestType::Patch => "patch",
266					HandlerRequestType::Query => "query",
267					HandlerRequestType::Mutation => "mutation",
268				};
269
270				if let Some(query_params_type) = mutation.query_params {
271					// We only want to add query params if the TS type has already been generated
272					let query_type = query_params_type.typescript_type;
273					if converter.converted_types.contains(&query_type) {
274						let query_params = format!("\t\t\tquery_params: {}", query_type);
275						ts_type.push_str(&format!("{}{}\n", spacing, query_params));
276					} else {
277						let query_params = format!("\t\t\tquery_params: {}", "any");
278						ts_type.push_str(&format!("{}{}\n", spacing, query_params));
279					}
280				}
281
282				if let Some(dynamic_path_type) = mutation.path {
283					let path_type = dynamic_path_type.typescript_type;
284					if converter.converted_types.contains(&path_type) {
285						let path = format!("\t\t\tpath: {}", path_type);
286						ts_type.push_str(&format!("{}{}\n", spacing, path));
287					} else {
288						let path = format!("\t\t\tpath: {}", "any");
289						ts_type.push_str(&format!("{}{}\n", spacing, path));
290					}
291				}
292
293				if let Some(input_body_type) = mutation.input_type {
294					let input_body = input_body_type.typescript_type;
295					if converter.converted_types.contains(&input_body) {
296						let body = format!("\t\t\tinput: {}", input_body);
297						ts_type.push_str(&format!("{}{}\n", spacing, body));
298					} else {
299						let body = format!("\t\t\tinput: {}", "any");
300						ts_type.push_str(&format!("{}{}\n", spacing, body));
301					}
302				}
303
304				// Output body defaults to any (we currently do not support typesafe output types)
305				// TODO: when we support output types, lets change this!
306				let output_body = format!("\t\t\toutput: {}", mutation.output_type.typescript_type);
307				ts_type.push_str(&format!("{}{}\n", spacing, output_body));
308
309				let request_type = format!("\t\t\ttype: '{}'", request_type);
310				ts_type.push_str(&format!("{}{}\n", spacing, request_type));
311
312				let dynamic_type = format!("\t\t\tisDynamic: {}", is_dynamic_route_path);
313				ts_type.push_str(&format!("{}{}\n", spacing, dynamic_type));
314
315				ts_type.push_str(&format!("\t\t}}\n"));
316
317				mutations_ts.push_str(&ts_type);
318			}
319		}
320	}
321
322	queries_ts.push_str("\t},");
323
324	// Only have tabs when types are present
325	if mutations_ts.len() < 2 {
326		mutations_ts.push_str("},");
327	} else {
328		mutations_ts.push_str("\t},");
329	}
330
331	let mut handlers_interface = format!("\n\nexport interface Handlers {{\n");
332
333	handlers_interface.push_str(&format!("\tqueries: {}\n", queries_ts));
334	handlers_interface.push_str(&format!("\tmutations: {}\n", mutations_ts));
335	handlers_interface.push_str("}");
336
337	// Add all the route handler types for mutations and queries
338	converter.generate(Some(GENERATED_TS_FILE_MESSAGE));
339	converter.generate(Some(&handlers_interface));
340	converter.generate(Some(&routes));
341
342	// Write the new types to the bindings file
343	converter.generate(None);
344}
345
346/// Function for generating a route handler object for use on the client
347/// It will output something similar to the following:
348/// const routes = {
349/// 	index: '/'
350/// }
351///
352pub fn generate_routes(routes_dir: &str) -> String {
353	let mut typescript_object = String::from("\n\nexport const routes = {");
354
355	for route_file in WalkDir::new(routes_dir.clone()) {
356		let entry = match route_file {
357			Ok(val) => val,
358			Err(e) => panic!("An error occurred what attempting to parse directory: {}", e),
359		};
360
361		// We only want to handle route files and no directories (the walkDir crate auto iterates through nested dirs)
362		if entry.path().is_dir() {
363			continue;
364		}
365
366		// Create a reference to the current route file and grab its contents as a string
367		let mut file = File::open(&entry.path()).unwrap();
368		let mut route_file_contents = String::new();
369		file.read_to_string(&mut route_file_contents).unwrap();
370
371		// We also want to exit early if the route is invalid
372		if !validate_route_handler(&route_file_contents) {
373			continue;
374		}
375
376		let file_name = entry.file_name();
377
378		// Make sure we ignore middleware and mod files from route generation
379		if file_name == "_middleware.rs" || file_name == "mod.rs" {
380			continue;
381		}
382
383		let parsed_route_dir = entry
384			.path()
385			.to_str()
386			.unwrap_or("/")
387			.to_string()
388			.replace(routes_dir, "")
389			.replace(".rs", "");
390
391		let handler_type = match get_handler_type(&route_file_contents) {
392			Some(name) => name,
393			None => String::from("get"),
394		};
395
396		// Construct our routes object
397		let route_key = RouteKey {
398			key: get_route_key(parsed_route_dir.clone(), &route_file_contents),
399			value: remove_last_occurrence(&parsed_route_dir, "index"),
400		};
401
402		let mut route = format!("\n\t{}: {{\n", route_key.key);
403		// Add the url
404		route.push_str(&format!("\t\turl: '{url}',\n", url = route_key.value));
405		// Add the route type
406		route.push_str(&format!("\t\ttype: '{route_type}',\n", route_type = handler_type));
407		// Make sure we close off the new route...
408		route.push_str("\t},");
409		typescript_object.push_str(&route);
410	}
411
412	// Once we are done we want to close off the object
413	typescript_object.push_str("\n} as const");
414
415	typescript_object
416}
417
418#[cfg(test)]
419mod tests {
420	use super::*;
421
422	#[test]
423	fn test_generate_handler_types() {
424		let out_dir = PathBuf::from("tests/mocks/temp");
425		let bindings_file = OpenOptions::new()
426		.write(true)
427		.create(true)
428		.truncate(true)
429		.open(format!("{}/bindings.ts", out_dir.as_os_str().to_str().unwrap()))
430		.unwrap();
431		let routes = generate_handler_types(PathBuf::from("tests/mocks/files"), &mut TypescriptConverter::new(true, "".to_string(), true, 4, bindings_file));
432
433		let mut expected_handlers: Vec<Handler> = Vec::new();
434
435		let hello_handler = Handler::Query(TypedQueryHandler {
436			request_type: HandlerRequestType::Query,
437			path: None,
438			query_params: None,
439			output_type: TypescriptType {
440				typescript_type: String::from("any"),
441				is_optional: false,
442			},
443			route_key: RouteKey {
444				key: String::from("hello"),
445				value: String::from("/hello"),
446			},
447		});
448
449		let mutation_handler = Handler::Mutation(TypedMutationHandler {
450			request_type: HandlerRequestType::Mutation,
451			query_params: None,
452			path: None,
453			input_type: None,
454			output_type: TypescriptType {
455				typescript_type: String::from("any"),
456				is_optional: false,
457			},
458			route_key: RouteKey {
459				key: String::from("mutation"),
460				value: String::from("/mutation"),
461			},
462		});
463
464		expected_handlers.push(mutation_handler);
465		expected_handlers.push(hello_handler);
466
467		assert_eq!(routes, expected_handlers);
468	}
469
470	#[test]
471	fn test_create_typescript_types() {
472		let out_dir = PathBuf::from("tests/mocks/temp");
473		let route_dir = PathBuf::from("tests/mocks/files");
474		let type_generation_dir = PathBuf::from("tests/mocks/files");
475
476		create_typescript_types(out_dir, route_dir, type_generation_dir);
477
478		let mut file = File::open("tests/mocks/temp/bindings.ts").unwrap();
479		let mut contents = String::new();
480		file.read_to_string(&mut contents).unwrap();
481
482		// Delete the temp file
483		std::fs::remove_file("tests/mocks/temp/bindings.ts").unwrap();
484
485		const _: &str = "// @generated automatically by Rapid-web (https://rapid.cincinnati.ventures). DO NOT CHANGE OR EDIT THIS FILE!
486
487export interface Handlers {
488	queries: {
489		hello: {
490				output: any
491				type: 'query'
492				isDynamic: false
493		},
494	},
495	mutations: {
496		mutation: {
497				output: any
498				type: 'mutation'
499				isDynamic: false
500		}
501	},
502}
503
504export const routes = {
505	mutation: {
506		url: '/mutation',
507		type: 'mutation',
508	},
509	hello: {
510		url: '/hello',
511		type: 'query',
512	},
513} as const
514";
515
516		// TODO: fix this test - has some issues with writing to files
517	}
518
519	#[test]
520	fn test_generate_routes() {
521		let routes = generate_routes("tests/mocks/files");
522		const EXPECTED: &str = "\n\nexport const routes = {\n\tmutation: {\n\t\turl: '/mutation',\n\t\ttype: 'mutation',\n\t},\n\thello: {\n\t\turl: '/hello',\n\t\ttype: 'query',\n\t},\n} as const";
523
524		assert_eq!(routes, EXPECTED);
525	}
526}
527
528