Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modernize Mount #66

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ license = "MIT"
iron = "*"
url = "*"
sequence_trie = "*"

[dev-dependencies]
stainless = "*"
iron-test = "*"

19 changes: 17 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
#![crate_name = "mount"]

#![deny(missing_docs)]
#![cfg_attr(test, deny(warnings))]
#![feature(core, path)]

#![feature(core, path, collections)]
#![cfg_attr(test, feature(plugin, io, test))]

//! `Mount` provides mounting middleware for the Iron framework.

extern crate iron;
extern crate url;
extern crate sequence_trie;

pub use mount::{Mount, OriginalUrl};
#[cfg(test)] #[plugin]
extern crate stainless;

#[cfg(test)]
extern crate test;

#[cfg(test)]
extern crate "iron-test" as itest;

pub use mount::{Mount, VirtualRoot, OriginalUrl, NoMatch};

mod mount;

#[cfg(test)]
mod tests;

83 changes: 52 additions & 31 deletions src/mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ use sequence_trie::SequenceTrie;
use std::fmt;

/// Exposes the original, unmodified path to be stored in `Request::extensions`.
#[derive(Copy)]
#[derive(Debug, Copy)]
pub struct OriginalUrl;
impl typemap::Key for OriginalUrl { type Value = Url; }

/// Exposes the mounting path, so a request can know its own relative address.
#[derive(Debug, Copy)]
pub struct VirtualRoot;
impl typemap::Key for VirtualRoot { type Value = Url; }

/// `Mount` is a simple mounting middleware.
///
/// Mounting allows you to install a handler on a route and have it receive requests as if they
Expand All @@ -25,7 +30,7 @@ pub struct Mount {
}

struct Match {
handler: Box<Handler + Send + Sync>,
handler: Box<Handler>,
length: usize
}

Expand Down Expand Up @@ -57,68 +62,84 @@ impl Mount {
/// For a given request, the *most specific* handler will be selected.
///
/// Existing handlers on the same route will be overwritten.
pub fn mount<H: Handler>(&mut self, route: &str, handler: H) -> &mut Mount {
pub fn on<H: Handler>(&mut self, route: &str, handler: H) -> &mut Mount {
// Parse the route into a list of strings. The unwrap is safe because strs are UTF-8.
let key: Vec<String> = Path::new(route).str_components().map(|s| s.unwrap().to_string()).collect();
let key = Path::new(route).str_components()
.map(|s| s.unwrap().to_string()).collect::<Vec<_>>();

// Insert a match struct into the trie.
self.inner.insert(key.as_slice(), Match {
handler: Box::new(handler) as Box<Handler + Send + Sync>,
handler: Box::new(handler) as Box<Handler>,
length: key.len()
});
self
}

/// The old way to mount handlers.
#[deprecated = "use .on instead"]
pub fn mount<H: Handler>(&mut self, route: &str, handler: H) -> &mut Mount {
self.on(route, handler)
}
}

impl Handler for Mount {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
let original = req.url.path.clone();

// If present, remove the trailing empty string (which represents a trailing slash).
// If it isn't removed the path will never match anything, because
// Path::str_components ignores trailing slashes and will never create routes
// ending in "".
let mut root = original.as_slice();
while root.last().map(|s| &**s) == Some("") {
root = &root[..root.len() - 1];
}

// Find the matching handler.
let matched = {
// Extract the request path.
let path = req.url.path.as_slice();

// If present, remove the trailing empty string (which represents a trailing slash).
// If it isn't removed the path will never match anything, because
// Path::str_components ignores trailing slashes and will never create routes
// ending in "".
let key = match path.last() {
Some(s) if s.is_empty() => &path[..path.len() - 1],
_ => path
};

// Search the Trie for the nearest most specific match.
match self.inner.get_ancestor(key) {
Some(matched) => matched,
None => return Err(IronError::new(NoMatch, status::NotFound))
}
let matched = match self.inner.get_ancestor(root) {
Some(matched) => matched,
None => return Err(IronError::new(NoMatch, status::NotFound))
};

// We have a match, so fire off the child.
// If another mount middleware hasn't already, insert the unmodified url
// into the extensions as the "original url".
let is_outer_mount = !req.extensions.contains::<OriginalUrl>();
if is_outer_mount {
let mut root_url = req.url.clone();
root_url.path = root.to_vec();

req.extensions.insert::<OriginalUrl>(req.url.clone());
req.extensions.insert::<VirtualRoot>(root_url);
} else {
req.extensions.get_mut::<VirtualRoot>().map(|old| {
old.path.push_all(root);
});
}

// Remove the prefix from the request's path before passing it to the mounted handler.
// If the prefix is entirely removed and no trailing slash was present, the new path
// will be the empty list. For the purposes of redirection, conveying that the path
// did not include a trailing slash is more important than providing a non-empty list.
// Remove the prefix from the request's path before passing it to the mounted
// handler. If the prefix is entirely removed and no trailing slash was present,
// the new path will be the empty list.
//
// For the purposes of redirection, conveying that the path did not include
// a trailing slash is more important than providing a non-empty list.
req.url.path = req.url.path.as_slice()[matched.length..].to_vec();

let res = matched.handler.handle(req);

// Reverse the URL munging, for future middleware.
req.url = match req.extensions.get::<OriginalUrl>() {
Some(original) => original.clone(),
None => panic!("OriginalUrl unexpectedly removed from req.extensions.")
};
req.url.path = original.clone();

// If this mount middleware is the outermost mount middleware,
// remove the original url from the extensions map to prevent leakage.
if is_outer_mount {
req.extensions.remove::<OriginalUrl>();
req.extensions.remove::<VirtualRoot>();
} else {
req.extensions.get_mut::<VirtualRoot>().map(|old| {
let old_len = old.path.len();
old.path.truncate(old_len - root.len());
});
}

res
Expand Down
77 changes: 77 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
pub use iron::prelude::*;
pub use iron::Handler;
pub use iron::{status, method};

// FIXME(reem): Write tests for OriginalUrl and VirtualRoot
pub use {Mount, OriginalUrl, VirtualRoot, NoMatch};

pub fn at(mount: &Mount, url: &str) -> Result<String, IronError> {
use std::old_io::util::NullReader;
use itest::mock::request;
use iron::Url;

let url = Url::parse(&format!("http://localhost:3000{}", url)).unwrap();
let rdr = &mut NullReader;
let mut req = request::new(method::Get, url, rdr);

mount.handle(&mut req).map(|res| {
res.body.unwrap_or(Box::new(NullReader)).read_to_string().unwrap()
})
}

describe! mount {
before_each {
let mut mount = Mount::new();

mount.on("/hello", |_: &mut Request| {
Ok(Response::with((status::Ok, "hello")))
});

mount.on("/intercept", |_: &mut Request| {
Ok(Response::with((status::Ok, "intercepted")))
});

mount.on("/trailing_slashes///", |_: &mut Request| {
Ok(Response::with((status::Ok, "trailing slashes")))
});

mount.on("/trailing_slash/", |_: &mut Request| {
Ok(Response::with((status::Ok, "trailing slash")))
});
}

it "should mount handlers" {
assert_eq!(&*at(&mount, "/hello").unwrap(), "hello");
assert_eq!(&*at(&mount, "/hello/and/more").unwrap(), "hello");

assert_eq!(&*at(&mount, "/intercept").unwrap(), "intercepted");
assert_eq!(&*at(&mount, "/intercept/with/more").unwrap(), "intercepted");
}

it "should work with trailing slashes" {
assert_eq!(&*at(&mount, "/hello/").unwrap(), "hello");
assert_eq!(&*at(&mount, "/hello//").unwrap(), "hello");
assert_eq!(&*at(&mount, "/hello//and/more").unwrap(), "hello");

assert_eq!(&*at(&mount, "/trailing_slash").unwrap(), "trailing slash");
assert_eq!(&*at(&mount, "/trailing_slash///").unwrap(), "trailing slash");
assert_eq!(&*at(&mount, "/trailing_slash/with_more").unwrap(), "trailing slash");
assert_eq!(&*at(&mount, "/trailing_slash//crazy/with_more").unwrap(), "trailing slash");

assert_eq!(&*at(&mount, "/trailing_slashes").unwrap(), "trailing slashes");
assert_eq!(&*at(&mount, "/trailing_slashes/").unwrap(), "trailing slashes");
assert_eq!(&*at(&mount, "/trailing_slashes///").unwrap(), "trailing slashes");
assert_eq!(
&*at(&mount, "/trailing_slashes///with_extra/crazy").unwrap(),
"trailing slashes"
);
}

it "should throw when no match is found" {
let err = at(&mount, "/notfound").unwrap_err();

assert_eq!(err.response.status, Some(status::NotFound));
err.error.downcast::<NoMatch>().unwrap();
}
}