///|
pub struct Route {
verb : String
path : String
handler : (Context) -> Context
}
///|
fn without_query(path : String) -> String {
match path.split("?").next() {
Some(first) => first.to_owned()
None => path
}
}
///|
/// Split a path into non-empty segments, ignoring any query string.
fn split_path(path : String) -> Array[String] {
let segments = []
for segment in without_query(path).split("/") {
if segment != "" {
segments.push(segment.to_owned())
}
}
segments
}
///|
/// Try to match a request path against this route's pattern.
/// A `:name` pattern segment captures the actual segment as a parameter.
/// Returns the captured parameters, or None when the shape does not match.
fn Route::try_match(
self : Route,
actual : Array[String],
) -> Map[String, String]? {
let pattern = split_path(self.path)
if pattern.length() != actual.length() {
return None
}
let params : Map[String, String] = Map([])
for i, segment in pattern {
if segment.has_prefix(":") {
params[segment[1:].to_owned()] = actual[i]
} else if segment != actual[i] {
return None
}
}
Some(params)
}
///|
/// Number of literal (non-parameter) segments — the more of them a route
/// has, the more specific it is.
fn Route::specificity(self : Route) -> Int32 {
let mut count = 0
for segment in split_path(self.path) {
if !segment.has_prefix(":") {
count = count + 1
}
}
count
}
///|
/// Verbs registered for routes whose pattern (ignoring the verb) matches
/// `path`, in registration order without duplicates.
fn App::allowed_verbs(self : App, path : String) -> Array[String] {
let actual = split_path(path)
let verbs = []
for route in self.routes {
if route.try_match(actual) is Some(_) && !verbs.contains(route.verb) {
verbs.push(route.verb)
}
}
verbs
}
///|
/// Find the best matching route for a request: shape-compatible routes
/// compete on specificity (static segments win over `:param` captures,
/// registration order breaks ties). Returns the route with its captured
/// parameters.
fn App::find_route(
self : App,
verb : String,
path : String,
) -> (Route, Map[String, String])? {
let actual = split_path(path)
let mut best : Route? = None
let mut best_params : Map[String, String] = Map([])
let mut best_score = -1
for route in self.routes {
if route.verb == verb {
match route.try_match(actual) {
Some(params) => {
let score = route.specificity()
if score > best_score {
best = Some(route)
best_params = params
best_score = score
}
}
None => ()
}
}
}
match best {
Some(route) => Some((route, best_params))
None => None
}
}