Skip to content

Commit

Permalink
Add route analysis checking duplicates
Browse files Browse the repository at this point in the history
  • Loading branch information
WilliamRagstad committed Sep 14, 2023
1 parent e331003 commit 2f2d8fd
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 21 deletions.
4 changes: 2 additions & 2 deletions src/analytics/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ fn construct_dependency_tree(files: &Vec<WXModule>) -> 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
Expand Down
3 changes: 2 additions & 1 deletion src/analytics/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod dependencies;
pub mod dependencies;
pub mod routes;
70 changes: 70 additions & 0 deletions src/analytics/routes.rs
Original file line number Diff line number Diff line change
@@ -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<WXInfoField>>;

fn extract_flat_routes(modules: &Vec<WXModule>) -> 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<String> {
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::<Vec<_>>();
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<WXModule>) {
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)
6 changes: 3 additions & 3 deletions src/engine/runner.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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::<Vec<_>>()
.join(", ")
);
Expand Down
5 changes: 3 additions & 2 deletions src/file/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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 },
})
}

Expand Down Expand Up @@ -752,7 +753,7 @@ impl<'a> WebXFileParser<'a> {

fn parse_module(&mut self) -> Result<WXModule, String> {
Ok(WXModule {
path: self.file.clone(),
path: WXModulePath::new(self.file.clone()),
scope: self.parse_scope(true, WXROOT_PATH)?,
})
}
Expand Down
72 changes: 59 additions & 13 deletions src/file/webx.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<WXUrlPathSegment>);

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
Expand All @@ -50,23 +57,45 @@ 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
/// A file data structure for WebX files.
#[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));
Expand All @@ -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),
}
}

Expand Down Expand Up @@ -116,7 +145,7 @@ pub struct WXScope {
pub scopes: Vec<WXScope>,
}

#[derive(Debug)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct WXModel {
/// The name of the model.
pub name: String,
Expand All @@ -134,7 +163,7 @@ pub struct WXHandler {
pub body: WXBody,
}

#[derive(Debug)]
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
pub enum WXRouteMethod {
CONNECT,
DELETE,
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/reporting/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 2f2d8fd

Please sign in to comment.