Skip to content

Commit

Permalink
Merge pull request #93 from tirr-c/catch-all
Browse files Browse the repository at this point in the history
Implement "catch-all" endpoint
  • Loading branch information
aturon authored Dec 3, 2018
2 parents df9876b + 4410f4c commit 721f954
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 8 deletions.
16 changes: 16 additions & 0 deletions examples/catch_all.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![feature(async_await, futures_api)]

async fn echo_path(path: tide::head::Path<String>) -> String {
format!("Your path is: {}", *path)
}

fn main() {
let mut app = tide::App::new(());
app.at("/echo_path").nest(|router| {
router.at("*").get(echo_path);
});

let address = "127.0.0.1:8000".to_owned();
println!("Server is listening on http://{}", address);
app.serve(address);
}
135 changes: 128 additions & 7 deletions path_table/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,25 @@ pub struct PathTable<R> {
wildcard: Option<Box<Wildcard<R>>>,
}

#[derive(Copy, Clone, PartialEq)]
enum WildcardKind {
Segment,
CatchAll,
}

impl std::fmt::Display for WildcardKind {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
WildcardKind::Segment => Ok(()),
WildcardKind::CatchAll => write!(fmt, "*"),
}
}
}

#[derive(Clone)]
struct Wildcard<R> {
name: String,
count_mod: WildcardKind,
table: PathTable<R>,
}

Expand All @@ -40,7 +56,10 @@ impl<R> std::fmt::Debug for PathTable<R> {
let mut dbg = fmt.debug_map();
dbg.entries(self.0.iter());
if let Some(wildcard) = self.1 {
dbg.entry(&format_args!("{{{}}}", wildcard.name), &wildcard.table);
dbg.entry(
&format_args!("{{{}}}{}", wildcard.name, wildcard.count_mod),
&wildcard.table,
);
}
dbg.finish()
}
Expand Down Expand Up @@ -108,26 +127,62 @@ impl<R> PathTable<R> {
let mut params = Vec::new();
let mut param_map = HashMap::new();

for segment in path.split('/') {
// Find all segments with their indices.
let segment_iter = path
.match_indices('/')
.chain(std::iter::once((path.len(), "")))
.scan(0usize, |prev_idx, (idx, _)| {
let starts_at = *prev_idx;
let segment = &path[starts_at..idx];
*prev_idx = idx + 1;
Some((starts_at, segment))
});

for (starts_at, mut segment) in segment_iter {
if segment.is_empty() {
continue;
}

if let Some(next_table) = table.next.get(segment) {
table = next_table;
} else if let Some(wildcard) = &table.wildcard {
let last = if wildcard.count_mod == WildcardKind::CatchAll {
segment = &path[starts_at..];
true
} else {
false
};

params.push(segment);

if !wildcard.name.is_empty() {
param_map.insert(&*wildcard.name, segment);
}

table = &wildcard.table;

if last {
break;
}
} else {
return None;
}
}

if table.accept.is_none() {
if let Some(wildcard) = &table.wildcard {
if wildcard.count_mod == WildcardKind::CatchAll {
params.push("");

if !wildcard.name.is_empty() {
param_map.insert(&*wildcard.name, "");
}

table = &wildcard.table;
}
}
}

table.accept.as_ref().map(|res| {
(
res,
Expand All @@ -148,24 +203,53 @@ impl<R> PathTable<R> {
/// If it doesn't already exist, this will make a new one.
pub fn setup_table(&mut self, path: &str) -> &mut PathTable<R> {
let mut table = self;
let mut forbid_next = false;
for segment in path.split('/') {
if segment.is_empty() {
continue;
}

if segment.starts_with('{') && segment.ends_with('}') {
let name = &segment[1..segment.len() - 1];
if forbid_next {
panic!("No segments are allowed after wildcard with `*` modifier");
}

let wildcard_opt = if segment.starts_with('{') {
if segment.ends_with('}') {
Some((&segment[1..segment.len() - 1], WildcardKind::Segment))
} else if segment.ends_with("}*") {
Some((&segment[1..segment.len() - 2], WildcardKind::CatchAll))
} else {
None
}
} else if segment == "*" {
Some(("", WildcardKind::CatchAll))
} else {
None
};

if let Some((name, count_mod)) = wildcard_opt {
if count_mod != WildcardKind::Segment {
forbid_next = true;
}

if table.wildcard.is_none() {
table.wildcard = Some(Box::new(Wildcard {
name: name.to_string(),
count_mod,
table: PathTable::new(),
}));
}

match table.wildcard_mut().unwrap() {
Wildcard { name: n, .. } if name != n => {
panic!("Route {} segment `{{{}}}` conflicts with existing wildcard segment `{{{}}}`", path, name, n);
Wildcard {
name: n,
count_mod: c,
..
} if name != n || count_mod != *c => {
panic!(
"Route {} segment `{{{}}}{}` conflicts with existing wildcard segment `{{{}}}{}`",
path, name, count_mod, n, c
);
}
Wildcard { table: t, .. } => {
table = t;
Expand Down Expand Up @@ -427,14 +511,51 @@ mod test {
assert!(params.map.is_empty());
}

#[test]
fn wildcard_count_mod() {
let mut table: PathTable<usize> = PathTable::new();
*table.setup("foo/{foo}*") = 0;
*table.setup("bar/{}*") = 1;
*table.setup("baz/*") = 2;
*table.setup("foo/bar") = 3;

let (&id, params) = table.route("foo/a/b").unwrap();
assert_eq!(id, 0);
assert_eq!(params.vec, &["a/b"]);
assert_eq!(params.map, [("foo", "a/b")].iter().cloned().collect());

let (&id, params) = table.route("bar/a/b").unwrap();
assert_eq!(id, 1);
assert_eq!(params.vec, &["a/b"]);
assert!(params.map.is_empty());

let (&id, params) = table.route("baz/a/b").unwrap();
assert_eq!(id, 2);
assert_eq!(params.vec, &["a/b"]);
assert!(params.map.is_empty());

let (&id, params) = table.route("foo/bar").unwrap();
assert_eq!(id, 3);
assert!(params.vec.is_empty());
assert!(params.map.is_empty());
}

#[test]
#[should_panic]
fn conflicting_wildcard_fails() {
fn conflicting_wildcard_name_fails() {
let mut table: PathTable<()> = PathTable::new();
*table.setup("foo/{foo}");
*table.setup("foo/{bar}");
}

#[test]
#[should_panic]
fn conflicting_wildcard_modifier_fails() {
let mut table: PathTable<()> = PathTable::new();
table.setup("foo/{foo}*");
table.setup("foo/{foo}");
}

#[test]
fn iter() {
let mut table: PathTable<usize> = PathTable::new();
Expand Down
11 changes: 10 additions & 1 deletion src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,17 @@ impl<Data: Clone + Send + Sync + 'static> Router<Data> {
/// match the respective part of the path of the incoming request. A wildcard segment on the
/// other hand extracts and parses the respective part of the path of the incoming request to
/// pass it along to the endpoint as an argument. A wildcard segment is either defined by "{}"
/// or by "{name}" for a so called named wildcard segment which must have an implementation of
/// or by "{name}" for a so called named wildcard segment which can be extracted using
/// `NamedSegment`. It is not possible to define wildcard segments with different names for
/// otherwise identical paths.
///
/// Wildcard definitions can be followed by an optional *wildcard modifier*. Currently, there is
/// only one modifier: `*`, which means that the wildcard will match to the end of given path,
/// no matter how many segments are left, even nothing. If there is a modifier for unnamed
/// wildcard definition, `{}` may be omitted. That is, `{}*` can be written as `*`. It is an
/// error to define two wildcard segments with different wildcard modifiers, or to write other
/// path segment after a segment with wildcard modifier.
///
/// Here are some examples omitting the HTTP verb based endpoint selection:
///
/// ```rust,no_run
Expand All @@ -85,6 +92,8 @@ impl<Data: Clone + Send + Sync + 'static> Router<Data> {
/// app.at("/hello");
/// app.at("/message/{}");
/// app.at("add_two/{num}");
/// app.at("static/{path}*");
/// app.at("single_page_app/*");
/// ```
///
/// Notice that there is no fallback route matching, i.e. either a resource is a full match or
Expand Down

0 comments on commit 721f954

Please sign in to comment.