use crate::router::Router;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppRoute {
List,
Detail,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRoute {
pub route: AppRoute,
pub id: Option<String>,
}
fn build_router() -> Router<AppRoute> {
Router::new()
.route("/", AppRoute::List)
.expect("static pattern \"/\" is valid")
.route("/items/:id", AppRoute::Detail)
.expect("static pattern \"/items/:id\" is valid")
}
fn router() -> &'static Router<AppRoute> {
static ROUTER: OnceLock<Router<AppRoute>> = OnceLock::new();
ROUTER.get_or_init(build_router)
}
pub fn resolve(path: &str) -> Option<ResolvedRoute> {
let route_match = router().resolve(path)?;
let id = match route_match.handler {
AppRoute::List => None,
AppRoute::Detail => Some(route_match.params.get("id")?.to_string()),
};
Some(ResolvedRoute {
route: *route_match.handler,
id,
})
}
pub fn title(route: AppRoute) -> &'static str {
match route {
AppRoute::List => "記事一覧",
AppRoute::Detail => "記事詳細",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_list_route() {
let resolved = resolve("/").expect("\"/\" should match");
assert_eq!(resolved.route, AppRoute::List);
assert_eq!(resolved.id, None);
}
#[test]
fn resolves_detail_route_and_captures_id() {
let resolved = resolve("/items/42").expect("should match");
assert_eq!(resolved.route, AppRoute::Detail);
assert_eq!(resolved.id, Some("42".to_string()));
}
#[test]
fn query_string_is_stripped_before_matching() {
let resolved = resolve("/items/2?ref=top").expect("should match ignoring query");
assert_eq!(resolved.id, Some("2".to_string()));
}
#[test]
fn trailing_slash_does_not_match() {
assert_eq!(resolve("/items/1/"), None);
}
#[test]
fn empty_id_segment_does_not_match() {
assert_eq!(resolve("/items/"), None);
}
#[test]
fn non_root_relative_path_does_not_match() {
assert_eq!(resolve("items/1"), None);
}
#[test]
fn unregistered_path_does_not_match() {
assert_eq!(resolve("/no-such-page"), None);
}
#[test]
fn xss_payload_like_id_is_captured_as_raw_string() {
let payload = "<img src=x onerror=alert(1)>";
let path = format!("/items/{payload}");
let resolved = resolve(&path).expect("should match");
assert_eq!(resolved.id, Some(payload.to_string()));
}
#[test]
fn titles_are_distinct_and_fixed() {
assert_eq!(title(AppRoute::List), "記事一覧");
assert_eq!(title(AppRoute::Detail), "記事詳細");
}
}