use super::{
convert::{convert_all_types_in_path, TypescriptConverter, TypescriptType},
util::{
extract_handler_types, get_handler_type, get_output_type_alias, get_route_key, is_dynamic_route, remove_last_occurrence, space,
HandlerRequestType, TypeClass, GENERATED_TS_FILE_MESSAGE,
},
};
use crate::util::validate_route_handler;
use std::{
fs::{File, OpenOptions},
io::prelude::*,
path::PathBuf,
};
use walkdir::WalkDir;
#[derive(Debug, Clone, PartialEq)]
pub enum Handler {
Query(TypedQueryHandler),
Mutation(TypedMutationHandler),
}
#[derive(Debug, Clone, PartialEq)]
pub struct RouteKey {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TypedQueryHandler {
pub request_type: HandlerRequestType,
pub path: Option<TypescriptType>,
pub query_params: Option<TypescriptType>,
pub output_type: TypescriptType,
pub route_key: RouteKey,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TypedMutationHandler {
pub request_type: HandlerRequestType,
pub query_params: Option<TypescriptType>,
pub path: Option<TypescriptType>,
pub input_type: Option<TypescriptType>,
pub output_type: TypescriptType,
pub route_key: RouteKey,
}
pub fn generate_handler_types(routes_path: PathBuf, converter: &mut TypescriptConverter) -> Vec<Handler> {
let mut handlers: Vec<Handler> = Vec::new();
let routes_dir = routes_path;
for route_file in WalkDir::new(routes_dir.clone()) {
let entry = match route_file {
Ok(val) => val,
Err(e) => panic!("An error occurred what attempting to parse directory: {}", e),
};
if entry.path().is_dir() {
continue;
}
let file_name = entry.file_name();
if file_name == "_middleware.rs" || file_name == "mod.rs" {
continue;
}
let mut file = File::open(&entry.path()).unwrap();
let mut route_file_contents = String::new();
file.read_to_string(&mut route_file_contents).unwrap();
if !validate_route_handler(&route_file_contents) {
continue;
}
let parsed_route_dir = entry
.path()
.to_str()
.unwrap_or("/")
.to_string()
.replace(routes_dir.to_str().unwrap_or("src/routes"), "")
.replace(".rs", "");
let route_key = RouteKey {
key: get_route_key(parsed_route_dir.clone(), &route_file_contents),
value: parsed_route_dir,
};
let handler_types = match extract_handler_types(&route_file_contents) {
Some(val) => {
if val.len() > 0 {
val
} else {
continue;
}
}
None => continue,
};
let mut query_params: Option<TypescriptType> = None;
let mut body_type: Option<TypescriptType> = None;
let mut path: Option<TypescriptType> = None;
let output_type: TypescriptType = get_output_type_alias(&route_file_contents);
let request_type = handler_types[0].as_ref().unwrap().handler_type.clone();
for typed in handler_types {
let rust_type = match typed {
Some(val) => val,
None => continue,
};
let converted_type = match rust_type.type_value {
Some(val) => converter.convert_primitive(val),
None => TypescriptType {
typescript_type: String::from("any"),
is_optional: false,
},
};
match rust_type.class {
Some(TypeClass::InputBody) => body_type = Some(converted_type),
Some(TypeClass::QueryParam) => query_params = Some(converted_type),
Some(TypeClass::Path) => path = Some(converted_type),
_ => continue,
}
}
match request_type {
HandlerRequestType::Get => {
handlers.push(Handler::Query(TypedQueryHandler {
request_type,
path,
query_params,
output_type,
route_key,
}));
}
HandlerRequestType::Query => {
handlers.push(Handler::Query(TypedQueryHandler {
request_type,
path,
query_params,
output_type,
route_key,
}));
}
_ => {
handlers.push(Handler::Mutation(TypedMutationHandler {
request_type,
query_params,
path,
input_type: body_type,
output_type,
route_key,
}));
}
}
}
handlers
}
pub fn create_typescript_types(out_dir: PathBuf, route_dir: PathBuf, type_generation_dir: PathBuf) {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(format!("{}/bindings.ts", out_dir.as_os_str().to_str().unwrap()))
.unwrap();
let mut converter = TypescriptConverter::new(true, "".to_string(), true, 4, file);
let handlers = generate_handler_types(route_dir.clone(), &mut converter);
if handlers.len() < 1 {
return;
}
let routes = generate_routes(route_dir.to_str().unwrap());
let mut queries_ts = String::from("{");
let mut mutations_ts = String::from("{");
convert_all_types_in_path(type_generation_dir.to_str().unwrap(), &mut converter);
for handler in handlers {
match handler {
Handler::Query(query) => {
let mut ts_type = format!("\n\t\t{}: {{\n", query.route_key.key);
let route_path = query.route_key.value;
let is_dynamic_route_path = is_dynamic_route(&route_path);
let spacing = space(2);
let request_type = match query.request_type {
HandlerRequestType::Post => "post",
HandlerRequestType::Put => "put",
HandlerRequestType::Delete => "delete",
HandlerRequestType::Get => "get",
HandlerRequestType::Patch => "patch",
HandlerRequestType::Query => "query",
HandlerRequestType::Mutation => "mutation",
};
if let Some(query_params_type) = query.query_params {
let query_type = query_params_type.typescript_type;
if converter.converted_types.contains(&query_type) {
let query_params = format!("\t\t\tquery_params: {}", query_type);
ts_type.push_str(&format!("{}{}\n", spacing, query_params));
} else {
let query_params = format!("\t\t\tquery_params: {}", "any");
ts_type.push_str(&format!("{}{}\n", spacing, query_params));
}
}
if let Some(dynamic_path_type) = query.path {
let path_type = dynamic_path_type.typescript_type;
if converter.converted_types.contains(&path_type) {
let path = format!("\t\t\tpath: {}", path_type);
ts_type.push_str(&format!("{}{}\n", spacing, path));
} else {
let path = format!("\t\t\tpath: {}", "any");
ts_type.push_str(&format!("{}{}\n", spacing, path));
}
}
let output_body = format!("\t\t\toutput: {}", query.output_type.typescript_type);
ts_type.push_str(&format!("{}{}\n", spacing, output_body));
let request_type = format!("\t\t\ttype: '{}'", request_type);
ts_type.push_str(&format!("{}{}\n", spacing, request_type));
let dynamic_type = format!("\t\t\tisDynamic: {}", is_dynamic_route_path);
ts_type.push_str(&format!("{}{}\n", spacing, dynamic_type));
ts_type.push_str(&format!("\t\t}},\n"));
queries_ts.push_str(&ts_type);
}
Handler::Mutation(mutation) => {
let mut ts_type = format!("\n\t\t{}: {{\n", mutation.route_key.key);
let route_path = mutation.route_key.value;
let spacing = space(2);
let is_dynamic_route_path = is_dynamic_route(&route_path);
let request_type = match mutation.request_type {
HandlerRequestType::Post => "post",
HandlerRequestType::Put => "put",
HandlerRequestType::Delete => "delete",
HandlerRequestType::Get => "get",
HandlerRequestType::Patch => "patch",
HandlerRequestType::Query => "query",
HandlerRequestType::Mutation => "mutation",
};
if let Some(query_params_type) = mutation.query_params {
let query_type = query_params_type.typescript_type;
if converter.converted_types.contains(&query_type) {
let query_params = format!("\t\t\tquery_params: {}", query_type);
ts_type.push_str(&format!("{}{}\n", spacing, query_params));
} else {
let query_params = format!("\t\t\tquery_params: {}", "any");
ts_type.push_str(&format!("{}{}\n", spacing, query_params));
}
}
if let Some(dynamic_path_type) = mutation.path {
let path_type = dynamic_path_type.typescript_type;
if converter.converted_types.contains(&path_type) {
let path = format!("\t\t\tpath: {}", path_type);
ts_type.push_str(&format!("{}{}\n", spacing, path));
} else {
let path = format!("\t\t\tpath: {}", "any");
ts_type.push_str(&format!("{}{}\n", spacing, path));
}
}
if let Some(input_body_type) = mutation.input_type {
let input_body = input_body_type.typescript_type;
if converter.converted_types.contains(&input_body) {
let body = format!("\t\t\tinput: {}", input_body);
ts_type.push_str(&format!("{}{}\n", spacing, body));
} else {
let body = format!("\t\t\tinput: {}", "any");
ts_type.push_str(&format!("{}{}\n", spacing, body));
}
}
let output_body = format!("\t\t\toutput: {}", mutation.output_type.typescript_type);
ts_type.push_str(&format!("{}{}\n", spacing, output_body));
let request_type = format!("\t\t\ttype: '{}'", request_type);
ts_type.push_str(&format!("{}{}\n", spacing, request_type));
let dynamic_type = format!("\t\t\tisDynamic: {}", is_dynamic_route_path);
ts_type.push_str(&format!("{}{}\n", spacing, dynamic_type));
ts_type.push_str(&format!("\t\t}}\n"));
mutations_ts.push_str(&ts_type);
}
}
}
queries_ts.push_str("\t},");
if mutations_ts.len() < 2 {
mutations_ts.push_str("},");
} else {
mutations_ts.push_str("\t},");
}
let mut handlers_interface = format!("\n\nexport interface Handlers {{\n");
handlers_interface.push_str(&format!("\tqueries: {}\n", queries_ts));
handlers_interface.push_str(&format!("\tmutations: {}\n", mutations_ts));
handlers_interface.push_str("}");
converter.generate(Some(GENERATED_TS_FILE_MESSAGE));
converter.generate(Some(&handlers_interface));
converter.generate(Some(&routes));
converter.generate(None);
}
pub fn generate_routes(routes_dir: &str) -> String {
let mut typescript_object = String::from("\n\nexport const routes = {");
for route_file in WalkDir::new(routes_dir.clone()) {
let entry = match route_file {
Ok(val) => val,
Err(e) => panic!("An error occurred what attempting to parse directory: {}", e),
};
if entry.path().is_dir() {
continue;
}
let mut file = File::open(&entry.path()).unwrap();
let mut route_file_contents = String::new();
file.read_to_string(&mut route_file_contents).unwrap();
if !validate_route_handler(&route_file_contents) {
continue;
}
let file_name = entry.file_name();
if file_name == "_middleware.rs" || file_name == "mod.rs" {
continue;
}
let parsed_route_dir = entry
.path()
.to_str()
.unwrap_or("/")
.to_string()
.replace(routes_dir, "")
.replace(".rs", "");
let handler_type = match get_handler_type(&route_file_contents) {
Some(name) => name,
None => String::from("get"),
};
let route_key = RouteKey {
key: get_route_key(parsed_route_dir.clone(), &route_file_contents),
value: remove_last_occurrence(&parsed_route_dir, "index"),
};
let mut route = format!("\n\t{}: {{\n", route_key.key);
route.push_str(&format!("\t\turl: '{url}',\n", url = route_key.value));
route.push_str(&format!("\t\ttype: '{route_type}',\n", route_type = handler_type));
route.push_str("\t},");
typescript_object.push_str(&route);
}
typescript_object.push_str("\n} as const");
typescript_object
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_handler_types() {
let out_dir = PathBuf::from("tests/mocks/temp");
let bindings_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(format!("{}/bindings.ts", out_dir.as_os_str().to_str().unwrap()))
.unwrap();
let routes = generate_handler_types(PathBuf::from("tests/mocks/files"), &mut TypescriptConverter::new(true, "".to_string(), true, 4, bindings_file));
let mut expected_handlers: Vec<Handler> = Vec::new();
let hello_handler = Handler::Query(TypedQueryHandler {
request_type: HandlerRequestType::Query,
path: None,
query_params: None,
output_type: TypescriptType {
typescript_type: String::from("any"),
is_optional: false,
},
route_key: RouteKey {
key: String::from("hello"),
value: String::from("/hello"),
},
});
let mutation_handler = Handler::Mutation(TypedMutationHandler {
request_type: HandlerRequestType::Mutation,
query_params: None,
path: None,
input_type: None,
output_type: TypescriptType {
typescript_type: String::from("any"),
is_optional: false,
},
route_key: RouteKey {
key: String::from("mutation"),
value: String::from("/mutation"),
},
});
expected_handlers.push(mutation_handler);
expected_handlers.push(hello_handler);
assert_eq!(routes, expected_handlers);
}
#[test]
fn test_create_typescript_types() {
let out_dir = PathBuf::from("tests/mocks/temp");
let route_dir = PathBuf::from("tests/mocks/files");
let type_generation_dir = PathBuf::from("tests/mocks/files");
create_typescript_types(out_dir, route_dir, type_generation_dir);
let mut file = File::open("tests/mocks/temp/bindings.ts").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
std::fs::remove_file("tests/mocks/temp/bindings.ts").unwrap();
const _: &str = "// @generated automatically by Rapid-web (https://rapid.cincinnati.ventures). DO NOT CHANGE OR EDIT THIS FILE!
export interface Handlers {
queries: {
hello: {
output: any
type: 'query'
isDynamic: false
},
},
mutations: {
mutation: {
output: any
type: 'mutation'
isDynamic: false
}
},
}
export const routes = {
mutation: {
url: '/mutation',
type: 'mutation',
},
hello: {
url: '/hello',
type: 'query',
},
} as const
";
}
#[test]
fn test_generate_routes() {
let routes = generate_routes("tests/mocks/files");
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";
assert_eq!(routes, EXPECTED);
}
}