Skip to content

Commit 0024494

Browse files
committed
More structured specificity approach
1 parent 056c587 commit 0024494

File tree

11 files changed

+95
-57
lines changed

11 files changed

+95
-57
lines changed

CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
- Raised MSRV from 1.63 to 1.85.
1818
- Switch to Rust 2024 edition.
19-
- Syntax for parameters has changed from `{name}` to `<name>`
20-
- Parameter characters `<` and `>` can no longer be escaped.
19+
- Syntax for parameters has changed from `{name}` to `<name>`, to remove ambiguity with formatted strings.
20+
- Parameter characters `<` and `>` can no longer be escaped, since they aren't valid URL characters anyways.
2121
- Display no longer shows nodes storing data.
2222

2323
### Removed
@@ -27,8 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727

2828
### Fixed
2929

30-
- Improved detection for structural conflicts.
31-
- Improved preference of more specific route matches.
30+
- Improved detection of structural conflicts.
31+
- Correcting routing specificity for inline parameters.
3232

3333
## [0.8.1] - 2025-01-07
3434

src/errors/delete.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use core::error::Error;
33

44
use crate::errors::TemplateError;
55

6-
#[derive(Debug, PartialEq, Eq)]
6+
#[derive(Eq, PartialEq, Debug)]
77
pub enum DeleteError {
88
/// A [`TemplateError`] that occurred during the delete.
99
Template(TemplateError),

src/errors/insert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use core::error::Error;
33

44
use crate::errors::TemplateError;
55

6-
#[derive(Debug, PartialEq, Eq)]
6+
#[derive(Eq, PartialEq, Debug)]
77
pub enum InsertError {
88
/// A [`TemplateError`] that occurred during the insert.
99
Template(TemplateError),

src/errors/template.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use alloc::{fmt, string::String};
22
use core::error::Error;
33

4-
#[derive(Debug, PartialEq, Eq)]
4+
#[derive(Eq, PartialEq, Debug)]
55
pub enum TemplateError {
66
/// The template is empty.
77
Empty,

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,5 @@ mod parser;
156156
mod router;
157157
pub use router::{Match, Parameters, Router};
158158

159+
mod specificity;
159160
mod state;

src/node.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use alloc::{boxed::Box, string::String, vec::Vec};
22
use core::cmp::Ordering;
33

4-
use crate::state::{DynamicState, EndWildcardState, StaticState, WildcardState};
4+
use crate::{
5+
specificity::Specificity,
6+
state::{DynamicState, EndWildcardState, StaticState, WildcardState},
7+
};
58

69
mod conflict;
710
mod delete;
@@ -11,7 +14,7 @@ mod insert;
1114
mod optimize;
1215
mod search;
1316

14-
#[derive(Clone, Debug, Eq, PartialEq)]
17+
#[derive(Clone, Eq, PartialEq, Debug)]
1518
pub struct NodeData {
1619
/// The key to the stored data.
1720
pub key: usize,
@@ -20,11 +23,11 @@ pub struct NodeData {
2023
pub template: String,
2124

2225
/// The specificity of the template.
23-
pub specificity: usize,
26+
pub specificity: Specificity,
2427
}
2528

2629
/// Represents a node in the tree structure.
27-
#[derive(Clone, Debug, Eq, PartialEq)]
30+
#[derive(Clone, Eq, PartialEq, Debug)]
2831
pub struct Node<S> {
2932
/// The type of Node, and associated structure data.
3033
pub state: S,
@@ -40,7 +43,7 @@ pub struct Node<S> {
4043
pub end_wildcard: Option<Box<Node<EndWildcardState>>>,
4144

4245
/// Flag indicating whether this node need optimization.
43-
/// During optimization, the shortcut flags are updated, and nodes sorted.
46+
/// During optimization, the shortcut flags are updated, specificity calculated, and nodes sorted.
4447
pub needs_optimization: bool,
4548
}
4649

src/node/optimize.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
1-
use crate::node::Node;
1+
use crate::{node::Node, specificity::Specificity};
22

33
impl<S> Node<S> {
44
pub(crate) fn optimize(&mut self) {
5+
self.optimize_inner(Specificity::default());
6+
}
7+
8+
fn optimize_inner(&mut self, parent: Specificity) {
59
if !self.needs_optimization {
610
return;
711
}
812

13+
if let Some(ref mut data) = self.data {
14+
data.specificity = parent.clone();
15+
}
16+
917
for child in &mut self.static_children {
10-
child.optimize();
18+
let child_specificity = parent.clone().count_static(child.state.prefix.len());
19+
child.optimize_inner(child_specificity);
1120
}
1221

1322
for child in &mut self.dynamic_children {
14-
child.optimize();
23+
let child_specificity = parent.clone().count_dynamic();
24+
child.optimize_inner(child_specificity);
1525
}
1626

1727
for child in &mut self.wildcard_children {
18-
child.optimize();
28+
let child_specificity = parent.clone().count_wildcard();
29+
child.optimize_inner(child_specificity);
1930
}
2031

2132
if let Some(child) = self.end_wildcard.as_mut() {
22-
child.optimize();
33+
let child_specificity = parent.count_wildcard();
34+
child.optimize_inner(child_specificity);
2335
}
2436

2537
self.static_children.sort();

src/parser.rs

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ use crate::errors::TemplateError;
1111
/// Characters that are not allowed in parameter names.
1212
const INVALID_PARAM_CHARS: [u8; 4] = [b'*', b'<', b'>', b'/'];
1313

14-
#[derive(Debug, Clone, PartialEq, Eq)]
14+
#[derive(Clone, Eq, PartialEq, Debug)]
1515
pub enum Part {
1616
Static { prefix: Vec<u8> },
1717
Dynamic { name: String },
1818
Wildcard { name: String },
1919
}
2020

21-
#[derive(Debug, Clone, PartialEq, Eq)]
21+
#[derive(Clone, Eq, PartialEq, Debug)]
2222
pub struct Template {
2323
pub parts: Vec<Part>,
2424
}
@@ -194,33 +194,6 @@ impl Template {
194194

195195
Ok((part, end + 1))
196196
}
197-
198-
/// Calculate a specificity of this template.
199-
/// This is not a perfect solution, but appears 'good enough' for now.
200-
pub fn specificity(&self) -> usize {
201-
let mut static_length = 0;
202-
let mut dynamic_count = 0;
203-
let mut wildcard_count = 0;
204-
205-
for part in &self.parts {
206-
match part {
207-
Part::Static { prefix } => {
208-
static_length += prefix.len();
209-
}
210-
Part::Dynamic { .. } => {
211-
dynamic_count += 1;
212-
}
213-
Part::Wildcard { .. } => {
214-
wildcard_count += 1;
215-
}
216-
}
217-
}
218-
219-
let mut specificity = static_length.saturating_mul(1000);
220-
specificity = specificity.saturating_sub(dynamic_count * 10);
221-
specificity = specificity.saturating_sub(wildcard_count * 100);
222-
specificity
223-
}
224197
}
225198

226199
#[cfg(test)]

src/router.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ use crate::{
88
errors::{DeleteError, InsertError},
99
node::{Node, NodeData},
1010
parser::Template,
11+
specificity::Specificity,
1112
state::RootState,
1213
};
1314

1415
/// Stores data from a successful router match.
15-
#[derive(Debug, Eq, PartialEq)]
16+
#[derive(Eq, PartialEq, Debug)]
1617
pub struct Match<'r, 'p, T> {
1718
/// A reference to the matching template data.
1819
pub data: &'r T,
@@ -81,24 +82,20 @@ impl<T> Router<T> {
8182
pub fn insert(&mut self, template: &str, data: T) -> Result<(), InsertError> {
8283
let mut parsed = Template::new(template.as_bytes())?;
8384

84-
// Check for any conflicts.
8585
if let Some(found) = self.root.conflict(&mut parsed.clone()) {
8686
return Err(InsertError::Conflict {
8787
template: template.to_owned(),
8888
conflict: found.template.to_string(),
8989
});
9090
}
9191

92-
// All good, proceed with insert.
9392
let key = self.storage.insert(data);
94-
95-
let specificity = parsed.specificity();
9693
self.root.insert(
9794
&mut parsed,
9895
NodeData {
9996
key,
10097
template: template.to_owned(),
101-
specificity,
98+
specificity: Specificity::default(),
10299
},
103100
);
104101

src/specificity.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use core::cmp::Ordering;
2+
3+
/// Measures how specific a template is in terms of matching priority.
4+
///
5+
/// The priority is as follows:
6+
/// 1. Static prefix length: more static segments, more specific
7+
/// 2. Dynamic parameter count: less dynamics, more specific
8+
/// 3. Wildcard parameter count: less wildcards, more specific
9+
#[derive(Clone, Eq, PartialEq, Debug, Default)]
10+
pub struct Specificity {
11+
pub static_length: usize,
12+
pub dynamics_count: usize,
13+
pub wildcards_count: usize,
14+
}
15+
16+
impl Specificity {
17+
pub const fn count_static(mut self, length: usize) -> Self {
18+
self.static_length += length;
19+
self
20+
}
21+
22+
pub const fn count_dynamic(mut self) -> Self {
23+
self.dynamics_count += 1;
24+
self
25+
}
26+
27+
pub const fn count_wildcard(mut self) -> Self {
28+
self.wildcards_count += 1;
29+
self
30+
}
31+
}
32+
33+
impl PartialOrd for Specificity {
34+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
35+
Some(self.cmp(other))
36+
}
37+
}
38+
39+
impl Ord for Specificity {
40+
fn cmp(&self, other: &Self) -> Ordering {
41+
self.static_length
42+
.cmp(&other.static_length)
43+
.then(other.dynamics_count.cmp(&self.dynamics_count))
44+
.then(other.wildcards_count.cmp(&self.wildcards_count))
45+
}
46+
}

0 commit comments

Comments
 (0)