use std::fmt::Display;
use crate::components::swap::SwapKey;
pub trait RouteTag {
const PATH: &'static str;
const PARAMS: &'static [&'static str] = &[];
}
pub trait RouteUrl: RouteTag + Sized {
fn path(self) -> String;
fn url(self) -> String {
nav_url(&self.path())
}
}
pub trait AppPaneGet: RouteTag {}
pub trait AppPanePost: RouteTag {}
pub trait FragmentGet<K: SwapKey>: RouteTag {}
pub trait FragmentPost<K: SwapKey>: RouteTag {}
pub trait FileDownloadGet: RouteTag {}
pub trait FileDownloadPost: RouteTag {}
pub trait ModalGet: RouteTag {}
pub trait FkSelectGet<K: SwapKey, M: SwapKey>: RouteTag {}
pub trait BoostPost: RouteTag {}
pub trait GenerationPost: RouteTag {}
pub struct RouteQueryBuilder<R: RouteUrl> {
route: R,
query: Vec<(String, String)>,
}
impl<R: RouteUrl> RouteQueryBuilder<R> {
pub fn new(route: R) -> Self {
Self {
route,
query: Vec::new(),
}
}
pub fn query(mut self, key: &str, value: impl Display) -> Self {
self.query.push((key.to_owned(), value.to_string()));
self
}
pub fn query_opt(self, key: &str, value: Option<impl Display>) -> Self {
match value {
Some(v) => self.query(key, v),
None => self,
}
}
pub fn build_with_query(self) -> String {
crate::components::nav_origin::with_nav_origin(&append_query(
self.route.path(),
&self.query,
))
}
pub fn build(self) -> String {
crate::components::nav_origin::with_nav_origin(&append_query(
trailing_slash(&self.route.path()),
&self.query,
))
}
}
pub fn trailing_slash(path: &str) -> String {
if path.is_empty() || path.ends_with('/') {
path.to_owned()
} else {
format!("{path}/")
}
}
pub fn nav_url(path: &str) -> String {
crate::components::nav_origin::nav_url(path)
}
fn append_query(path: String, query: &[(String, String)]) -> String {
if query.is_empty() {
return path;
}
let mut out = path;
out.push('?');
for (i, (key, value)) in query.iter().enumerate() {
if i > 0 {
out.push('&');
}
encode_query_component(&mut out, key);
out.push('=');
encode_query_component(&mut out, value);
}
out
}
fn encode_query_component(out: &mut String, s: &str) {
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(char::from(b));
}
_ => {
out.push('%');
out.push(char::from(b"0123456789ABCDEF"[(b >> 4) as usize]));
out.push(char::from(b"0123456789ABCDEF"[(b & 0xf) as usize]));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestIdRoute {
id: i64,
}
impl RouteTag for TestIdRoute {
const PATH: &'static str = "/items/{id}/edit";
const PARAMS: &'static [&'static str] = &["id"];
}
impl RouteUrl for TestIdRoute {
fn path(self) -> String {
format!("/items/{}/edit", self.id)
}
}
struct TestStaticRoute;
impl RouteTag for TestStaticRoute {
const PATH: &'static str = "/proposals/create";
}
impl RouteUrl for TestStaticRoute {
fn path(self) -> String {
Self::PATH.to_owned()
}
}
#[test]
fn route_url_adds_slash() {
assert_eq!(TestStaticRoute.url(), "/proposals/create/");
assert_eq!(TestIdRoute { id: 1 }.url(), "/items/1/edit/");
}
#[test]
fn query_params_and_encoding() {
assert_eq!(
RouteQueryBuilder::new(TestStaticRoute)
.query("ClientID", 42)
.query("return", "client")
.build_with_query(),
"/proposals/create?ClientID=42&return=client"
);
assert_eq!(
RouteQueryBuilder::new(TestStaticRoute)
.query("identifier", "a+b")
.build_with_query(),
"/proposals/create?identifier=a%2Bb"
);
}
#[test]
fn query_opt_skips_none() {
assert_eq!(
RouteQueryBuilder::new(TestStaticRoute)
.query("ClientID", 1)
.query_opt("return", None::<&str>)
.build_with_query(),
"/proposals/create?ClientID=1"
);
}
#[tokio::test]
async fn url_and_query_builder_carry_dashboard_origin() {
crate::components::nav_origin::scope_from_dashboard(true, async {
#[cfg(feature = "plugin-dashboard")]
{
assert_eq!(TestStaticRoute.url(), "/proposals/create/?from=dashboard");
assert_eq!(
RouteQueryBuilder::new(TestStaticRoute)
.query("tab", "active")
.build(),
"/proposals/create/?tab=active&from=dashboard"
);
assert_eq!(
RouteQueryBuilder::new(TestStaticRoute)
.query("tab", "active")
.build(),
RouteQueryBuilder::new(TestStaticRoute)
.query("tab", "active")
.query("from", "dashboard")
.build()
);
}
#[cfg(not(feature = "plugin-dashboard"))]
{
assert_eq!(TestStaticRoute.url(), "/proposals/create/");
}
})
.await;
}
}