From 2f2d8fd70eae3750710b67e3d9d6f10525740bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20R=C3=A5gstad?= Date: Thu, 14 Sep 2023 20:38:22 +0200 Subject: [PATCH] Add route analysis checking duplicates --- src/analytics/dependencies.rs | 4 +- src/analytics/mod.rs | 3 +- src/analytics/routes.rs | 70 ++++++++++++++++++++++++++++++++++ src/engine/runner.rs | 6 +-- src/file/parser.rs | 5 ++- src/file/webx.rs | 72 ++++++++++++++++++++++++++++------- src/reporting/error.rs | 2 + 7 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 src/analytics/routes.rs diff --git a/src/analytics/dependencies.rs b/src/analytics/dependencies.rs index ae2bfc0..6b4bf48 100644 --- a/src/analytics/dependencies.rs +++ b/src/analytics/dependencies.rs @@ -32,10 +32,10 @@ fn construct_dependency_tree(files: &Vec) -> DependencyTree { for file in files.iter() { // Insert dependencies into the tree as keys and the file path as the value. for dependency in file.scope.includes.iter() { - let dependency_target = file.path.join(dependency); + let dependency_target = file.path.inner.join(dependency); tree.entry(dependency_target) .or_insert(Vec::new()) - .push(file.path.clone()); + .push(file.path.inner.clone()); } } tree diff --git a/src/analytics/mod.rs b/src/analytics/mod.rs index 0fbd273..1e79562 100644 --- a/src/analytics/mod.rs +++ b/src/analytics/mod.rs @@ -1 +1,2 @@ -pub mod dependencies; \ No newline at end of file +pub mod dependencies; +pub mod routes; diff --git a/src/analytics/routes.rs b/src/analytics/routes.rs new file mode 100644 index 0000000..ceb03e3 --- /dev/null +++ b/src/analytics/routes.rs @@ -0,0 +1,70 @@ +use colored::*; + +use std::collections::HashMap; + +use crate::{file::webx::{WXModule, WXScope, WXUrlPath, WXROOT_PATH, WXRouteMethod, WXInfoField}, reporting::error::{exit_error, ERROR_DUPLICATE_ROUTE}}; + +type FlatRoutes = HashMap<(WXRouteMethod, WXUrlPath), Vec>; + +fn extract_flat_routes(modules: &Vec) -> FlatRoutes { + let mut routes = HashMap::new(); + fn flatten_scopes(module_name: String, scope: &WXScope, path_prefix: WXUrlPath, routes: &mut FlatRoutes) { + for route in scope.routes.iter() { + let flat_path = path_prefix.combine(&route.path); + let route_key = (route.method.clone(), flat_path); + if routes.contains_key(&route_key) { + routes.get_mut(&route_key).unwrap().push(route.info.clone()); + } else { + routes.insert(route_key, vec![route.info.clone()]); + } + } + for sub_scope in scope.scopes.iter() { + let sub_scope_path = path_prefix.combine(&sub_scope.path); + flatten_scopes(module_name.clone(), sub_scope, sub_scope_path, routes); + } + } + for module in modules.iter() { + flatten_scopes(module.path.module_name(), &module.scope, WXROOT_PATH, &mut routes); + } + routes +} + +fn extract_duplicate_routes(routes: &FlatRoutes) -> Vec { + routes + .iter() + .filter(|(_, modules)| modules.len() > 1) + .map(|((method, path), modules)| { + let locations = modules + .iter() + .map(|info| + format!("{} line {}", info.path.module_name(), info.line) + .bright_black().to_string()) + .collect::>(); + format!( + "Route {} {} is defined in modules:\n - {}", + method.to_string().green(), + path.to_string().yellow(), + locations.join("\n - ") + ) + }) + .collect() +} + +pub fn analyse_module_routes(modules: &Vec) { + let routes = extract_flat_routes(modules); + let duplicate_routes = extract_duplicate_routes(&routes); + if !duplicate_routes.is_empty() { + exit_error( + format!( + "Duplicate routes detected:\n - {}", + duplicate_routes.join("\n - ") + ), + ERROR_DUPLICATE_ROUTE, + ); + } +} + +// Route verification, check for: +// - duplicate route (paths and methods) +// - invalid route combinations (e.g. GET + body) +// - return type for each route (HTML, JSON, or unknown) diff --git a/src/engine/runner.rs b/src/engine/runner.rs index 64817c3..297dc4b 100644 --- a/src/engine/runner.rs +++ b/src/engine/runner.rs @@ -1,7 +1,6 @@ use std::path::Path; -use crate::analytics::dependencies::analyse_module_deps; -use crate::file::webx::WXModule; +use crate::analytics::{dependencies::analyse_module_deps, routes::analyse_module_routes}; use crate::project::{load_modules, load_project_config}; const PROJECT_CONFIG_FILE_NAME: &str = "webx.config.json"; @@ -12,12 +11,13 @@ pub fn run(root: &Path, prod: bool) { let source_root = root.join(&config.src); let webx_modules = load_modules(&source_root); analyse_module_deps(&webx_modules); + analyse_module_routes(&webx_modules); println!( "Webx modules: {:?}", webx_modules .iter() - .map(WXModule::module_name) + .map(|m| m.path.module_name()) .collect::>() .join(", ") ); diff --git a/src/file/parser.rs b/src/file/parser.rs index 88058b5..0929bde 100644 --- a/src/file/parser.rs +++ b/src/file/parser.rs @@ -12,7 +12,7 @@ use std::{ use super::webx::{ WXBody, WXBodyType, WXHandler, WXModel, WXRoute, WXRouteHandler, WXRouteMethod, WXRouteReqBody, - WXScope, WXTypedIdentifier, WXUrlPath, WXUrlPathSegment, WXROOT_PATH, + WXScope, WXTypedIdentifier, WXUrlPath, WXUrlPathSegment, WXROOT_PATH, WXInfoField, WXModulePath, }; struct WebXFileParser<'a> { @@ -609,6 +609,7 @@ impl<'a> WebXFileParser<'a> { pre_handlers: self.parse_route_handlers(), body: self.parse_code_body(), post_handlers: self.parse_route_handlers(), + info: WXInfoField { path: WXModulePath::new(self.file.clone()), line: self.line }, }) } @@ -752,7 +753,7 @@ impl<'a> WebXFileParser<'a> { fn parse_module(&mut self) -> Result { Ok(WXModule { - path: self.file.clone(), + path: WXModulePath::new(self.file.clone()), scope: self.parse_scope(true, WXROOT_PATH)?, }) } diff --git a/src/file/webx.rs b/src/file/webx.rs index 926f5ea..9ad3c8c 100644 --- a/src/file/webx.rs +++ b/src/file/webx.rs @@ -1,11 +1,17 @@ use std::{ - fmt::{self, Formatter}, + fmt::{self, Formatter, Display, Debug}, path::PathBuf, }; +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct WXInfoField { + pub path: WXModulePath, + pub line: usize, +} + pub type WXType = String; -#[derive(Clone)] +#[derive(Clone, Hash, PartialEq, Eq)] pub struct WXTypedIdentifier { pub name: String, pub type_: WXType, @@ -17,16 +23,17 @@ impl fmt::Debug for WXTypedIdentifier { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum WXUrlPathSegment { Literal(String), Parameter(WXTypedIdentifier), Regex(String), } +#[derive(Hash, PartialEq, Eq)] pub struct WXUrlPath(pub Vec); -impl fmt::Debug for WXUrlPath { +impl Display for WXUrlPath { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let c = self.0.clone(); let ss = c @@ -50,6 +57,20 @@ impl fmt::Debug for WXUrlPath { } } +impl Debug for WXUrlPath { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl WXUrlPath { + pub fn combine(&self, other: &WXUrlPath) -> WXUrlPath { + let mut path = self.0.clone(); + path.extend(other.0.clone()); + WXUrlPath(path) + } +} + pub const WXROOT_PATH: WXUrlPath = WXUrlPath(vec![]); /// # WebX module @@ -57,16 +78,24 @@ pub const WXROOT_PATH: WXUrlPath = WXUrlPath(vec![]); #[derive(Debug)] pub struct WXModule { /// The path to the file. - pub path: PathBuf, + pub path: WXModulePath, /// Global webx module scope. pub scope: WXScope, } -impl WXModule { +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct WXModulePath { + pub inner: PathBuf, +} + +impl WXModulePath { + pub fn new(inner: PathBuf) -> Self { + Self { inner } + } /// "/path/to/file.webx" -> "path/to" pub fn parent(&self) -> String { let cwd = std::env::current_dir().unwrap().canonicalize().unwrap(); - let path = self.path.canonicalize().unwrap(); + let path = self.inner.canonicalize().unwrap(); let stripped = path .strip_prefix(&cwd) .expect(&format!("Failed to strip prefix of {:?}", path)); @@ -80,15 +109,15 @@ impl WXModule { /// "/path/to/file.webx" -> "file" pub fn name(&self) -> &str { - match self.path.file_name() { + match self.inner.file_name() { Some(name) => match name.to_str() { Some(name) => match name.split('.').next() { Some(name) => name, - None => panic!("Failed to extract file module name of {:?}", self.path), + None => panic!("Failed to extract file module name of {:?}", self.inner), }, - None => panic!("Failed to convert file name to string of {:?}", self.path), + None => panic!("Failed to convert file name to string of {:?}", self.inner), }, - None => panic!("Failed to get file name of {:?}", self.path), + None => panic!("Failed to get file name of {:?}", self.inner), } } @@ -116,7 +145,7 @@ pub struct WXScope { pub scopes: Vec, } -#[derive(Debug)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct WXModel { /// The name of the model. pub name: String, @@ -134,7 +163,7 @@ pub struct WXHandler { pub body: WXBody, } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq, Clone)] pub enum WXRouteMethod { CONNECT, DELETE, @@ -147,6 +176,22 @@ pub enum WXRouteMethod { TRACE, } +impl Display for WXRouteMethod { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + WXRouteMethod::CONNECT => write!(f, "CONNECT"), + WXRouteMethod::DELETE => write!(f, "DELETE"), + WXRouteMethod::GET => write!(f, "GET"), + WXRouteMethod::HEAD => write!(f, "HEAD"), + WXRouteMethod::OPTIONS => write!(f, "OPTIONS"), + WXRouteMethod::PATCH => write!(f, "PATCH"), + WXRouteMethod::POST => write!(f, "POST"), + WXRouteMethod::PUT => write!(f, "PUT"), + WXRouteMethod::TRACE => write!(f, "TRACE"), + } + } +} + pub enum WXBodyType { TS, TSX, @@ -214,6 +259,7 @@ impl fmt::Debug for WXRouteHandler { #[derive(Debug)] pub struct WXRoute { + pub info: WXInfoField, /// HTTP method of the route. pub method: WXRouteMethod, /// The path of the route. diff --git a/src/reporting/error.rs b/src/reporting/error.rs index 5cdf3e8..4d827e6 100644 --- a/src/reporting/error.rs +++ b/src/reporting/error.rs @@ -6,12 +6,14 @@ pub const ERROR_READ_PROJECT_CONFIG: i32 = 2; pub const ERROR_CIRCULAR_DEPENDENCY: i32 = 3; pub const ERROR_PARSE_IO: i32 = 4; pub const ERROR_SYNTAX: i32 = 5; +pub const ERROR_DUPLICATE_ROUTE: i32 = 6; pub fn code_to_name(code: i32) -> String { match code { ERROR_READ_WEBX_FILES => "READ_WEBX_FILES".to_owned(), ERROR_READ_PROJECT_CONFIG => "READ_PROJECT_CONFIG".to_owned(), ERROR_CIRCULAR_DEPENDENCY => "CIRCULAR_DEPENDENCY".to_owned(), + ERROR_DUPLICATE_ROUTE => "DUPLICATE_ROUTE".to_owned(), ERROR_PARSE_IO => "PARSE_IO".to_owned(), ERROR_SYNTAX => "SYNTAX".to_owned(), _ => format!("UNKNOWN {}", code),