use dashmap::DashMap;
use std::sync::OnceLock;
static ROUTES: OnceLock<DashMap<String, String>> = OnceLock::new();
fn registry() -> &'static DashMap<String, String> {
ROUTES.get_or_init(DashMap::new)
}
pub fn register(name: impl Into<String>, pattern: impl Into<String>) {
registry().insert(name.into(), pattern.into());
}
pub fn resolve(name: &str, params: &[(&str, &str)]) -> String {
let pattern = registry()
.get(name)
.map(|r| r.clone())
.unwrap_or_else(|| panic!("rok-core: no named route registered for '{name}'"));
let mut url = pattern;
let mut query: Vec<(&str, &str)> = Vec::new();
for &(key, val) in params {
let placeholder = format!(":{key}");
if url.contains(placeholder.as_str()) {
url = url.replace(placeholder.as_str(), val);
} else {
query.push((key, val));
}
}
if !query.is_empty() {
let qs: String = query
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("&");
url = format!("{url}?{qs}");
}
url
}
#[macro_export]
macro_rules! route {
($name:expr) => {
$crate::named_routes::resolve($name, &[])
};
($name:expr, $($key:ident = $val:expr),+ $(,)?) => {
$crate::named_routes::resolve($name, &[$( (stringify!($key), &$val.to_string()) ),+])
};
}
#[cfg(test)]
mod tests {
use super::*;
fn setup() {
register("posts.show", "/posts/:id");
register("users.index", "/users");
register("posts.index", "/posts");
}
#[test]
fn resolve_path_param() {
setup();
assert_eq!(resolve("posts.show", &[("id", "42")]), "/posts/42");
}
#[test]
fn resolve_no_params() {
setup();
assert_eq!(resolve("users.index", &[]), "/users");
}
#[test]
fn resolve_query_param() {
setup();
assert_eq!(resolve("posts.index", &[("page", "2")]), "/posts?page=2");
}
#[test]
fn resolve_mixed_params() {
register("posts.comments", "/posts/:post_id/comments/:id");
let url = resolve("posts.comments", &[("post_id", "1"), ("id", "5")]);
assert_eq!(url, "/posts/1/comments/5");
}
}