Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 63 additions & 60 deletions goodrouter.code-workspace
Original file line number Diff line number Diff line change
@@ -1,64 +1,67 @@
{
"folders": [
{
"name": "assets",
"path": "assets",
},
{
"name": "fixtures",
"path": "fixtures",
},
{
"name": "npm/www",
"path": "packages/npm/www",
},
{
"name": "npm/goodrouter",
"path": "packages/npm/goodrouter",
},
{
"name": "cargo/goodrouter",
"path": "packages/cargo/goodrouter",
},
{
"name": "net/Goodrouter",
"path": "packages/net/Goodrouter",
},
{
"name": "net/Goodrouter.Spec",
"path": "packages/net/Goodrouter.Spec",
},
{
"name": "net/Goodrouter.Bench",
"path": "packages/net/Goodrouter.Bench",
},
],
"settings": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "always",
"source.organizeImports": "always",
},
"editor.rulers": [100],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"rust-analyzer.check.command": "clippy",
"typescript.tsdk": "./node_modules/typescript/lib",
"npm.packageManager": "npm",
"folders": [
{
"name": "assets",
"path": "assets",
},
{
"name": "fixtures",
"path": "fixtures",
},
{
"name": "npm/www",
"path": "packages/npm/www",
},
{
"name": "npm/goodrouter",
"path": "packages/npm/goodrouter",
},
{
"name": "cargo/goodrouter",
"path": "packages/cargo/goodrouter",
},
"extensions": {
"recommendations": [
"editorconfig.editorconfig",
"ryanluker.vscode-coverage-gutters",
"tamasfe.even-better-toml",
"ms-dotnettools.csharp",
"redhat.vscode-xml",
"streetsidesoftware.code-spell-checker",
"rust-lang.rust-analyzer",
"vadimcn.vscode-lldb",
"esbenp.prettier-vscode",
"eseom.nunjucks-template",
"firefox-devtools.vscode-firefox-debug",
"msjsdiag.debugger-for-chrome",
],
{
"name": "net/Goodrouter",
"path": "packages/net/Goodrouter",
},
{
"name": "net/Goodrouter.Spec",
"path": "packages/net/Goodrouter.Spec",
},
{
"name": "net/Goodrouter.Bench",
"path": "packages/net/Goodrouter.Bench",
},
],
"settings": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "always",
"source.organizeImports": "always",
},
"editor.rulers": [100],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"rust-analyzer.check.command": "clippy",
"typescript.tsdk": "./node_modules/typescript/lib",
"npm.packageManager": "npm",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
},
},
"extensions": {
"recommendations": [
"editorconfig.editorconfig",
"ryanluker.vscode-coverage-gutters",
"tamasfe.even-better-toml",
"ms-dotnettools.csharp",
"redhat.vscode-xml",
"streetsidesoftware.code-spell-checker",
"rust-lang.rust-analyzer",
"vadimcn.vscode-lldb",
"esbenp.prettier-vscode",
"eseom.nunjucks-template",
"firefox-devtools.vscode-firefox-debug",
"msjsdiag.debugger-for-chrome",
],
},
}
3 changes: 2 additions & 1 deletion packages/cargo/goodrouter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod route_node;
pub mod router;
mod string_utility;
mod template;

pub mod router;
27 changes: 27 additions & 0 deletions packages/cargo/goodrouter/src/route_node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
mod functions;
mod merge;
mod traits;

use std::{cell, collections::BTreeSet, rc};

#[derive(Debug)]
pub struct RouteNodeRc<'r, K>(pub rc::Rc<cell::RefCell<RouteNode<'r, K>>>);

#[derive(Debug)]
pub struct RouteNodeWeak<'r, K>(pub rc::Weak<cell::RefCell<RouteNode<'r, K>>>);

#[derive(Debug)]
pub struct RouteNode<'r, K> {
// the route's key, if any
pub route_key: Option<K>,
// the route parameter names
pub route_parameter_names: Vec<&'r str>,
// suffix that comes after the parameter value (if any!) of the path
anchor: &'r str,
// does this node has a parameter
has_parameter: bool,
// children that represent the rest of the path that needs to be matched
children: BTreeSet<RouteNodeRc<'r, K>>,
// parent node, should only be null for the root node
parent: Option<RouteNodeWeak<'r, K>>,
}
221 changes: 221 additions & 0 deletions packages/cargo/goodrouter/src/route_node/functions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use super::*;
use crate::string_utility::find_common_prefix_length;
use crate::template::template_pairs::parse_template_pairs;
use regex::Regex;
use std::borrow::Cow;
use std::cmp::min;

impl<'r, K> RouteNodeRc<'r, K> {
pub fn parse<'f>(
&self,
path: &'f str,
maximum_parameter_value_length: usize,
) -> (Option<K>, Vec<&'r str>, Vec<&'f str>)
where
K: Copy,
{
let mut path = path;
let mut parameter_values: Vec<&str> = Default::default();

let node = self.0.borrow();

if node.has_parameter {
// we are matching a parameter value! If the path's length is 0, there is no match, because a parameter value should have at least length 1
if path.is_empty() {
return Default::default();
}

// look for the anchor in the path. If the anchor is empty, match the remainder of the path
let index = if node.anchor.is_empty() {
Some(path.len())
} else {
path[..min(
maximum_parameter_value_length + node.anchor.len(),
path.len(),
)]
.find(node.anchor)
};

if let Some(index) = index {
let value = &path[..index];

// remove the matches part from the path
path = &path[index + node.anchor.len()..];

parameter_values.push(value);
} else {
return Default::default();
}
} else {
// if this node does not represent a parameter we expect the path to start with the `anchor`
if !path.starts_with(node.anchor) {
// this node does not match the path
return Default::default();
}

// we successfully matches the node to the path, now remove the matched part from the path
path = &path[node.anchor.len()..];
}

for child_rc in &node.children {
if let (Some(child_route_name), child_route_parameter_names, mut child_parameters_values) =
child_rc.parse(path, maximum_parameter_value_length)
{
let mut parameter_values = parameter_values.clone();
parameter_values.append(&mut child_parameters_values);
return (
Some(child_route_name),
child_route_parameter_names,
parameter_values,
);
}
}

// if the node had a route name and there is no path left to match against then we found a route
if path.is_empty() {
if let Some(route_key) = node.route_key {
return (
Some(route_key),
node.route_parameter_names.clone(),
parameter_values,
);
}
}

Default::default()
}

pub fn stringify<'f>(&self, parameter_values: Vec<Cow<'f, str>>) -> Cow<'f, str>
where
'r: 'f,
{
let mut parameter_values = parameter_values.clone();
let mut current_node_rc = Some(self.clone());
let mut path_parts = Vec::new();

while let Some(node_rc) = current_node_rc {
let node = node_rc.0.borrow();
path_parts.insert(0, Cow::Borrowed(node.anchor));

if node.has_parameter {
let value = parameter_values.pop().unwrap();
path_parts.insert(0, value);
}

current_node_rc = node
.parent
.as_ref()
.map(|parent_node_weak| parent_node_weak.try_into().unwrap());
}

path_parts
.into_iter()
.reduce(|path, path_part| path + path_part)
.unwrap()
}

pub fn insert(
&self,
route_key: K,
template: &'r str,
parameter_placeholder_re: &'r Regex,
) -> RouteNodeRc<'r, K>
where
K: Copy,
{
let template_pairs: Vec<_> = parse_template_pairs(template, parameter_placeholder_re).collect();
let route_parameter_names: Vec<_> = template_pairs
.iter()
.cloned()
.filter_map(|(_anchor, parameter)| parameter)
.collect();

let mut node_current_rc = self.clone();
for index in 0..template_pairs.len() {
let (anchor, parameter) = template_pairs[index];
let has_parameter = parameter.is_some();
let route_key = if index == template_pairs.len() - 1 {
Some(route_key)
} else {
None
};

let (common_prefix_length, child_node_rc) = node_current_rc
.0
.borrow()
.find_similar_child(anchor, has_parameter);

node_current_rc = node_current_rc.merge(
child_node_rc.as_ref(),
anchor,
has_parameter,
route_key,
route_parameter_names.clone(),
common_prefix_length,
);
}

node_current_rc
}
}

impl<'r, K> RouteNode<'r, K> {
pub fn find_similar_child(
&self,
anchor: &'r str,
has_parameter: bool,
) -> (usize, Option<RouteNodeRc<'r, K>>) {
let anchor_chars: Vec<_> = anchor.chars().collect();

for child_node_rc in self.children.iter() {
if child_node_rc.0.borrow().has_parameter != has_parameter {
continue;
}

let child_anchor_chars: Vec<_> = child_node_rc.0.borrow().anchor.chars().collect();

let common_prefix_length = find_common_prefix_length(&anchor_chars, &child_anchor_chars);

if common_prefix_length == 0 {
continue;
}

return (common_prefix_length, Some(child_node_rc.clone()));
}

Default::default()
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::template::TEMPLATE_PLACEHOLDER_REGEX;
use itertools::Itertools;

#[test]
fn route_node_permutations() {
let route_configs = ["/a", "/b/{x}", "/b/{x}/", "/b/{x}/c", "/b/{y}/d"];

let mut node_root_previous_rc = None;

for route_configs in route_configs.iter().permutations(route_configs.len()) {
let node_root_rc = RouteNodeRc::default();

for template in route_configs {
node_root_rc.insert(template, template, &TEMPLATE_PLACEHOLDER_REGEX);
}

{
let node_root = node_root_rc.0.borrow();
assert_eq!(node_root.children.len(), 1);
}

if let Some(node_root_previous) = node_root_previous_rc {
assert_eq!(node_root_rc, node_root_previous);
}

node_root_previous_rc = Some(node_root_rc.clone());
}
}
}
Loading