add router core
This commit is contained in:
22 files changed
+5005
-1
No files matched your search
@@ -0,0 +1,144 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use urlencoding::encode;
|
||||
|
||||
use crate::{
|
||||
segments::{NameMap, NamedSegment},
|
||||
Name,
|
||||
};
|
||||
|
||||
pub fn resolve_name(
|
||||
map: &NameMap,
|
||||
name: &Name,
|
||||
parameters: &HashMap<Name, String>,
|
||||
) -> Option<String> {
|
||||
debug_assert!(
|
||||
map.contains_key(&name),
|
||||
"named navigation to unknown name: {name}"
|
||||
);
|
||||
let target = map.get(&name)?;
|
||||
|
||||
let mut res = String::new();
|
||||
for t in target {
|
||||
res += "/";
|
||||
match t {
|
||||
NamedSegment::Fixed(f) => res += f,
|
||||
NamedSegment::Parameter(p) => {
|
||||
debug_assert!(
|
||||
parameters.contains_key(p),
|
||||
"named navigation is missing parameter: target {name} parameter {p}"
|
||||
);
|
||||
let val = parameters.get(p)?;
|
||||
|
||||
res += &encode(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if res.is_empty() {
|
||||
res += "/";
|
||||
}
|
||||
|
||||
Some(res)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
prelude::RootIndex,
|
||||
routes::{ParameterRoute, Route, Segment},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_map() -> NameMap {
|
||||
NamedSegment::from_segment(
|
||||
&Segment::<&str>::empty()
|
||||
.fixed(
|
||||
"fixed",
|
||||
Route::empty().name::<u8>().nested(
|
||||
Segment::empty().catch_all(ParameterRoute::empty::<u16>().name::<u32>()),
|
||||
),
|
||||
)
|
||||
.catch_all(ParameterRoute::empty::<i8>().name::<i16>()),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_index() {
|
||||
assert_eq!(
|
||||
resolve_name(&test_map(), &Name::of::<RootIndex>(), &HashMap::new()),
|
||||
Some(String::from("/"))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed() {
|
||||
assert_eq!(
|
||||
resolve_name(&test_map(), &Name::of::<u8>(), &HashMap::new()),
|
||||
Some(String::from("/fixed"))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching() {
|
||||
assert_eq!(
|
||||
resolve_name(&test_map(), &Name::of::<i16>(), &{
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<i8>(), String::from("test"));
|
||||
r
|
||||
}),
|
||||
Some(String::from("/test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested() {
|
||||
assert_eq!(
|
||||
resolve_name(&test_map(), &Name::of::<u32>(), &{
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<u16>(), String::from("nested"));
|
||||
r
|
||||
}),
|
||||
Some(String::from("/fixed/nested"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "named navigation to unknown name: bool"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn missing_name_debug() {
|
||||
resolve_name(&test_map(), &Name::of::<bool>(), &HashMap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn missing_name_release() {
|
||||
assert!(resolve_name(&test_map(), &Name::of::<bool>(), &HashMap::new()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "named navigation is missing parameter: target u32 parameter u16"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn missing_parameter_debug() {
|
||||
resolve_name(&test_map(), &Name::of::<u32>(), &HashMap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn missing_parameter_release() {
|
||||
assert!(resolve_name(&test_map(), &Name::of::<u32>(), &HashMap::new()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_encoding() {
|
||||
assert_eq!(
|
||||
resolve_name(&test_map(), &Name::of::<u32>(), &{
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<u16>(), String::from("🥳"));
|
||||
r
|
||||
}),
|
||||
Some(String::from("/fixed/%F0%9F%A5%B3"))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
use either::Either;
|
||||
use urlencoding::decode;
|
||||
|
||||
use crate::{
|
||||
navigation::NavigationTarget,
|
||||
routes::{ParameterRoute, Route, RouteContent, Segment},
|
||||
RouterState,
|
||||
};
|
||||
|
||||
pub fn route_segment<T: Clone>(
|
||||
segment: &Segment<T>,
|
||||
values: &[&str],
|
||||
state: RouterState<T>,
|
||||
) -> Either<RouterState<T>, NavigationTarget> {
|
||||
route_segment_internal(segment, values, state, None, false)
|
||||
}
|
||||
|
||||
fn route_segment_internal<T: Clone>(
|
||||
segment: &Segment<T>,
|
||||
values: &[&str],
|
||||
state: RouterState<T>,
|
||||
mut fallback: Option<RouteContent<T>>,
|
||||
mut clear_fallback: bool,
|
||||
) -> Either<RouterState<T>, NavigationTarget> {
|
||||
// fallback
|
||||
if let Some(fb) = &segment.fallback {
|
||||
fallback = Some(fb.clone());
|
||||
}
|
||||
if let Some(clear) = &segment.clear_fallback {
|
||||
clear_fallback = *clear;
|
||||
}
|
||||
|
||||
// index route
|
||||
if values.is_empty() {
|
||||
if let Some(c) = &segment.index {
|
||||
return merge(state, c.clone());
|
||||
}
|
||||
return Either::Left(state);
|
||||
}
|
||||
|
||||
// fixed route
|
||||
if let Some(r) = segment.fixed.get(values[0]) {
|
||||
return merge_route(values, r, state, fallback, clear_fallback);
|
||||
}
|
||||
|
||||
// matching routes
|
||||
for (m, r) in &segment.matching {
|
||||
if m.matches(values[0]) {
|
||||
return merge_parameter_route(values, r, state, fallback, clear_fallback);
|
||||
}
|
||||
}
|
||||
|
||||
// catchall
|
||||
if let Some(c) = &segment.catch_all {
|
||||
return merge_parameter_route(values, c.as_ref(), state, fallback, clear_fallback);
|
||||
}
|
||||
|
||||
merge_fallback(state, fallback, clear_fallback)
|
||||
}
|
||||
|
||||
fn merge<T: Clone>(
|
||||
mut state: RouterState<T>,
|
||||
content: RouteContent<T>,
|
||||
) -> Either<RouterState<T>, NavigationTarget> {
|
||||
match content {
|
||||
RouteContent::Content(c) => state.content.push(c),
|
||||
RouteContent::Redirect(t) => return Either::Right(t),
|
||||
RouteContent::MultiContent { main, named } => {
|
||||
if let Some(main) = main {
|
||||
state.content.push(main);
|
||||
}
|
||||
|
||||
for (name, content) in named {
|
||||
state.named_content.entry(name).or_default().push(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
Either::Left(state)
|
||||
}
|
||||
|
||||
fn merge_route<T: Clone>(
|
||||
values: &[&str],
|
||||
route: &Route<T>,
|
||||
mut state: RouterState<T>,
|
||||
fallback: Option<RouteContent<T>>,
|
||||
clear_fallback: bool,
|
||||
) -> Either<RouterState<T>, NavigationTarget> {
|
||||
// merge content
|
||||
if let Some(c) = &route.content {
|
||||
match merge(state, c.clone()) {
|
||||
Either::Left(s) => state = s,
|
||||
Either::Right(t) => return Either::Right(t),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(n) = &route.name {
|
||||
state.names.insert(n.clone());
|
||||
}
|
||||
|
||||
match (&route.nested, values.is_empty()) {
|
||||
(Some(n), _) => route_segment_internal(n, &values[1..], state, fallback, clear_fallback),
|
||||
(None, false) => merge_fallback(state, fallback, clear_fallback),
|
||||
_ => Either::Left(state),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_parameter_route<T: Clone>(
|
||||
values: &[&str],
|
||||
route: &ParameterRoute<T>,
|
||||
mut state: RouterState<T>,
|
||||
fallback: Option<RouteContent<T>>,
|
||||
clear_fallback: bool,
|
||||
) -> Either<RouterState<T>, NavigationTarget> {
|
||||
// merge content
|
||||
if let Some(c) = &route.content {
|
||||
match merge(state, c.clone()) {
|
||||
Either::Left(s) => state = s,
|
||||
Either::Right(t) => return Either::Right(t),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(n) = &route.name {
|
||||
state.names.insert(n.clone());
|
||||
}
|
||||
|
||||
state.parameters.insert(
|
||||
route.key.clone(),
|
||||
decode(values[0]).unwrap(/* string already is UTF-8 */).into_owned(),
|
||||
);
|
||||
|
||||
match (&route.nested, values.is_empty()) {
|
||||
(Some(n), _) => route_segment_internal(n, &values[1..], state, fallback, clear_fallback),
|
||||
(None, false) => merge_fallback(state, fallback, clear_fallback),
|
||||
_ => Either::Left(state),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_fallback<T: Clone>(
|
||||
mut state: RouterState<T>,
|
||||
fallback: Option<RouteContent<T>>,
|
||||
clear_fallback: bool,
|
||||
) -> Either<RouterState<T>, NavigationTarget> {
|
||||
// fallback clearing
|
||||
if clear_fallback {
|
||||
state.content.clear();
|
||||
state.names.clear();
|
||||
state.parameters.clear();
|
||||
}
|
||||
|
||||
// fallback content
|
||||
match fallback {
|
||||
Some(fallback) => merge(state, fallback),
|
||||
None => Either::Left(state),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
routes::{multi, ContentAtom},
|
||||
Name,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_segment() -> Segment<&'static str> {
|
||||
Segment::content(ContentAtom("index"))
|
||||
.fixed("fixed", Route::content(ContentAtom("fixed")).name::<bool>())
|
||||
.matching(
|
||||
String::from("matching"),
|
||||
ParameterRoute::content::<u8>(ContentAtom("matching"))
|
||||
.nested(Segment::empty().fixed("nested", ContentAtom("matching nested"))),
|
||||
)
|
||||
.catch_all(
|
||||
ParameterRoute::content::<u16>(ContentAtom("catch all"))
|
||||
.nested(Segment::empty().fixed("nested", ContentAtom("catch all nested"))),
|
||||
)
|
||||
.fixed(
|
||||
"nested",
|
||||
Route::content(ContentAtom("nested")).name::<u32>().nested(
|
||||
Segment::content(ContentAtom("nested index"))
|
||||
.fixed("again", ContentAtom("nested again")),
|
||||
),
|
||||
)
|
||||
.fixed("redirect", "/redirect")
|
||||
.fixed(
|
||||
"fallback",
|
||||
Route::content(ContentAtom("fallback")).nested(
|
||||
Segment::empty()
|
||||
.fixed(
|
||||
"keep",
|
||||
Route::content(ContentAtom("keep route")).nested(
|
||||
Segment::content(ContentAtom("keep index"))
|
||||
.fallback(ContentAtom("keep")),
|
||||
),
|
||||
)
|
||||
.fixed(
|
||||
"clear",
|
||||
Route::content(ContentAtom("clear route")).nested(
|
||||
Segment::empty()
|
||||
.fallback(ContentAtom("clear"))
|
||||
.clear_fallback(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.fixed(
|
||||
"no_fallback",
|
||||
Route::content(ContentAtom("no fallback")).nested(
|
||||
Segment::empty()
|
||||
.fixed(
|
||||
"keep",
|
||||
Route::content(ContentAtom("keep route"))
|
||||
.nested(Segment::empty().clear_fallback(false)),
|
||||
)
|
||||
.fixed(
|
||||
"clear",
|
||||
Route::content(ContentAtom("clear route"))
|
||||
.nested(Segment::empty().clear_fallback(true)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.fixed(
|
||||
"named_content",
|
||||
Route::content(
|
||||
multi(None)
|
||||
.add_named::<i8>(ContentAtom("1"))
|
||||
.add_named::<i16>(ContentAtom("2")),
|
||||
)
|
||||
.nested(Segment::content(multi(Some(ContentAtom("3"))))),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_index() {
|
||||
let state = route_segment(
|
||||
&test_segment(),
|
||||
&[],
|
||||
RouterState {
|
||||
path: String::from("/"),
|
||||
can_go_back: false,
|
||||
can_go_forward: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(state.content, vec![ContentAtom("index")]);
|
||||
assert!(state.names.is_empty());
|
||||
assert!(state.parameters.is_empty());
|
||||
assert_eq!(state.path, String::from("/"));
|
||||
assert_eq!(state.can_go_back, false);
|
||||
assert_eq!(state.can_go_forward, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_fixed() {
|
||||
let state = route_segment(&test_segment(), &["fixed"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||
assert_eq!(state.names, {
|
||||
let mut r = HashSet::new();
|
||||
r.insert(Name::of::<bool>());
|
||||
r
|
||||
});
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_matching() {
|
||||
let state = route_segment(&test_segment(), &["matching"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(state.content, vec![ContentAtom("matching")]);
|
||||
assert!(state.names.is_empty());
|
||||
assert_eq!(state.parameters, {
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<u8>(), String::from("matching"));
|
||||
r
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_matching_nested() {
|
||||
let state = route_segment(&test_segment(), &["matching", "nested"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(
|
||||
state.content,
|
||||
vec![ContentAtom("matching"), ContentAtom("matching nested")]
|
||||
);
|
||||
assert!(state.names.is_empty());
|
||||
assert_eq!(state.parameters, {
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<u8>(), String::from("matching"));
|
||||
r
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_catch_all() {
|
||||
let state = route_segment(&test_segment(), &["invalid"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(state.content, vec![ContentAtom("catch all")]);
|
||||
assert!(state.names.is_empty());
|
||||
assert_eq!(state.parameters, {
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<u16>(), String::from("invalid"));
|
||||
r
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_catch_all_nested() {
|
||||
let state = route_segment(&test_segment(), &["invalid", "nested"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(
|
||||
state.content,
|
||||
vec![ContentAtom("catch all"), ContentAtom("catch all nested")]
|
||||
);
|
||||
assert!(state.names.is_empty());
|
||||
assert_eq!(state.parameters, {
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<u16>(), String::from("invalid"));
|
||||
r
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_nested_index() {
|
||||
let state = route_segment(&test_segment(), &["nested"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(
|
||||
state.content,
|
||||
vec![ContentAtom("nested"), ContentAtom("nested index")]
|
||||
);
|
||||
assert_eq!(state.names, {
|
||||
let mut r = HashSet::new();
|
||||
r.insert(Name::of::<u32>());
|
||||
r
|
||||
});
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_nested_again() {
|
||||
let state = route_segment(&test_segment(), &["nested", "again"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(
|
||||
state.content,
|
||||
vec![ContentAtom("nested"), ContentAtom("nested again")]
|
||||
);
|
||||
assert_eq!(state.names, {
|
||||
let mut r = HashSet::new();
|
||||
r.insert(Name::of::<u32>());
|
||||
r
|
||||
});
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_redirect() {
|
||||
let state = route_segment(&test_segment(), &["redirect"], Default::default());
|
||||
assert_eq!(state.unwrap_right(), "/redirect".into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_fallback_keep() {
|
||||
let state = route_segment(
|
||||
&test_segment(),
|
||||
&["fallback", "keep", "invalid"],
|
||||
Default::default(),
|
||||
);
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(
|
||||
state.content,
|
||||
vec![
|
||||
ContentAtom("fallback"),
|
||||
ContentAtom("keep route"),
|
||||
ContentAtom("keep")
|
||||
]
|
||||
);
|
||||
assert!(state.names.is_empty());
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_fallback_clear() {
|
||||
let state = route_segment(
|
||||
&test_segment(),
|
||||
&["fallback", "clear", "invalid"],
|
||||
Default::default(),
|
||||
);
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(state.content, vec![ContentAtom("clear")]);
|
||||
assert!(state.names.is_empty());
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_named_content() {
|
||||
let state = route_segment(&test_segment(), &["named_content"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(state.content, vec![ContentAtom("3")]);
|
||||
assert_eq!(state.named_content, {
|
||||
let mut r = BTreeMap::new();
|
||||
r.insert(Name::of::<i8>(), vec![ContentAtom("1")]);
|
||||
r.insert(Name::of::<i16>(), vec![ContentAtom("2")]);
|
||||
r
|
||||
});
|
||||
assert!(state.names.is_empty());
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "not yet implemented"]
|
||||
fn no_fallback() {
|
||||
let state = route_segment(
|
||||
&test_segment(),
|
||||
&["no_fallback", "keep", "invalid"],
|
||||
Default::default(),
|
||||
);
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(
|
||||
state.content,
|
||||
vec![
|
||||
ContentAtom("fallback"),
|
||||
ContentAtom("keep route"),
|
||||
ContentAtom("keep")
|
||||
]
|
||||
);
|
||||
assert!(state.names.is_empty());
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "not yet implemented"]
|
||||
fn no_fallback_with_clearing() {
|
||||
let state = route_segment(
|
||||
&test_segment(),
|
||||
&["fallback", "clear", "invalid"],
|
||||
Default::default(),
|
||||
);
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert!(state.content.is_empty());
|
||||
assert!(state.names.is_empty());
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_encoding() {
|
||||
let state = route_segment(&test_segment(), &["%F0%9F%A5%B3"], Default::default());
|
||||
assert!(state.is_left());
|
||||
|
||||
let state = state.unwrap_left();
|
||||
assert_eq!(state.content, vec![ContentAtom("catch all")]);
|
||||
assert!(state.names.is_empty());
|
||||
assert_eq!(state.parameters, {
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<u16>(), "🥳".to_string());
|
||||
r
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use urlencoding::encode;
|
||||
|
||||
use crate::{routes::Segment, Name};
|
||||
|
||||
pub fn gen_sitemap<T: Clone>(seg: &Segment<T>, current: &str, map: &mut Vec<String>) {
|
||||
for (p, r) in &seg.fixed {
|
||||
let current = format!("{current}/{p}");
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_sitemap(n, ¤t, map);
|
||||
}
|
||||
}
|
||||
|
||||
for (_, r) in &seg.matching {
|
||||
let current = format!("{current}/\\{}", r.key);
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_sitemap(n, ¤t, map);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(r) = &seg.catch_all {
|
||||
let current = format!("{current}/\\{}", r.key);
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_sitemap(n, ¤t, map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gen_parameter_sitemap<T: Clone>(
|
||||
seg: &Segment<T>,
|
||||
parameters: &BTreeMap<Name, Vec<String>>,
|
||||
current: &str,
|
||||
map: &mut Vec<String>,
|
||||
) {
|
||||
for (p, r) in &seg.fixed {
|
||||
let current = format!("{current}/{p}");
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_parameter_sitemap(n, parameters, ¤t, map);
|
||||
}
|
||||
}
|
||||
|
||||
for (m, r) in &seg.matching {
|
||||
if let Some(rp) = parameters.get(&r.key) {
|
||||
for p in rp {
|
||||
if m.matches(p) {
|
||||
let current = format!("{current}/{}", encode(p).into_owned());
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_parameter_sitemap(n, parameters, ¤t, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(r) = &seg.catch_all {
|
||||
if let Some(rp) = parameters.get(&r.key) {
|
||||
for p in rp {
|
||||
let current = format!("{current}/{}", encode(p).into_owned());
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_parameter_sitemap(n, parameters, ¤t, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::routes::{ParameterRoute, Route};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_segment() -> Segment<&'static str> {
|
||||
Segment::empty()
|
||||
.fixed(
|
||||
"fixed",
|
||||
Route::empty().nested(Segment::empty().fixed("nested", Route::empty())),
|
||||
)
|
||||
.matching(
|
||||
String::from("m1"),
|
||||
ParameterRoute::empty::<u8>().nested(
|
||||
Segment::empty().matching(String::from("n2"), ParameterRoute::empty::<u16>()),
|
||||
),
|
||||
)
|
||||
.matching(String::from("no match"), ParameterRoute::empty::<u32>())
|
||||
.matching(String::from("no parameter"), ParameterRoute::empty::<u64>())
|
||||
.catch_all(
|
||||
ParameterRoute::empty::<u32>()
|
||||
.nested(Segment::empty().catch_all(ParameterRoute::empty::<u16>())),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sitemap() {
|
||||
let mut result = Vec::new();
|
||||
result.push(String::from("/"));
|
||||
gen_sitemap(&test_segment(), "", &mut result);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![
|
||||
"/",
|
||||
"/fixed",
|
||||
"/fixed/nested",
|
||||
"/\\u8",
|
||||
"/\\u8/\\u16",
|
||||
"/\\u32",
|
||||
"/\\u64",
|
||||
"/\\u32",
|
||||
"/\\u32/\\u16"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sitemap_with_parameters() {
|
||||
let mut parameters = BTreeMap::new();
|
||||
parameters.insert(Name::of::<u8>(), vec!["m1".to_string(), "m2".to_string()]);
|
||||
parameters.insert(Name::of::<u16>(), vec!["n1".to_string(), "n2".to_string()]);
|
||||
parameters.insert(Name::of::<u32>(), vec!["catch all".to_string()]);
|
||||
|
||||
let mut result = Vec::new();
|
||||
result.push(String::from("/"));
|
||||
gen_parameter_sitemap(&test_segment(), ¶meters, "", &mut result);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![
|
||||
"/",
|
||||
"/fixed",
|
||||
"/fixed/nested",
|
||||
"/m1",
|
||||
"/m1/n2",
|
||||
"/catch%20all",
|
||||
"/catch%20all/n1",
|
||||
"/catch%20all/n2"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use either::Either;
|
||||
|
||||
use crate::{
|
||||
navigation::{NavigationTarget, Query},
|
||||
segments::NameMap,
|
||||
Name,
|
||||
};
|
||||
|
||||
use super::resolve_name;
|
||||
|
||||
pub fn resolve_target(
|
||||
names: &NameMap,
|
||||
target: &NavigationTarget,
|
||||
) -> Either<Either<String, Name>, String> {
|
||||
match target {
|
||||
NavigationTarget::Internal(i) => Either::Left(Either::Left(i.clone())),
|
||||
NavigationTarget::Named {
|
||||
name,
|
||||
parameters,
|
||||
query,
|
||||
} => Either::Left(
|
||||
resolve_name(names, name, parameters)
|
||||
.map(|mut p| {
|
||||
if let Some(q) = query {
|
||||
match q {
|
||||
Query::Single(s) => {
|
||||
if !s.starts_with('?') {
|
||||
p += "?";
|
||||
}
|
||||
p += &s;
|
||||
}
|
||||
#[cfg(feature = "serde")]
|
||||
Query::List(l) => {
|
||||
let res = serde_urlencoded::to_string(l);
|
||||
// TODO: find a test case where this assertion is not met
|
||||
debug_assert!(res.is_ok(), "cannot serialize query list: {l:?}");
|
||||
if let Ok(q) = res {
|
||||
p += "?";
|
||||
p += &q;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p
|
||||
})
|
||||
.map(|p| Either::Left(p))
|
||||
.unwrap_or(Either::Right(name.clone())),
|
||||
),
|
||||
NavigationTarget::External(e) => Either::Right(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{prelude::RootIndex, routes::Segment, segments::NamedSegment};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolve_internal() {
|
||||
let names = NamedSegment::from_segment(&Segment::<&str>::empty());
|
||||
assert_eq!(
|
||||
resolve_target(&names, &NavigationTarget::Internal("/test".to_string())),
|
||||
Either::Left(Either::Left(String::from("/test")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_named() {
|
||||
let names = NamedSegment::from_segment(&Segment::<&str>::empty());
|
||||
assert_eq!(
|
||||
resolve_target(&names, &NavigationTarget::named::<RootIndex>()),
|
||||
Either::Left(Either::Left(String::from("/")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_named_with_query_single() {
|
||||
let names = NamedSegment::from_segment(&Segment::<&str>::empty());
|
||||
let without = resolve_target(
|
||||
&names,
|
||||
&NavigationTarget::named::<RootIndex>().query("huhu"),
|
||||
);
|
||||
let with = resolve_target(
|
||||
&names,
|
||||
&NavigationTarget::named::<RootIndex>().query("?huhu"),
|
||||
);
|
||||
let correct = Either::Left(Either::Left(String::from("/?huhu")));
|
||||
assert_eq!(with, correct);
|
||||
assert_eq!(without, correct);
|
||||
assert_eq!(with, without);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "serde")]
|
||||
fn resolve_named_with_query_list() {
|
||||
let names = NamedSegment::from_segment(&Segment::<&str>::empty());
|
||||
assert_eq!(
|
||||
resolve_target(
|
||||
&names,
|
||||
&NavigationTarget::named::<RootIndex>()
|
||||
.query(vec![("some", "test"), ("another", "value")])
|
||||
),
|
||||
Either::Left(Either::Left(String::from("/?some=test&another=value")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_external() {
|
||||
let names = NamedSegment::from_segment(&Segment::<&str>::empty());
|
||||
assert_eq!(
|
||||
resolve_target(
|
||||
&names,
|
||||
&NavigationTarget::External("https://dioxuslabs.com/".to_string())
|
||||
),
|
||||
Either::Right(String::from("https://dioxuslabs.com/"))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user