pub(crate) mod matcher;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::sync::Arc;
use crate::request::Request;
use crate::response::Response;
use crate::server::ConnectionInfo;
use matcher::Segment;
#[derive(Clone)]
pub struct PathParams {
params: HashMap<String, String>,
}
impl PathParams {
#[doc(hidden)]
pub fn from_map(params: HashMap<String, String>) -> Self {
PathParams { params }
}
pub fn get(&self, name: &str) -> Option<&str> {
self.params.get(name).map(String::as_str)
}
}
#[doc(hidden)]
pub fn request_host<'a>(request: &'a Request, connection: &'a ConnectionInfo) -> &'a str {
request.headers.iter()
.find(|h| h.name.eq_ignore_ascii_case("host"))
.map(|h| h.value.as_ref())
.filter(|v: &&str| !v.is_empty())
.or(connection.sni_hostname.as_deref())
.unwrap_or("")
}
#[doc(hidden)]
pub fn host_matches(expected: &str, actual: &str) -> bool {
let actual = if expected.contains(':') {
actual
} else if let Some(rest) = actual.strip_prefix('[') {
rest.split(']').next().unwrap_or(rest)
} else {
actual.rsplit_once(':').map_or(actual, |(host, port)| if port.bytes().all(|b| b.is_ascii_digit()) { host } else { actual })
};
let expected = expected.trim_start_matches('[').trim_end_matches(']');
actual.eq_ignore_ascii_case(expected)
}
type HandlerFn =
Arc<dyn Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static>;
#[derive(Clone)]
struct Route {
method: String,
segments: Vec<Segment>,
handler: HandlerFn,
}
#[derive(Clone)]
pub struct RouteInfo {
pub method: String,
pub pattern: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RouteConflict {
pub method: String,
pub pattern: String,
pub earlier: String,
pub kind: RouteConflictKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RouteConflictKind {
Duplicate,
Shadowed,
}
impl std::fmt::Display for RouteConflict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind {
RouteConflictKind::Duplicate => write!(
f,
"route {} {} is registered twice (first as {} {}); the second never runs",
self.method, self.pattern, self.method, self.earlier
),
RouteConflictKind::Shadowed => write!(
f,
"route {} {} never runs: {} {}, registered before it, matches every path it does",
self.method, self.pattern, self.method, self.earlier
),
}
}
}
impl RouteConflict {
#[doc(hidden)]
pub fn warn_once(&self) {
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
let message = self.to_string();
let fresh = SEEN
.get_or_init(|| Mutex::new(HashSet::new()))
.lock()
.map(|mut seen| seen.insert(message.clone()))
.unwrap_or(false);
if fresh {
eprintln!("rws: warning: {message}");
}
}
}
#[doc(hidden)]
pub fn conflicts<'a>(routes: impl Iterator<Item = (&'a str, &'a [Segment])>) -> Vec<RouteConflict> {
let all: Vec<(&str, &[Segment])> = routes.collect();
(1..all.len())
.filter_map(|i| conflict_with(all[..i].iter().copied(), all[i].0, all[i].1))
.collect()
}
pub(crate) fn conflict_with<'a>(
earlier: impl Iterator<Item = (&'a str, &'a [Segment])>,
method: &str,
segments: &[Segment],
) -> Option<RouteConflict> {
if matcher::never_matches(segments) {
return None;
}
for (m, segs) in earlier {
if m != method {
continue;
}
let kind = if matcher::equivalent(segs, segments) {
RouteConflictKind::Duplicate
} else if matcher::covers(segs, segments) {
RouteConflictKind::Shadowed
} else {
continue;
};
return Some(RouteConflict {
method: method.to_string(),
pattern: matcher::segments_to_pattern(segments),
earlier: matcher::segments_to_pattern(segs),
kind,
});
}
None
}
#[derive(Clone)]
pub struct Router {
routes: Vec<Route>,
host: Option<String>,
}
impl Router {
pub fn new() -> Self {
Router { routes: Vec::new(), host: None }
}
pub fn with_host(mut self, host: &str) -> Self {
self.host = Some(host.to_string());
self
}
pub fn get<F>(self, pattern: &str, handler: F) -> Self
where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
self.add("GET", pattern, handler)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn websocket<F>(self, pattern: &str, handler: F) -> Self
where
F: Fn(crate::websocket::WebSocketConnection, PathParams) + Send + Sync + 'static,
{
let handler = Arc::new(handler);
self.get(pattern, move |req, params, _| {
let (handler, params) = (Arc::clone(&handler), params.clone());
crate::websocket::WebSocket::upgrade(req, move |ws| handler(ws, params))
})
}
pub fn post<F>(self, pattern: &str, handler: F) -> Self
where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
self.add("POST", pattern, handler)
}
pub fn put<F>(self, pattern: &str, handler: F) -> Self
where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
self.add("PUT", pattern, handler)
}
pub fn patch<F>(self, pattern: &str, handler: F) -> Self
where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
self.add("PATCH", pattern, handler)
}
pub fn delete<F>(self, pattern: &str, handler: F) -> Self
where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
self.add("DELETE", pattern, handler)
}
#[doc(hidden)]
pub fn add<F>(self, method: &str, pattern: &str, handler: F) -> Self
where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
let router = self.add_unchecked(method, pattern, handler);
if let Some(conflict) = router.last_route_conflict() {
conflict.warn_once();
}
router
}
pub(crate) fn add_unchecked<F>(mut self, method: &str, pattern: &str, handler: F) -> Self
where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
self.routes.push(Route {
method: method.to_string(),
segments: matcher::parse_pattern(pattern),
handler: Arc::new(handler),
});
self
}
pub fn route<H: crate::handler::Handler<()>>(self, handler: H) -> Self {
let unit = Arc::new(());
self.add(H::METHOD, H::PATTERN, move |request, params, connection| {
handler.handle(request, params, connection, &unit)
})
}
pub fn route_conflicts(&self) -> Vec<RouteConflict> {
conflicts(self.routes.iter().map(|r| (r.method.as_str(), r.segments.as_slice())))
}
fn last_route_conflict(&self) -> Option<RouteConflict> {
let (last, earlier) = self.routes.split_last()?;
conflict_with(earlier.iter().map(|r| (r.method.as_str(), r.segments.as_slice())), &last.method, &last.segments)
}
pub fn route_entries(&self) -> Vec<RouteInfo> {
self.routes.iter().map(|r| RouteInfo {
method: r.method.clone(),
pattern: matcher::segments_to_pattern(&r.segments),
}).collect()
}
pub fn handle(&self, request: &Request, connection: &ConnectionInfo) -> Option<Response> {
let (route, params) = self.find(request, connection)?;
Some((route.handler)(request, ¶ms, connection))
}
pub fn matches(&self, request: &Request, connection: &ConnectionInfo) -> bool {
self.find(request, connection).is_some()
}
fn find(&self, request: &Request, connection: &ConnectionInfo) -> Option<(&Route, PathParams)> {
if let Some(required_host) = &self.host {
if !host_matches(required_host, request_host(request, connection)) {
return None;
}
}
let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
for route in &self.routes {
if route.method != request.method {
continue;
}
if let Some(params) = matcher::try_match(&route.segments, &path_segs) {
return Some((route, PathParams::from_map(params)));
}
}
None
}
}