This commit is contained in:
parent
00f0aaeeff
commit
88ae866b91
2 changed files with 58 additions and 3 deletions
|
|
@ -97,4 +97,36 @@ mod tests {
|
||||||
StatusCode::CREATED
|
StatusCode::CREATED
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn dispatch_matches_terminal_slashes_for_exact_and_pattern_routes() {
|
||||||
|
let router = Router::new()
|
||||||
|
.route("/api", |_, response| async move {
|
||||||
|
response.status(StatusCode::ACCEPTED)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.route_pattern("/api/test/{id}", |_, response, _| async move {
|
||||||
|
response.status(StatusCode::CREATED)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.fallback(|_, response| async move { response.status(StatusCode::IM_A_TEAPOT) })
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for uri in ["/api/test", "/api/test/"] {
|
||||||
|
assert_eq!(
|
||||||
|
dispatch_request(request(Method::GET, uri), &router)
|
||||||
|
.await
|
||||||
|
.status,
|
||||||
|
StatusCode::ACCEPTED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for uri in ["/api/test/1", "/api/test/1/"] {
|
||||||
|
assert_eq!(
|
||||||
|
dispatch_request(request(Method::GET, uri), &router)
|
||||||
|
.await
|
||||||
|
.status,
|
||||||
|
StatusCode::CREATED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ impl Router {
|
||||||
path: String,
|
path: String,
|
||||||
handler: HttpHandler,
|
handler: HttpHandler,
|
||||||
) -> Result<Self, RouterError> {
|
) -> Result<Self, RouterError> {
|
||||||
|
let path = normalize_path(&path);
|
||||||
if self
|
if self
|
||||||
.routes
|
.routes
|
||||||
.insert((method, path.clone()), handler)
|
.insert((method, path.clone()), handler)
|
||||||
|
|
@ -166,6 +167,7 @@ impl Router {
|
||||||
pattern: String,
|
pattern: String,
|
||||||
handler: DynamicHttpHandler,
|
handler: DynamicHttpHandler,
|
||||||
) -> Result<Self, RouterError> {
|
) -> Result<Self, RouterError> {
|
||||||
|
let pattern = normalize_path(&pattern);
|
||||||
let segments = parse_pattern(&pattern)?;
|
let segments = parse_pattern(&pattern)?;
|
||||||
if self
|
if self
|
||||||
.pattern_routes
|
.pattern_routes
|
||||||
|
|
@ -189,9 +191,10 @@ impl Router {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handler(&self, method: &Method, path: &str) -> Option<HttpHandler> {
|
pub(crate) fn handler(&self, method: &Method, path: &str) -> Option<HttpHandler> {
|
||||||
|
let path = normalize_path(path);
|
||||||
self.routes
|
self.routes
|
||||||
.get(&(Some(method.clone()), path.to_string()))
|
.get(&(Some(method.clone()), path.clone()))
|
||||||
.or_else(|| self.routes.get(&(None, path.to_string())))
|
.or_else(|| self.routes.get(&(None, path)))
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,10 +203,11 @@ impl Router {
|
||||||
method: &Method,
|
method: &Method,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Option<(DynamicHttpHandler, RouteParams)> {
|
) -> Option<(DynamicHttpHandler, RouteParams)> {
|
||||||
|
let path = normalize_path(path);
|
||||||
self.pattern_routes
|
self.pattern_routes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|route| route.method.is_none() || route.method.as_ref() == Some(method))
|
.filter(|route| route.method.is_none() || route.method.as_ref() == Some(method))
|
||||||
.filter_map(|route| match_pattern(&route.segments, path).map(|params| (route, params)))
|
.filter_map(|route| match_pattern(&route.segments, &path).map(|params| (route, params)))
|
||||||
.max_by_key(|(route, _)| (route.method.is_some(), route.static_segments))
|
.max_by_key(|(route, _)| (route.method.is_some(), route.static_segments))
|
||||||
.map(|(route, params)| (route.handler.clone(), params))
|
.map(|(route, params)| (route.handler.clone(), params))
|
||||||
}
|
}
|
||||||
|
|
@ -212,6 +216,25 @@ impl Router {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Canonicalize a route path for matching.
|
||||||
|
///
|
||||||
|
/// HTTP request paths are absolute, but accepting an omitted leading slash in
|
||||||
|
/// route registration is convenient. A terminal slash (or several terminal
|
||||||
|
/// slashes) does not identify a different resource, except for the root path.
|
||||||
|
fn normalize_path(path: &str) -> String {
|
||||||
|
let path = if path.starts_with('/') {
|
||||||
|
path.to_string()
|
||||||
|
} else {
|
||||||
|
format!("/{path}")
|
||||||
|
};
|
||||||
|
let normalized = path.trim_end_matches('/');
|
||||||
|
if normalized.is_empty() {
|
||||||
|
"/".to_string()
|
||||||
|
} else {
|
||||||
|
normalized.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_pattern(pattern: &str) -> Result<Vec<PatternSegment>, RouterError> {
|
fn parse_pattern(pattern: &str) -> Result<Vec<PatternSegment>, RouterError> {
|
||||||
let path = pattern.strip_prefix('/').unwrap_or(pattern);
|
let path = pattern.strip_prefix('/').unwrap_or(pattern);
|
||||||
let path = path.strip_suffix('/').unwrap_or(path);
|
let path = path.strip_suffix('/').unwrap_or(path);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue