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
47pub 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 if entry.path().is_dir() {
61 continue;
62 }
63
64 let file_name = entry.file_name();
66
67 if file_name == "_middleware.rs" || file_name == "mod.rs" {
69 continue;
70 }
71
72 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 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 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 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 if handlers.len() < 1 {
185 return;
186 }
187
188 let routes = generate_routes(route_dir.to_str().unwrap());
189
190 let mut queries_ts = String::from("{");
192 let mut mutations_ts = String::from("{");
193
194 convert_all_types_in_path(type_generation_dir.to_str().unwrap(), &mut converter);
199
200 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 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 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 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 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 converter.generate(Some(GENERATED_TS_FILE_MESSAGE));
339 converter.generate(Some(&handlers_interface));
340 converter.generate(Some(&routes));
341
342 converter.generate(None);
344}
345
346pub 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 if entry.path().is_dir() {
363 continue;
364 }
365
366 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 if !validate_route_handler(&route_file_contents) {
373 continue;
374 }
375
376 let file_name = entry.file_name();
377
378 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 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 route.push_str(&format!("\t\turl: '{url}',\n", url = route_key.value));
405 route.push_str(&format!("\t\ttype: '{route_type}',\n", route_type = handler_type));
407 route.push_str("\t},");
409 typescript_object.push_str(&route);
410 }
411
412 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 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 }
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