1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
use std::error::Error;
use std::fmt;
use std::collections::BTreeMap;

use iron::{Request, Response, Handler, IronResult, IronError, Url};
use iron::status;
use iron::typemap::Key;

use recognizer::Router as Recognizer;
use recognizer::Params as RecognizerParams;
use recognizer::Match;

use url::parse_path;

const LEFTOVER_PARAM_NAME: &'static str = "mountrouter_leftover";

// Can't use 0x00 here because route-recognizer does
// let val = char as u32 - 1;
const ENDSTRING: char = '\x01';

/// `Params` is stored in request extensions and gives access to captured dynamic parameters
///
/// ```ignore
/// let ref params = req.extensions.get::<Router>().unwrap()
/// ```
pub type Params = BTreeMap<String, String>;

struct RouteParams {
	handler: Box<Handler>,
}

/// `Router` provides an interface to apply different handlers to different routes as middleware for
/// the Iron framework.
pub struct Router {
    recognizer: Recognizer<RouteParams>,
    mounted_recognizer: Recognizer<RouteParams>,
}

impl Router {
    /// Construct new `Router`.
    ///
    /// ```
    /// # use iron_mountrouter::Router;
    /// let router = Router::new();
    /// ```
    pub fn new() -> Router {
        Router {
            recognizer: Recognizer::new(),
            mounted_recognizer: Recognizer::new(),
        }
    }

    /// Add a new route to a `Router`, matching given pattern.
    ///
    /// `route` is a pattern supported by [route recognizer]
    /// (https://crates.io/crates/route-recognizer):
    ///
    /// string like `/static/page/here` is matched entirely;
    ///
    /// `:name` is a dynamic param, matching any string that does not contain slash `/`, i.e.
    ///
    /// `/page/:page-num/content` will match `/page/17/content` or `/page/abbra-t/content`
    /// but not `/page/17/5/content`.
    ///
    /// Colon only works at the start of the segment so `/page-:num` works like usual string match
    /// and will not match `/page-5`.
    ///
    /// Currently there is no way to set param type so it catches only strings and does not perform
    /// any additional checks.
    ///
    /// `*name` is a dynamic wildcard param, matching any string (even those containing slashes`/`).
    /// `/redirect/to/*path` will match `/redirect/to/example.com/example`.
    ///
    /// Captured parameters are stored in [Params](type.Params.html) type and can be accessed
    /// in request extensions:
    ///
    /// ```ignore
    /// let ref params = req.extensions.get::<Router>().unwrap()
    /// ```
    ///
    /// Unwrap is safe here because router always adds extension if route is matched, even if no
    /// params were captured.
    ///
    /// `handler` is any [Iron handler]
    /// (http://ironframework.io/doc/iron/middleware/trait.Handler.html). Working with mountrouter
    /// you will most often use [MethodPicker](struct.MethodPicker.html) to pick appropriate
    /// handler for given http method.
    ///
    /// If `is_mounted` is true, router will match any string *starting* with route. Original url is
    /// preserved, while stripped part is stored in request extensions using `StrippedUrl`. Further
    /// `Router`s will used stripped url to match their routes. For example,
    ///
    /// ```ignore
    /// let mut router = Router::new();
    /// router.add_route("/book/:page/", handler, true);
    /// fn handler(req: &mut Request) -> IronResult<Response> {
    ///     // for path "/book/17/page/3"
    ///
    ///     // ["book", "17", "page", "3"]
    ///     println!("{:?}", req.url.path);
    ///     // ["page", "3"]
    ///     println!("{:?}", req.extensions.get::<StrippedUrl>().unwrap().path);
    /// }
    /// ```
    /// If `is_mounted` is set, route is forced to end with slash: slash is appended to the end of
    /// route if it is not there.
    pub fn add_route<H, S>(&mut self, route: S, handler: H, is_mounted: bool) -> &mut Router
    where H: Handler, S: AsRef<str> {
    	let params = RouteParams {
    		handler: Box::new(handler),
    	};
    	if is_mounted {
    		let r = route.as_ref();
    		let mut modified_route = String::with_capacity(r.len() + 2 + LEFTOVER_PARAM_NAME.len());
    		modified_route.push_str(r);
    		if !r.ends_with('/') {
				modified_route.push('/');
			}
    		modified_route.push('*');
    		modified_route.push_str(LEFTOVER_PARAM_NAME);
	        self.mounted_recognizer.add(&modified_route, params);
    	} else {
	        self.recognizer.add(route.as_ref(), params);
    	}
        self
    }

    fn recognize(&self, path: &str) -> MatchResult<Match<&RouteParams>> {
    	match self.recognizer.recognize(path) {
    		Ok(res) => MatchResult::Direct(res),
    		Err(_) => {
    			let mut modified_path = path.to_string();
    			modified_path.push(ENDSTRING);
    			match self.mounted_recognizer.recognize(&modified_path) {
    				Ok(res) => MatchResult::Mounted(res),
    				Err(_) => MatchResult::None,
    			}
    		}
    	}
    }
    
    fn append_params(req: &mut Request, params: &RecognizerParams) {
    	if !req.extensions.contains::<Router>() {
			req.extensions.insert::<Router>(Params::new());
    	}
    	let p = req.extensions.get_mut::<Router>().unwrap();
    	for name in params.keys() {
    		if name != LEFTOVER_PARAM_NAME {
    			p.insert(name.to_string(), params[name.as_ref()].clone());
			}
    	}
    }
}

impl Key for Router { type Value = Params; }

impl Handler for Router {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
    	let path = if req.extensions.contains::<StrippedUrl>() {
    		req.extensions.get::<StrippedUrl>().unwrap().path.join("/")
    	} else {
	        req.url.path.join("/")
    	};
        match self.recognize(&path) {
        	MatchResult::Direct(matched) => {
            	Router::append_params(req, &matched.params);
	            matched.handler.handler.handle(req)
        	},
        	MatchResult::Mounted(matched) => {
        		let len = matched.params[LEFTOVER_PARAM_NAME].len();
            	let leftover = "/".to_string() + &matched.params[LEFTOVER_PARAM_NAME][0 .. len - 1];
            	let (new_path, _, _) = parse_path(&leftover).unwrap();
		        if !req.extensions.contains::<StrippedUrl>() {
		        	let mut stripped_url = req.url.clone();
		        	stripped_url.path = new_path;
		            req.extensions.insert::<StrippedUrl>(stripped_url);
		        } else {
		        	req.extensions.get_mut::<StrippedUrl>().unwrap().path = new_path;
		        }
            	Router::append_params(req, &matched.params);
	            matched.handler.handler.handle(req)
        	},
        	MatchResult::None => Err(IronError::new(NoRoute, status::NotFound))
        }
    }
}


/// `StrippedUrl` serves as a key in request extensions. It contains non-matched part of url if 
/// route was mounted
///
/// ```ignore
/// let ref stripped_url = req.extensions.get::<StrippedUrl>()
/// ```
#[derive(Copy, Clone)]
pub struct StrippedUrl;
impl Key for StrippedUrl { type Value = Url; }


/// `NoRoute` is error type returned to Iron if no route was matched in Router
#[derive(Debug)]
pub struct NoRoute;

impl fmt::Display for NoRoute {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str("No matching route found.")
    }
}

impl Error for NoRoute {
    fn description(&self) -> &str { "No Route" }
}

enum MatchResult<T> {
	Direct(T),
	Mounted(T),
	None,
}