///|
// Route matching. A request is matched on its normalized Host and path; among
// all matching routes the most specific one wins (a named host beats any host,
// an exact path beats a prefix, a longer prefix beats a shorter one). This is
// decided by specificity rather than array order so adding a broad catch-all
// route never shadows a precise rule listed later.
///|
/// Strip the port a Host header often carries (`example.com:8080`) and
/// lowercase it, so host matching compares names only and is case-insensitive.
pub fn normalize_host(host : String) -> String {
let h = match host.find(":") {
Some(colon) => host[0:colon].to_owned()
None => host
}
h.to_lower()
}
///|
/// Whether a host matcher accepts the (already normalized, lowercase) host.
pub fn host_matches(matcher : HostMatch, host : String) -> Bool {
match matcher {
Any => true
Host(name) => name.to_lower() == host
}
}
///|
/// Whether a path matcher accepts the request path. `Exact` compares the whole
/// path; `Prefix` tests a literal prefix (nginx semantics), which matches the
/// prefix itself and everything beneath it.
pub fn path_matches(matcher : PathMatch, path : String) -> Bool {
match matcher {
Exact(want) => want == path
Prefix(prefix) => path.has_prefix(prefix)
}
}
///|
/// Whether a route matches this host and path.
pub fn route_matches(route : Route, host : String, path : String) -> Bool {
host_matches(route.host, host) && path_matches(route.path, path)
}
///|
/// A route's specificity score; higher wins. Named hosts outrank any host, and
/// within the same host exact paths outrank prefixes, with longer prefixes more
/// specific than shorter ones.
pub fn specificity(route : Route) -> Int {
let host_score = match route.host {
Any => 0
Host(_) => 1_000_000
}
let path_score = match route.path {
Exact(want) => 100_000 + want.length()
Prefix(prefix) => prefix.length()
}
host_score + path_score
}
///|
/// Select the most specific route matching `host` and `path`, or `None` when no
/// route matches (the runtime answers 404 in that case). The host argument is
/// normalized before comparison.
pub fn select_route(
routes : Array[Route],
host : String,
path : String,
) -> Route? {
let h = normalize_host(host)
let mut best : Route? = None
let mut best_score = -1
for route in routes {
if route_matches(route, h, path) {
let score = specificity(route)
if score > best_score {
best_score = score
best = Some(route)
}
}
}
best
}