Skip to content

Commit

Permalink
feat: add catch all routes (#197)
Browse files Browse the repository at this point in the history
* feat: add catch all routes
  • Loading branch information
mtt-artis authored Sep 5, 2023
1 parent 4b6ad29 commit 85309ee
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 151 deletions.
138 changes: 47 additions & 91 deletions crates/router/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@

mod files;
mod route;

use files::Files;
use route::RouteAffinity;
use std::path::{Path, PathBuf};
use std::time::Instant;
use wws_config::Config;
Expand Down Expand Up @@ -50,6 +48,7 @@ impl Routes {
for route_path in route_paths {
routes.push(Route::new(path, route_path, &prefix, config));
}
routes.sort();
println!("✅ Workers loaded in {:?}.", start.elapsed());

Self { routes, prefix }
Expand All @@ -59,23 +58,16 @@ impl Routes {
self.routes.iter()
}

/// Based on a set of routes and a given path, it provides the best
/// match based on the parametrized URL score. See the [`Route::can_manage_path`]
/// method to understand how to calculate the score.
/// Provides the **first** route that can handle the given path.
/// This only works because the routes are already sorted.
/// Because a '/a/b' route may be served by:
/// - /a/b.js
/// - /a/[id].js
/// - /[id]/b.wasm
/// - /[id]/[other].wasm
/// - /[id]/[..all].wasm
pub fn retrieve_best_route<'a>(&'a self, path: &str) -> Option<&'a Route> {
// Keep it to avoid calculating the score twice when iterating
// to look for the best route
let mut best_score = -1;

self.routes
.iter()
.fold(None, |acc, item| match item.affinity(path) {
RouteAffinity::CanManage(score) if best_score == -1 || score < best_score => {
best_score = score;
Some(item)
}
_ => acc,
})
self.iter().find(|r| r.can_manage(path))
}

/// Defines a prefix in the context of the application.
Expand Down Expand Up @@ -113,90 +105,54 @@ impl Routes {
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;

#[test]
fn route_path_affinity() {
let build_route = |file: &str| -> Route {
let project_config = Config::default();
Route::new(
Path::new("../../tests/data/params"),
PathBuf::from(format!("../../tests/data/params{file}")),
"",
&project_config,
)
};

// Route initializes the Wasm module. We create these
// variables to avoid loading the same Wasm module multiple times
let param_route = build_route("/[id].wasm");
let fixed_route = build_route("/fixed.wasm");
let param_folder_route = build_route("/[id]/fixed.wasm");
let param_sub_route = build_route("/sub/[id].wasm");

let tests = [
(&param_route, "/a", RouteAffinity::CanManage(1)),
(&fixed_route, "/fixed", RouteAffinity::CanManage(0)),
(&fixed_route, "/a", RouteAffinity::CannotManage),
(&param_folder_route, "/a", RouteAffinity::CannotManage),
(&param_folder_route, "/a/fixed", RouteAffinity::CanManage(1)),
(&param_sub_route, "/a/b", RouteAffinity::CannotManage),
(&param_sub_route, "/sub/b", RouteAffinity::CanManage(2)),
];

for t in tests {
assert_eq!(t.0.affinity(t.1), t.2);
}
fn routes_sorted_on_creation() {
let project_config = Config::default();
let router = Routes::new(
Path::new("../../tests/data/params"),
"",
Vec::new(),
&project_config,
);

let mut sorted_router = Routes::new(
Path::new("../../tests/data/params"),
"",
Vec::new(),
&project_config,
);

sorted_router.routes.sort();

assert_eq!(router.routes, sorted_router.routes);
}

#[test]
fn best_route_by_affinity() {
let build_route = |file: &str| -> Route {
let project_config = Config::default();
Route::new(
Path::new("../../tests/data/params"),
PathBuf::from(format!("../../tests/data/params{file}")),
"",
&project_config,
)
};

// Route initializes the Wasm module. We create these
// variables to avoid loading the same Wasm module multiple times
let param_route = build_route("/[id].wasm");
let fixed_route = build_route("/fixed.wasm");
let param_folder_route = build_route("/[id]/fixed.wasm");
let param_sub_route = build_route("/sub/[id].wasm");

// I'm gonna use this values for comparison as `routes` consumes
// the Route elements.
let param_path = param_route.path.clone();
let fixed_path = fixed_route.path.clone();
let param_folder_path = param_folder_route.path.clone();
let param_sub_path = param_sub_route.path.clone();

let routes = Routes {
routes: vec![
param_route,
fixed_route,
param_folder_route,
param_sub_route,
],
prefix: String::from("/"),
};
fn retrieve_best_route() {
let project_config = Config::default();
let router = Routes::new(
Path::new("../../tests/data/params"),
"",
Vec::new(),
&project_config,
);

let tests = [
("/a", Some(param_path)),
("/fixed", Some(fixed_path)),
("/a/fixed", Some(param_folder_path)),
("/sub/b", Some(param_sub_path)),
("/any", Some("/[id]")),
("/fixed", Some("/fixed")),
("/any/fixed", Some("/[id]/fixed")),
("/any/sub", Some("/[id]/sub")),
("/sub/any", Some("/sub/[id]")),
("/sub/any/catch/all/routes", Some("/sub/[...all]")),
("/sub/sub/any/catch/all/routes", Some("/sub/sub/[...all]")),
("/donot/exist", None),
];

for t in tests {
let route = routes.retrieve_best_route(t.0);
for (given_path, expected_path) in tests {
let route = router.retrieve_best_route(given_path);

if let Some(path) = t.1 {
if let Some(path) = expected_path {
assert!(route.is_some());
assert_eq!(route.unwrap().path, path);
} else {
Expand Down
Loading

0 comments on commit 85309ee

Please sign in to comment.