use super::convert::{convert_primitive, TypescriptType};
use syn::{parse_file, Expr, File as SynFile, Generics, Item, Lit, Type};
pub const GENERATED_TS_FILE_MESSAGE: &str =
"// @generated automatically by Rapid-web (https://rapid.cincinnati.ventures). DO NOT CHANGE OR EDIT THIS FILE!";
#[derive(Debug)]
pub enum TypeClass {
InputBody,
QueryParam,
Path,
Invalid,
Return,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HandlerRequestType {
Get,
Post,
Delete,
Put,
Patch,
Query,
Mutation,
}
#[derive(Debug)]
pub struct HandlerType {
pub type_value: Option<Type>,
pub class: Option<TypeClass>,
pub handler_type: HandlerRequestType,
}
pub fn extract_handler_types(route_source: &str) -> Option<Vec<Option<HandlerType>>> {
let parsed_file: SynFile = syn::parse_str(route_source).expect("Error: Syn could not parse handler source file!");
for item in parsed_file.items {
if let Item::Fn(function) = item {
if is_valid_handler("rapid_handler", function.attrs) {
let mut function_types: Vec<Option<HandlerType>> = Vec::new();
let arg_types = function.sig.inputs.iter();
let function_name = function.sig.ident;
for type_value in arg_types {
if let syn::FnArg::Typed(typed) = type_value {
let rust_type = *typed.ty.clone();
let type_class = get_type_class(rust_type.clone());
function_types.push(Some(HandlerType {
type_value: Some(rust_type),
class: type_class,
handler_type: match function_name.to_string().as_str() {
"get" => HandlerRequestType::Get,
"post" => HandlerRequestType::Post,
"delete" => HandlerRequestType::Delete,
"put" => HandlerRequestType::Put,
"patch" => HandlerRequestType::Patch,
"query" => HandlerRequestType::Query,
"mutation" => HandlerRequestType::Mutation,
_ => HandlerRequestType::Get,
},
}));
}
}
function_types.push(Some(HandlerType {
type_value: None,
class: Some(TypeClass::Return),
handler_type: match function_name.to_string().as_str() {
"get" => HandlerRequestType::Get,
"post" => HandlerRequestType::Post,
"delete" => HandlerRequestType::Delete,
"put" => HandlerRequestType::Put,
"patch" => HandlerRequestType::Patch,
"query" => HandlerRequestType::Query,
"mutation" => HandlerRequestType::Mutation,
_ => HandlerRequestType::Get,
},
}));
return Some(function_types);
}
}
}
None
}
pub fn get_handler_type(route_source: &str) -> Option<String> {
let parsed_file: SynFile = syn::parse_str(route_source).expect("Error: Syn could not parse handler source file!");
for item in parsed_file.items {
if let Item::Fn(function) = item {
if is_valid_handler("rapid_handler", function.attrs) {
let function_name = function.sig.ident.to_string();
return Some(function_name);
}
}
}
None
}
pub fn get_type_class(rust_type: Type) -> Option<TypeClass> {
match rust_type {
Type::Reference(path) => get_type_class(*path.elem),
Type::Path(path) => {
let segment = path.path.segments.last().unwrap();
let tokens = &segment.ident;
Some(match tokens.to_string().as_str() {
"RapidPath" => TypeClass::Path,
"RapidQuery" => TypeClass::QueryParam,
"RapidJson" => TypeClass::InputBody, _ => TypeClass::Invalid,
})
}
_ => None,
}
}
pub fn is_valid_handler(macro_name: &str, attributes: Vec<syn::Attribute>) -> bool {
attributes
.iter()
.any(|attr| attr.path().segments.iter().any(|segment| segment.ident == macro_name))
}
pub fn space(space_amount: u32) -> String {
let mut space_string = "".to_string();
for _ in 0..space_amount {
space_string.push(' ');
}
space_string
}
pub fn indent(amount: u32) -> String {
let mut new_amount = String::new();
for _ in 0..amount {
new_amount.push('\n');
}
new_amount
}
pub fn get_struct_generics(type_generics: Generics) -> String {
let mut generic_params: Vec<String> = Vec::new();
for generic_param in type_generics.params {
if let syn::GenericParam::Type(rust_type) = generic_param {
generic_params.push(rust_type.ident.to_string());
}
}
if generic_params.is_empty() {
"".to_string()
} else {
format!("<{}>", generic_params.join(", "))
}
}
pub fn get_route_key(file_path: String, handler_source: &str) -> String {
let file = parse_file(handler_source).expect("Error: Rapid compiler could not parse handler source file!");
let fallback_key = file_path.replacen("/", "", 1).replace("/", "_").replace(".rs", "");
for item in file.items {
match item {
Item::Const(item_const) => {
if item_const.ident.to_string() == "ROUTE_KEY" {
return match *item_const.expr {
Expr::Lit(item) => match item.lit {
Lit::Str(val) => {
let key = val.token().to_string();
if key == "index" {
panic!("Invalid route key: 'index' is a reserved route key for rapid-web");
}
key
}
_ => continue,
},
_ => continue,
};
}
continue;
}
_ => continue,
}
}
fallback_key
}
pub fn get_output_type_alias(handler_source: &str) -> TypescriptType {
let file = parse_file(handler_source).expect("Error: Rapid compiler could not parse handler source file!");
let fallback_alias = TypescriptType {
typescript_type: "any".to_string(),
is_optional: false,
};
for item in file.items {
if let Item::Type(item_type) = item {
if item_type.ident.to_string() == "RapidOutput" {
let syn_type = *item_type.ty;
return convert_primitive(&syn_type);
}
}
}
return fallback_alias;
}
pub fn is_dynamic_route(str: &str) -> bool {
let regex = regex::Regex::new(r"_.*?_").unwrap();
regex.is_match(str)
}
pub fn remove_last_occurrence(s: &str, sub: &str) -> String {
let mut split = s.rsplitn(2, sub);
let back = split.next().unwrap_or("");
let front = split.next().unwrap_or("").to_owned();
front + back
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_remove_last_occurrence() {
let test_string = "/src/routes/service/index";
let test_substring = "index";
assert_eq!(remove_last_occurrence(test_string, test_substring), "/src/routes/service/");
}
#[test]
fn test_is_dynamic_route() {
let test_string = "/src/routes/service/_id_";
let test_string_2 = "/src/routes/service/index";
assert_eq!(is_dynamic_route(test_string), true);
assert_eq!(is_dynamic_route(test_string_2), false);
}
}