rapid_web/shift/
util.rs

1use super::convert::{convert_primitive, TypescriptType};
2use syn::{parse_file, Expr, File as SynFile, Generics, Item, Lit, Type};
3
4pub const GENERATED_TS_FILE_MESSAGE: &str =
5	"// @generated automatically by Rapid-web (https://rapid.cincinnati.ventures). DO NOT CHANGE OR EDIT THIS FILE!";
6
7#[derive(Debug)]
8pub enum TypeClass {
9	InputBody,
10	QueryParam,
11	Path,
12	Invalid,
13	Return,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum HandlerRequestType {
18	Get,
19	Post,
20	Delete,
21	Put,
22	Patch,
23	Query,
24	Mutation,
25}
26
27#[derive(Debug)]
28pub struct HandlerType {
29	pub type_value: Option<Type>,
30	pub class: Option<TypeClass>,
31	pub handler_type: HandlerRequestType,
32}
33
34pub fn extract_handler_types(route_source: &str) -> Option<Vec<Option<HandlerType>>> {
35	let parsed_file: SynFile = syn::parse_str(route_source).expect("Error: Syn could not parse handler source file!");
36	for item in parsed_file.items {
37		// The first route handler that we find we want to break out
38		// Any valid handler functions found after the first one are ignored (in rapid, only one handler is allowed per file)
39		if let Item::Fn(function) = item {
40			if is_valid_handler("rapid_handler", function.attrs) {
41				let mut function_types: Vec<Option<HandlerType>> = Vec::new();
42				let arg_types = function.sig.inputs.iter();
43				let function_name = function.sig.ident;
44
45				for type_value in arg_types {
46					if let syn::FnArg::Typed(typed) = type_value {
47						let rust_type = *typed.ty.clone();
48						let type_class = get_type_class(rust_type.clone());
49						function_types.push(Some(HandlerType {
50							type_value: Some(rust_type),
51							class: type_class,
52							handler_type: match function_name.to_string().as_str() {
53								"get" => HandlerRequestType::Get,
54								"post" => HandlerRequestType::Post,
55								"delete" => HandlerRequestType::Delete,
56								"put" => HandlerRequestType::Put,
57								"patch" => HandlerRequestType::Patch,
58								"query" => HandlerRequestType::Query,
59								"mutation" => HandlerRequestType::Mutation,
60								_ => HandlerRequestType::Get,
61							},
62						}));
63					}
64				}
65
66				function_types.push(Some(HandlerType {
67					type_value: None,
68					class: Some(TypeClass::Return),
69					handler_type: match function_name.to_string().as_str() {
70						"get" => HandlerRequestType::Get,
71						"post" => HandlerRequestType::Post,
72						"delete" => HandlerRequestType::Delete,
73						"put" => HandlerRequestType::Put,
74						"patch" => HandlerRequestType::Patch,
75						"query" => HandlerRequestType::Query,
76						"mutation" => HandlerRequestType::Mutation,
77						_ => HandlerRequestType::Get,
78					},
79				}));
80
81				return Some(function_types);
82			}
83		}
84	}
85
86	None
87}
88
89pub fn get_handler_type(route_source: &str) -> Option<String> {
90	let parsed_file: SynFile = syn::parse_str(route_source).expect("Error: Syn could not parse handler source file!");
91	for item in parsed_file.items {
92		// The first route handler that we find we want to break out
93		// Any valid handler functions found after the first one are ignored (in rapid, only one handler is allowed per file)
94		if let Item::Fn(function) = item {
95			if is_valid_handler("rapid_handler", function.attrs) {
96				let function_name = function.sig.ident.to_string();
97				return Some(function_name);
98			}
99		}
100	}
101
102	None
103}
104
105pub fn get_type_class(rust_type: Type) -> Option<TypeClass> {
106	match rust_type {
107		Type::Reference(path) => get_type_class(*path.elem),
108		Type::Path(path) => {
109			let segment = path.path.segments.last().unwrap();
110			let tokens = &segment.ident;
111
112			Some(match tokens.to_string().as_str() {
113				"RapidPath" => TypeClass::Path,
114				"RapidQuery" => TypeClass::QueryParam,
115				"RapidJson" => TypeClass::InputBody, // TODO: support return statements here as well (right now we are defaulting to invalid until implemented)
116				_ => TypeClass::Invalid,
117			})
118		}
119		_ => None,
120	}
121}
122
123/// Method for checking if a handler function is valid
124/// Handlers are only valid if they have a "#[rapid_handler]" macro on them
125pub fn is_valid_handler(macro_name: &str, attributes: Vec<syn::Attribute>) -> bool {
126	attributes
127		.iter()
128		.any(|attr| attr.path().segments.iter().any(|segment| segment.ident == macro_name))
129}
130
131/// Method for creating spacing for the generated typescript file by rapid
132pub fn space(space_amount: u32) -> String {
133	let mut space_string = "".to_string();
134	for _ in 0..space_amount {
135		space_string.push(' ');
136	}
137	space_string
138}
139
140/// Method for providing indentions within strings (mainly used in generated typescript file that is generated by rapid)
141pub fn indent(amount: u32) -> String {
142	let mut new_amount = String::new();
143
144	for _ in 0..amount {
145		new_amount.push('\n');
146	}
147	new_amount
148}
149
150/// Function for extracting generics from a rust struct
151pub fn get_struct_generics(type_generics: Generics) -> String {
152	let mut generic_params: Vec<String> = Vec::new();
153
154	for generic_param in type_generics.params {
155		if let syn::GenericParam::Type(rust_type) = generic_param {
156			generic_params.push(rust_type.ident.to_string());
157		}
158	}
159
160	if generic_params.is_empty() {
161		"".to_string()
162	} else {
163		format!("<{}>", generic_params.join(", "))
164	}
165}
166
167/// A function for getting the route key of a rapid route handler
168/// Handlers will have a route key that is always unique (we can never have duplicate route keys)
169/// If we find a handler with a ROUTE_KEY constant delcared in it we want to use it as the route key for the handler (This is the key that clients will use to request the route)
170pub fn get_route_key(file_path: String, handler_source: &str) -> String {
171	// Parse the file into a rust syntax tree
172	let file = parse_file(handler_source).expect("Error: Rapid compiler could not parse handler source file!");
173
174	// Generate a default route_key as a fallback (this is based on the file path)
175	let fallback_key = file_path.replacen("/", "", 1).replace("/", "_").replace(".rs", "");
176
177	// Look for a variable called "ROUTE_KEY"
178	for item in file.items {
179		match item {
180			Item::Const(item_const) => {
181				if item_const.ident.to_string() == "ROUTE_KEY" {
182					return match *item_const.expr {
183						Expr::Lit(item) => match item.lit {
184							Lit::Str(val) => {
185								let key = val.token().to_string();
186
187								if key == "index" {
188									panic!("Invalid route key: 'index' is a reserved route key for rapid-web");
189								}
190
191								key
192							}
193							_ => continue,
194						},
195						_ => continue,
196					};
197				}
198
199				continue;
200			}
201			_ => continue,
202		}
203	}
204
205	fallback_key
206}
207
208pub fn get_output_type_alias(handler_source: &str) -> TypescriptType {
209	// Parse the file into a rust syntax tree
210	let file = parse_file(handler_source).expect("Error: Rapid compiler could not parse handler source file!");
211
212	// Fallback to an `any` type if we can't find a type alias configured by the user
213	let fallback_alias = TypescriptType {
214		typescript_type: "any".to_string(),
215		is_optional: false,
216	};
217
218	for item in file.items {
219		if let Item::Type(item_type) = item {
220			// Check to make sure that the user named the type "Output"
221			// We do not want to support any other names (for now)
222			if item_type.ident.to_string() == "RapidOutput" {
223				let syn_type = *item_type.ty;
224				return convert_primitive(&syn_type);
225			}
226		}
227	}
228
229	return fallback_alias;
230}
231
232/// Function for checking if a rapid route path is dynamic (example: "/route/todo/_id_" -- this route is dynamic because it has a "_id_" substring)
233pub fn is_dynamic_route(str: &str) -> bool {
234	// Check for a regex match for a substring similar to "_anythingInHere_"
235	let regex = regex::Regex::new(r"_.*?_").unwrap();
236	regex.is_match(str)
237}
238
239/// Removes the last occurrence of a substring in a string
240pub fn remove_last_occurrence(s: &str, sub: &str) -> String {
241	let mut split = s.rsplitn(2, sub);
242	let back = split.next().unwrap_or("");
243	let front = split.next().unwrap_or("").to_owned();
244	front + back
245}
246
247#[cfg(test)]
248mod tests {
249	use super::*;
250
251	#[test]
252	fn test_remove_last_occurrence() {
253		let test_string = "/src/routes/service/index";
254		let test_substring = "index";
255
256		assert_eq!(remove_last_occurrence(test_string, test_substring), "/src/routes/service/");
257	}
258
259	#[test]
260	fn test_is_dynamic_route() {
261		let test_string = "/src/routes/service/_id_";
262		let test_string_2 = "/src/routes/service/index";
263
264		assert_eq!(is_dynamic_route(test_string), true);
265		assert_eq!(is_dynamic_route(test_string_2), false);
266	}
267}