general cleanup

This commit is contained in:
Evan Almloff committed 2023-05-31 17:12:52 -05:00
1 parent 09cabe4e8b
commit ece8f0fb22
15 files changed
+155 -767

No files matched your search

+14 -5
View File
@@ -1,6 +1,6 @@
use dioxus::prelude::*;
use crate::{hooks::use_generic_route, routable::Routable};
use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
#[derive(Clone)]
pub(crate) struct OutletContext {
@@ -16,7 +16,10 @@ pub(crate) fn use_outlet_context(cx: &ScopeState) -> &OutletContext {
}
impl OutletContext {
pub(crate) fn render<R: Routable + Clone>(cx: &ScopeState) -> Element<'_> {
pub(crate) fn render<R: Routable + Clone>(cx: Scope) -> Element<'_> {
let router = use_router_internal::<R>(cx)
.as_ref()
.expect("Outlet must be inside of a router");
let outlet = use_outlet_context(cx);
let current_level = outlet.current_level;
cx.provide_context({
@@ -25,8 +28,14 @@ impl OutletContext {
}
});
use_generic_route::<R>(cx)
.expect("Outlet must be inside of a router")
.render(cx, current_level)
if let Some(error) = router.render_error(cx) {
if current_level == 0 {
return Some(error);
} else {
return None;
}
}
router.current().render(cx, current_level)
}
}
+78 -640
View File
@@ -11,12 +11,8 @@ use crate::{
};
/// An error that can occur when navigating.
pub enum NavigationFailure<R: Routable> {
/// The router failed to navigate to an external URL.
External(String),
/// The router failed to navigate to an internal URL.
Internal(<R as std::str::FromStr>::Err),
}
#[derive(Debug, Clone)]
pub struct ExternalNavigationFailure(String);
/// A function the router will call after every routing update.
pub type RoutingCallback<R> = Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
@@ -40,6 +36,8 @@ where
prefix: Option<String>,
history: Box<dyn HistoryProvider<R>>,
unresolved_error: Option<ExternalNavigationFailure>,
}
/// A collection of router data that manages all routing functionality.
@@ -54,8 +52,6 @@ where
routing_callback: Option<RoutingCallback<R>>,
failure_external_navigation: fn(Scope) -> Element,
failure_named_navigation: fn(Scope) -> Element,
failure_redirection_limit: fn(Scope) -> Element,
}
impl<R: Routable> Clone for GenericRouterContext<R> {
@@ -66,8 +62,6 @@ impl<R: Routable> Clone for GenericRouterContext<R> {
subscriber_update: self.subscriber_update.clone(),
routing_callback: self.routing_callback.clone(),
failure_external_navigation: self.failure_external_navigation,
failure_named_navigation: self.failure_named_navigation,
failure_redirection_limit: self.failure_redirection_limit,
}
}
}
@@ -88,6 +82,7 @@ where
can_go_forward: false,
prefix: Default::default(),
history: cfg.history,
unresolved_error: None,
}));
let subscriber_update = mark_dirty.clone();
@@ -101,8 +96,6 @@ where
routing_callback: cfg.on_update,
failure_external_navigation: cfg.failure_external_navigation,
failure_named_navigation: cfg.failure_named_navigation,
failure_redirection_limit: cfg.failure_redirection_limit,
};
// set the updater
@@ -134,46 +127,61 @@ where
///
/// Will fail silently if there is no previous location to go to.
pub fn go_back(&self) {
self.state.write().unwrap().history.go_back();
self.update_subscribers();
{
self.state.write().unwrap().history.go_back();
}
self.change_route();
}
/// Go back to the next location.
///
/// Will fail silently if there is no next location to go to.
pub fn go_forward(&self) {
self.state.write().unwrap().history.go_forward();
self.update_subscribers();
{
self.state.write().unwrap().history.go_forward();
}
self.change_route();
}
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(&self, target: impl Into<NavigationTarget<R>>) -> Option<NavigationFailure<R>> {
pub fn push(
&self,
target: impl Into<NavigationTarget<R>>,
) -> Option<ExternalNavigationFailure> {
let target = target.into();
let mut state = self.state_mut();
match target {
NavigationTarget::Internal(p) => state.history.push(p),
NavigationTarget::Internal(p) => {
let mut state = self.state_mut();
state.history.push(p)
}
NavigationTarget::External(e) => return self.external(e),
}
self.update_subscribers();
None
self.change_route()
}
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(&self, target: impl Into<NavigationTarget<R>>) -> Option<NavigationFailure<R>> {
pub fn replace(
&self,
target: impl Into<NavigationTarget<R>>,
) -> Option<ExternalNavigationFailure> {
let target = target.into();
let mut state = self.state_mut();
match target {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::External(e) => return self.external(e),
{
let mut state = self.state_mut();
match target {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::External(e) => return self.external(e),
}
}
self.update_subscribers();
None
self.change_route()
}
/// The route that is currently active.
@@ -189,11 +197,18 @@ where
self.state.read().unwrap().prefix.clone()
}
fn external(&self, external: String) -> Option<NavigationFailure<R>> {
fn external(&self, external: String) -> Option<ExternalNavigationFailure> {
let mut state = self.state_mut();
match state.history.external(external.clone()) {
true => None,
false => Some(NavigationFailure::External(external)),
false => {
let failure = ExternalNavigationFailure(external);
state.unresolved_error = Some(failure.clone());
self.update_subscribers();
Some(failure)
}
}
}
@@ -216,614 +231,37 @@ where
(self.subscriber_update)(id);
}
}
/// Clear any unresolved errors
pub fn clear_error(&self) {
self.state.write().unwrap().unresolved_error = None;
self.update_subscribers();
}
pub(crate) fn render_error<'a>(&self, cx: Scope<'a>) -> Element<'a> {
self.state
.read()
.unwrap()
.unresolved_error
.as_ref()
.and_then(|_| (self.failure_external_navigation)(cx))
}
fn change_route(&self) -> Option<ExternalNavigationFailure> {
if let Some(callback) = &self.routing_callback {
let myself = self.clone();
if let Some(new) = callback(myself) {
let mut state = self.state_mut();
match new {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::External(e) => return self.external(e),
}
}
}
self.update_subscribers();
None
}
}
// #[cfg(test)]
// mod tests {
// //! The tests for [`RouterContext`] test various functions that are not exposed as public.
// //! However, several of those have an observable effect on the behavior of exposed functions.
// //!
// //! The alternative would be to send messages via the services channel and calling one of the
// //! `run` functions. However, for readability and clarity, it was chosen to directly call the
// //! private functions.
// use std::sync::Mutex;
// use crate::{
// history::MemoryHistory,
// routes::{ParameterRoute, Route, RouteContent},
// };
// use super::*;
// fn test_segment() -> Segment<&'static str> {
// Segment::content(RouteContent::Content(ContentAtom("index")))
// .fixed(
// "fixed",
// Route::content(RouteContent::Content(ContentAtom("fixed"))).name::<bool>(),
// )
// .fixed(
// "redirect",
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
// String::from("fixed"),
// ))),
// )
// .fixed(
// "redirection-loop",
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
// String::from("/redirection-loop"),
// ))),
// )
// .fixed(
// "%F0%9F%8E%BA",
// Route::content(RouteContent::Content(ContentAtom("🎺"))),
// )
// .catch_all(ParameterRoute::empty::<bool>())
// }
// #[test]
// fn new_provides_update_to_history() {
// struct TestHistory {}
// impl HistoryProvider for TestHistory {
// fn current_route(&self) -> String {
// todo!()
// }
// fn current_query(&self) -> Option<String> {
// todo!()
// }
// fn go_back(&mut self) {
// todo!()
// }
// fn go_forward(&mut self) {
// todo!()
// }
// fn push(&mut self, _path: String) {
// todo!()
// }
// fn replace(&mut self, _path: String) {
// todo!()
// }
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
// callback();
// }
// }
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(TestHistory {}),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert!(matches!(
// s.receiver.try_next().unwrap().unwrap(),
// RouterMessage::Update
// ));
// }
// #[test]
// fn update_routing() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert_eq!(s.names, s.state.try_read().unwrap().name_map);
// s.init();
// let state = s.state.try_read().unwrap();
// 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());
// assert_eq!(state.path, String::from("/fixed"));
// assert_eq!(state.query, Some(String::from("test=value")));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// assert_eq!(s.names, state.name_map);
// }
// #[test]
// fn update_routing_root_index() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("index")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<RootIndex>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/"));
// assert!(state.query.is_none());
// assert!(state.prefix.is_none());
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn update_routing_redirect() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// 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());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// #[should_panic = "reached redirect limit of 25"]
// #[cfg(debug_assertions)]
// fn update_routing_redirect_debug() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn update_routing_redirect_release() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("redirect limit")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureRedirectionLimit>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/redirection-loop"));
// assert_eq!(state.can_go_back, false);
// assert_eq!(state.can_go_forward, false);
// }
// #[test]
// fn update_subscribers() {
// let ids = Arc::new(Mutex::new(Vec::new()));
// let ids2 = Arc::clone(&ids);
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// Segment::empty(),
// Box::<MemoryHistory>::default(),
// Arc::new(move |id| {
// ids2.lock().unwrap().push(id);
// }),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// let id0 = Arc::new(0);
// s.subscribe(Arc::clone(&id0));
// let id1 = Arc::new(1);
// s.subscribe(Arc::clone(&id1));
// let id1 = Arc::try_unwrap(id1).unwrap();
// s.update_subscribers();
// assert_eq!(s.subscribers.len(), 1);
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id0);
// assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]);
// }
// #[test]
// fn push_internal() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.push(NavigationTarget::Internal(String::from("/fixed")));
// s.init();
// let state = s.state.try_read().unwrap();
// 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());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn push_named() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.push(NavigationTarget::named::<bool>());
// s.init();
// let state = s.state.try_read().unwrap();
// 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());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn push_external() {
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// tx.unbounded_send(RouterMessage::Push(NavigationTarget::External(
// String::from("https://dioxuslabs.com/"),
// )))
// .unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("external target")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureExternalNavigation>());
// r
// });
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(
// Name::of::<FailureExternalNavigation>(),
// String::from("https://dioxuslabs.com/"),
// );
// r
// });
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_named() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.replace(NavigationTarget::named::<bool>());
// s.init();
// let state = s.state.try_read().unwrap();
// 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());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_internal() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.replace(NavigationTarget::Internal(String::from("/fixed")));
// s.init();
// let state = s.state.try_read().unwrap();
// 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());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_external() {
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External(
// String::from("https://dioxuslabs.com/"),
// )))
// .unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("external target")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureExternalNavigation>());
// r
// });
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(
// Name::of::<FailureExternalNavigation>(),
// String::from("https://dioxuslabs.com/"),
// );
// r
// });
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn subscribe() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// Segment::empty(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// let id = Arc::new(0);
// s.subscribe(Arc::clone(&id));
// assert_eq!(s.subscribers.len(), 1);
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id);
// }
// #[test]
// fn routing_callback() {
// let paths = Arc::new(Mutex::new(Vec::new()));
// let paths2 = Arc::clone(&paths);
// let (mut s, c, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// Some(Arc::new(move |state| {
// paths2.lock().unwrap().push(state.path.clone());
// Some("/%F0%9F%8E%BA".into())
// })),
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert!(paths.lock().unwrap().is_empty());
// s.init();
// assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]);
// c.unbounded_send(RouterMessage::Update).unwrap();
// s.run_current();
// assert_eq!(
// *paths.lock().unwrap(),
// vec![String::from("/fixed"), String::from("/%F0%9F%8E%BA")]
// );
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("🎺")])
// }
// #[test]
// fn url_decoding_do() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert!(state.content.is_empty());
// assert!(state.names.is_empty());
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(Name::of::<bool>(), String::from("🥳"));
// r
// });
// assert_eq!(state.path, String::from("/%F0%9F%A5%B3"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn url_decoding_do_not() {
// let (mut s, c, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// c.unbounded_send(RouterMessage::Update).unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("🎺")]);
// assert!(state.names.is_empty());
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/%F0%9F%8E%BA"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn prefix() {
// struct TestHistory {}
// impl HistoryProvider for TestHistory {
// fn current_route(&self) -> String {
// String::from("/")
// }
// fn current_query(&self) -> Option<String> {
// None
// }
// fn current_prefix(&self) -> Option<String> {
// Some(String::from("/prefix"))
// }
// fn can_go_back(&self) -> bool {
// false
// }
// fn can_go_forward(&self) -> bool {
// false
// }
// fn go_back(&mut self) {
// todo!()
// }
// fn go_forward(&mut self) {
// todo!()
// }
// fn push(&mut self, _path: String) {
// todo!()
// }
// fn replace(&mut self, _path: String) {
// todo!()
// }
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
// callback();
// }
// }
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(TestHistory {}),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("index")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<RootIndex>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/"));
// assert!(state.query.is_none());
// assert_eq!(state.prefix, Some(String::from("/prefix")));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// }