rapid_web/shift/
convert.rs

1use super::util::{get_struct_generics, indent, space};
2use log::error;
3use std::{env::current_dir, ffi::OsStr, fs::File, io::prelude::*};
4use syn::{parse_file, Item, ItemStruct, ItemType, Type};
5use walkdir::WalkDir;
6
7#[derive(PartialEq, Debug, Clone)]
8pub struct TypescriptType {
9	pub typescript_type: String,
10	pub is_optional: bool,
11}
12
13impl TypescriptType {
14	pub fn new(typescript_type: String, is_optional: bool) -> Self {
15		Self {
16			typescript_type,
17			is_optional,
18		}
19	}
20}
21
22impl From<String> for TypescriptType {
23	fn from(typescript_type: String) -> TypescriptType {
24		TypescriptType {
25			typescript_type,
26			is_optional: false,
27		}
28	}
29}
30
31/// Function for checking if a type is generic and converting the generic type to a typescript type
32fn convert_generic_type(generic_type: &syn::GenericArgument) -> TypescriptType {
33	match generic_type {
34		syn::GenericArgument::Type(rust_type) => convert_primitive(rust_type),
35		_ => "any".to_string().into(), // We could use "unknown" here but "any" is fine for now
36	}
37}
38
39/// Function for converting basic rust primitive types to typescript types and interfaces
40pub fn convert_primitive(rust_primitive: &Type) -> TypescriptType {
41	match rust_primitive {
42		Type::Tuple(path) => {
43			let mut tuple_types: Vec<String> = Vec::new();
44			for field in path.elems.iter() {
45				let converted_primitive = convert_primitive(field);
46				tuple_types.push(converted_primitive.typescript_type);
47			}
48
49			TypescriptType {
50				typescript_type: format!("{:?}", tuple_types).replace(r#"""#, ""),
51				is_optional: false,
52			}
53		}
54		// If we find a reference type we want to extract it and re-call type conversion
55		Type::Reference(path) => convert_primitive(&path.elem),
56		Type::Path(path) => {
57			let segment = path.path.segments.last().unwrap();
58			let tokens = &segment.ident;
59			let arguments = &segment.arguments;
60			let parsed_type = tokens.to_string();
61			// TODO: Add cases here for chrono dates as well
62			match parsed_type.as_str() {
63				"u8" => TypescriptType::new("number".to_string(), false),
64				"u16" => TypescriptType::new("number".to_string(), false),
65				"i64" => TypescriptType::new("number".to_string(), false),
66				"u64" => TypescriptType::new("number".to_string(), false),
67				"u128" => TypescriptType::new("number".to_string(), false),
68				"i8" => TypescriptType::new("number".to_string(), false),
69				"i16" => TypescriptType::new("number".to_string(), false),
70				"i32" => TypescriptType::new("number".to_string(), false),
71				"u32" => TypescriptType::new("number".to_string(), false),
72				"i128" => TypescriptType::new("number".to_string(), false),
73				"f32" => TypescriptType::new("number".to_string(), false),
74				"f64" => TypescriptType::new("number".to_string(), false),
75				"isize" => TypescriptType::new("number".to_string(), false),
76				"usize" => TypescriptType::new("number".to_string(), false),
77				"bool" => TypescriptType::new("boolean".to_string(), false),
78				"char" => TypescriptType::new("string".to_string(), false),
79				"str" => TypescriptType::new("string".to_string(), false),
80				"String" => TypescriptType::new("string".to_string(), false),
81				"Value" => TypescriptType::new("any".to_string(), false),
82				"NaiveDateTime" => TypescriptType::new("Date".to_string(), false),
83				"DateTime" => TypescriptType::new("Date".to_string(), false),
84				"Uuid" => TypescriptType::new("string".to_string(), false),
85				"RapidPath" => TypescriptType {
86					is_optional: false,
87					typescript_type: match arguments {
88						syn::PathArguments::Parenthesized(parenthesized_argument) => {
89							format!("{:?}", parenthesized_argument)
90						}
91						syn::PathArguments::AngleBracketed(anglebracketed_argument) => {
92							convert_generic_type(anglebracketed_argument.args.first().unwrap()).typescript_type
93						}
94						_ => "unknown".to_string(),
95					},
96				},
97				"RapidJson" => TypescriptType {
98					is_optional: false,
99					typescript_type: match arguments {
100						syn::PathArguments::Parenthesized(parenthesized_argument) => {
101							format!("{:?}", parenthesized_argument)
102						}
103						syn::PathArguments::AngleBracketed(anglebracketed_argument) => {
104							convert_generic_type(anglebracketed_argument.args.first().unwrap()).typescript_type
105						}
106						_ => "unknown".to_string(),
107					},
108				},
109				"Union" => TypescriptType {
110					is_optional: false,
111					typescript_type: match arguments {
112						syn::PathArguments::AngleBracketed(anglebracketed_argument) => {
113							let mut converted_types: Vec<TypescriptType> = Vec::new();
114							for generic_type in anglebracketed_argument.args.iter() {
115								converted_types.push(convert_generic_type(generic_type));
116							}
117
118							// A `Union` type will only ever have two generic types (per `/shift/types.rs`)
119							format!("{} | {}", converted_types[0].typescript_type, converted_types[1].typescript_type)
120						}
121						_ => "unknown".to_string(),
122					},
123				},
124				"Option" => TypescriptType {
125					is_optional: true,
126					typescript_type: match arguments {
127						syn::PathArguments::Parenthesized(parenthesized_argument) => {
128							format!("{:?}", parenthesized_argument)
129						}
130						syn::PathArguments::AngleBracketed(anglebracketed_argument) => {
131							convert_generic_type(anglebracketed_argument.args.first().unwrap()).typescript_type
132						}
133						_ => "unknown".to_string(),
134					},
135				},
136				"Vec" => match arguments {
137					syn::PathArguments::Parenthesized(parenthesized_argument) => {
138						format!("{:?}", parenthesized_argument)
139					}
140					syn::PathArguments::AngleBracketed(anglebracketed_argument) => format!(
141						"Array<{}>",
142						match convert_generic_type(anglebracketed_argument.args.first().unwrap()) {
143							TypescriptType {
144								is_optional: true,
145								typescript_type,
146							} => format!("{} | undefined", typescript_type),
147							TypescriptType {
148								is_optional: false,
149								typescript_type,
150							} => typescript_type,
151						}
152					),
153					_ => "unknown".to_string(),
154				}
155				.into(),
156				"HashMap" => match arguments {
157					syn::PathArguments::Parenthesized(parenthesized_argument) => {
158						format!("{:?}", parenthesized_argument)
159					}
160					syn::PathArguments::AngleBracketed(anglebracketed_argument) => format!(
161						"Record<{}>",
162						anglebracketed_argument
163							.args
164							.iter()
165							.map(|arg| match convert_generic_type(arg) {
166								TypescriptType {
167									is_optional: true,
168									typescript_type,
169								} => format!("{} | undefined", typescript_type),
170								TypescriptType {
171									is_optional: false,
172									typescript_type,
173								} => typescript_type,
174							})
175							.collect::<Vec<String>>()
176							.join(", ")
177					),
178					_ => "any".to_string(),
179				}
180				.into(),
181				// By default just convert the type to a string
182				_ => parsed_type.to_string().into(),
183			}
184		}
185		_ => "any".to_string().into(),
186	}
187}
188
189/// Function for getting the type name as a string from a rust struct
190pub fn get_rust_typename(rust_type: &Type) -> String {
191	match rust_type {
192		Type::Reference(path) => get_rust_typename(&path.elem),
193		Type::Path(path) => {
194			let segment = path.path.segments.last().unwrap();
195			let tokens = &segment.ident;
196			let parsed_type = tokens.to_string();
197			let arguments = &segment.arguments;
198			match parsed_type.as_str() {
199				"RapidPath" => match arguments {
200					syn::PathArguments::AngleBracketed(anglebracketed_argument) => match anglebracketed_argument.args.first().unwrap() {
201						syn::GenericArgument::Type(rust_type) => get_rust_typename(rust_type),
202						_ => "unknown".to_string(),
203					},
204					_ => "unknown".to_string(),
205				},
206				"RapidJson" => match arguments {
207					syn::PathArguments::AngleBracketed(anglebracketed_argument) => match anglebracketed_argument.args.first().unwrap() {
208						syn::GenericArgument::Type(rust_type) => get_rust_typename(rust_type),
209						_ => "unknown".to_string(),
210					},
211					_ => "unknown".to_string(),
212				},
213				_ => parsed_type,
214			}
215		}
216		_ => String::from("any"),
217	}
218}
219
220// TODO: support `Function`, `TypeAlias`, `Enum`, and `Const`
221#[allow(dead_code)]
222pub enum ConversionType {
223	Primitive,
224	Struct,
225	Const,
226	Enum,
227	Function,  // Coming soon
228	TypeAlias, // Coming soon
229}
230
231/// Provides ability to convert syn (https://crates.io/crates/syn) parser types to typescript types with ease
232pub struct TypescriptConverter {
233	pub is_interface: bool,
234	pub store: String,
235	pub should_export: bool,
236	pub indentation: u32,
237	pub file: File,
238	pub converted_types: Vec<String>,
239}
240
241impl TypescriptConverter {
242	pub fn new(is_interface: bool, initial_store_value: String, should_export: bool, indentation: u32, file: File) -> Self {
243		Self {
244			is_interface,
245			store: initial_store_value,
246			should_export,
247			indentation,
248			file,
249			converted_types: Vec::new(),
250		}
251	}
252
253	/// Function that converts a syn struct to a typescript interface and pushes it to the `store` field
254	pub fn convert_struct(&mut self, rust_struct: ItemStruct) {
255		let export_str = if self.should_export { "export " } else { "" };
256
257		let keyword = if self.is_interface { "interface" } else { "type" };
258
259		let spacing = space(self.indentation);
260
261		// Push an indentation to the typescript file
262		self.store.push_str(&indent(2));
263
264		let type_scaffold = format!(
265			"{export}{key} {name}{generics} {{\n",
266			export = export_str,
267			key = keyword,
268			name = rust_struct.ident,
269			generics = get_struct_generics(rust_struct.generics.clone())
270		);
271
272		// Push our type scaffold to the store string (this string will eventually be pushed to a file once formed fully)
273		self.store.push_str(&type_scaffold);
274
275		// Parse all of the structs fields
276		for field in rust_struct.fields {
277			let field_name = field.ident.unwrap().to_string();
278			let field_type = convert_primitive(&field.ty);
279			let optional_marking = if field_type.is_optional { "?" } else { "" };
280
281			// For each rust struct field we want to form a valid typescript field and add that field to the typescript type/interface
282			self.store.push_str(&format!(
283				"{space}{name}{optional}: {ts_type};\n",
284				space = spacing,
285				name = field_name,
286				ts_type = field_type.typescript_type,
287				optional = optional_marking
288			));
289		}
290
291		// Close out our newly generated interface/type
292		self.store.push_str("}");
293
294		// Now we want to update the converted types array with the name of the newrly created type
295		self.converted_types.push(rust_struct.ident.to_string());
296	}
297
298	/// Converts rust primitives to typescript types
299	pub fn convert_primitive(&mut self, primitive: Type) -> TypescriptType {
300		let converted = convert_primitive(&primitive);
301		// We only want to update the converted types if the old typename is not the same as the newly converted type
302		if !(get_rust_typename(&primitive) == converted.typescript_type) {
303			self.converted_types.push(converted.clone().typescript_type);
304		}
305		converted
306	}
307
308	// TODO: we should also convert constants to typescript aliases as well
309	#[allow(dead_code)]
310	pub fn convert_const() {}
311
312	// TODO: enums in typescript suck but might be ideal to atleast support conversion
313	#[allow(dead_code)]
314	pub fn convert_enum() {}
315
316	// TODO: support converting all functions to typescript types
317	#[allow(dead_code)]
318	pub fn convert_function() {}
319
320	/// Converts rust type aliases to typescript types or interfaces
321	pub fn convert_type_alias(&mut self, rust_type_alias: ItemType) {
322		let export_str = if self.should_export { "export " } else { "" };
323
324		// Set our type keyword (it defaults to "type" because it does not make sense to use interfaces here)
325		let keyword = "type";
326
327		// Push an indentation to the typescript file
328		self.store.push_str(&indent(2));
329
330		// Get the name of the type alias
331		let alias_name = rust_type_alias.ident.to_string();
332
333		let converted_type = convert_primitive(&rust_type_alias.ty);
334
335		// Scaffold our new type
336		let type_scaffold = format!(
337			"{export}{key} {name} = {type_value};",
338			key = keyword,
339			export = export_str,
340			name = alias_name,
341			type_value = converted_type.typescript_type
342		);
343
344		// After constructing the new type lets push it onto the store string
345		self.store.push_str(&type_scaffold);
346
347		// Now we want to update the converted types array with the name of the newly created type
348		self.converted_types.push(alias_name);
349	}
350
351	pub fn generate(&mut self, types: Option<&str>) {
352		match types {
353			Some(val) => {
354				self.file.write_all(val.as_bytes()).expect("Could not write to typescript bindings file!");
355			}
356			None => {
357				self.file
358					.write_all(self.store.as_bytes())
359					.expect("Could not write to typescript bindings file!");
360			}
361		}
362	}
363}
364
365/// Function for generating typescript bindings for every rust type in every file in a given rapid project
366pub fn convert_all_types_in_path(directory: &str, converter_instance: &mut TypescriptConverter) {
367	// Get the directory that we will be parsing
368	let parsing_directory = current_dir().unwrap().join(directory);
369
370	for entry in WalkDir::new(&parsing_directory).sort_by_file_name() {
371		match entry {
372			Ok(dir_entry) => {
373				let path = dir_entry.path();
374
375				// We want to break out if we found a directory
376				if path.is_dir() {
377					continue;
378				}
379
380				// We are only interested in rust files (break out if we do not find one)
381				if !(path.extension().unwrap_or(OsStr::new("")).to_str().unwrap_or("") == "rs") {
382					continue;
383				}
384
385				// Create a reference to the current route file and grab its contents as a string
386				let mut file = File::open(&path).expect("Could not open file while attempting to generate Typescript types!");
387				let mut file_contents = String::new();
388				file.read_to_string(&mut file_contents)
389					.expect("Could not open file while attempting to generate Typescript types!");
390
391				// Parse the file into a rust syntax tree
392				let file = parse_file(&file_contents).expect("Error: Syn could not parse handler source file!");
393
394				// Grab the routes file name
395				let file_name = dir_entry.file_name();
396
397				// We want to ignore all middleware files by default
398				if file_name == "_middleware.rs" || file_name == "mod.rs" {
399					continue;
400				}
401
402				// Go through the newly parsed file and look for types that we want to convert
403				// TODO: add more support here for other rust types (enums, constants, etc)
404				for item in file.items {
405					match item {
406						Item::Struct(val) => {
407							converter_instance.convert_struct(val);
408						}
409						Item::Type(val) => {
410							// We want to ignore all type aliases that have the name `RapidOutput`
411							if val.ident.to_string() == "RapidOutput" {
412								continue;
413							}
414							converter_instance.convert_type_alias(val)
415						}
416						_ => {
417							// If we found a rust item that we do not care about lets just continue
418							continue;
419						}
420					}
421				}
422			}
423			Err(_) => {
424				// if we were not able to parse the file lets error out
425				error!("An error occurred when attempting to parse directory with path: {:?}", parsing_directory);
426				continue;
427			}
428		}
429	}
430}
431
432
433#[cfg(test)]
434mod tests {
435	use super::*;
436
437	#[test]
438	fn test_convert_primitive() {
439		let mut converter = TypescriptConverter::new(false, "".to_string(), false, 0, File::create("test.ts").unwrap());
440		let converted = converter.convert_primitive(syn::parse_str::<Type>("u8").unwrap());
441		assert_eq!(converted.typescript_type, "number");
442		assert_eq!(converted.is_optional, false);
443
444		let converted = converter.convert_primitive(syn::parse_str::<Type>("u16").unwrap());
445		assert_eq!(converted.typescript_type, "number");
446		assert_eq!(converted.is_optional, false);
447
448		let converted = converter.convert_primitive(syn::parse_str::<Type>("i64").unwrap());
449		assert_eq!(converted.typescript_type, "number");
450		assert_eq!(converted.is_optional, false);
451
452		let converted = converter.convert_primitive(syn::parse_str::<Type>("u64").unwrap());
453		assert_eq!(converted.typescript_type, "number");
454		assert_eq!(converted.is_optional, false);
455
456		let converted = converter.convert_primitive(syn::parse_str::<Type>("u128").unwrap());
457		assert_eq!(converted.typescript_type, "number");
458		assert_eq!(converted.is_optional, false);
459
460		let converted = converter.convert_primitive(syn::parse_str::<Type>("i8").unwrap());
461		assert_eq!(converted.typescript_type, "number");
462		assert_eq!(converted.is_optional, false);
463
464		let converted = converter.convert_primitive(syn::parse_str::<Type>("i16").unwrap());
465		assert_eq!(converted.typescript_type, "number");
466		assert_eq!(converted.is_optional, false);
467
468		let converted = converter.convert_primitive(syn::parse_str::<Type>("i32").unwrap());
469		assert_eq!(converted.typescript_type, "number");
470		assert_eq!(converted.is_optional, false);
471
472		let converted = converter.convert_primitive(syn::parse_str::<Type>("u32").unwrap());
473		assert_eq!(converted.typescript_type, "number");
474		assert_eq!(converted.is_optional, false);
475
476		let converted = converter.convert_primitive(syn::parse_str::<Type>("i128").unwrap());
477		assert_eq!(converted.typescript_type, "number");
478		assert_eq!(converted.is_optional, false);
479
480		let converted = converter.convert_primitive(syn::parse_str::<Type>("f32").unwrap());
481		assert_eq!(converted.typescript_type, "number");
482
483		let converted = converter.convert_primitive(syn::parse_str::<Type>("f64").unwrap());
484		assert_eq!(converted.typescript_type, "number");
485
486		let converted = converter.convert_primitive(syn::parse_str::<Type>("isize").unwrap());
487		assert_eq!(converted.typescript_type, "number");
488
489		let converted = converter.convert_primitive(syn::parse_str::<Type>("usize").unwrap());
490
491		assert_eq!(converted.typescript_type, "number");
492
493		let converted = converter.convert_primitive(syn::parse_str::<Type>("bool").unwrap());
494		assert_eq!(converted.typescript_type, "boolean");
495
496		let converted = converter.convert_primitive(syn::parse_str::<Type>("char").unwrap());
497		assert_eq!(converted.typescript_type, "string");
498
499		let converted = converter.convert_primitive(syn::parse_str::<Type>("str").unwrap());
500		assert_eq!(converted.typescript_type, "string");
501
502		let converted = converter.convert_primitive(syn::parse_str::<Type>("String").unwrap());
503		assert_eq!(converted.typescript_type, "string");
504
505		let converted = converter.convert_primitive(syn::parse_str::<Type>("Value").unwrap());
506		assert_eq!(converted.typescript_type, "any");
507
508		let converted = converter.convert_primitive(syn::parse_str::<Type>("NaiveDateTime").unwrap());
509		assert_eq!(converted.typescript_type, "Date");
510
511		let converted = converter.convert_primitive(syn::parse_str::<Type>("DateTime").unwrap());
512		assert_eq!(converted.typescript_type, "Date");
513
514		let converted = converter.convert_primitive(syn::parse_str::<Type>("Uuid").unwrap());
515		assert_eq!(converted.typescript_type, "string");
516
517		let converted = converter.convert_primitive(syn::parse_str::<Type>("RapidPath<String>").unwrap());
518		assert_eq!(converted.typescript_type, "string");
519
520		let converted = converter.convert_primitive(syn::parse_str::<Type>("RapidJson<String>").unwrap());
521		assert_eq!(converted.typescript_type, "string");
522
523		let converted = converter.convert_primitive(syn::parse_str::<Type>("Union<String, u8>").unwrap());
524		assert_eq!(converted.typescript_type, "string | number");
525
526		let converted = converter.convert_primitive(syn::parse_str::<Type>("Option<String>").unwrap());
527		assert_eq!(converted.typescript_type, "string");
528
529		let converted = converter.convert_primitive(syn::parse_str::<Type>("Vec<String>").unwrap());
530		assert_eq!(converted.typescript_type, "Array<string>");
531
532		let converted = converter.convert_primitive(syn::parse_str::<Type>("HashMap<String, String>").unwrap());
533		assert_eq!(converted.typescript_type, "Record<string, string>");
534	}
535}