yew_router_nested/
switch.rs

1//! Parses routes into enums or structs.
2use crate::route::Route;
3use std::fmt::Write;
4
5/// Alias to Switch.
6///
7/// Eventually Switch will be renamed to Routable and this alias will be removed.
8#[allow(bare_trait_objects)]
9pub type Routable = Switch;
10
11/// Derivable routing trait that allows instances of implementors to be constructed from Routes.
12///
13/// # Note
14/// Don't try to implement this yourself, rely on the derive macro.
15///
16/// # Example
17/// ```
18/// use yew_router::{route::Route, Switch};
19/// #[derive(Debug, Switch, PartialEq)]
20/// enum TestEnum {
21///     #[to = "/test/route"]
22///     TestRoute,
23///     #[to = "/capture/string/{path}"]
24///     CaptureString { path: String },
25///     #[to = "/capture/number/{num}"]
26///     CaptureNumber { num: usize },
27///     #[to = "/capture/unnamed/{doot}"]
28///     CaptureUnnamed(String),
29/// }
30///
31/// assert_eq!(
32///     TestEnum::switch(Route::new_no_state("/test/route")),
33///     Some(TestEnum::TestRoute)
34/// );
35/// assert_eq!(
36///     TestEnum::switch(Route::new_no_state("/capture/string/lorem")),
37///     Some(TestEnum::CaptureString {
38///         path: "lorem".to_string()
39///     })
40/// );
41/// assert_eq!(
42///     TestEnum::switch(Route::new_no_state("/capture/number/22")),
43///     Some(TestEnum::CaptureNumber { num: 22 })
44/// );
45/// assert_eq!(
46///     TestEnum::switch(Route::new_no_state("/capture/unnamed/lorem")),
47///     Some(TestEnum::CaptureUnnamed("lorem".to_string()))
48/// );
49/// ```
50pub trait Switch: Sized {
51    /// Based on a route, possibly produce an itself.
52    fn switch<STATE>(route: Route<STATE>) -> Option<Self> {
53        Self::from_route_part(route.route, Some(route.state)).0
54    }
55
56    /// Get self from a part of the state
57    fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>);
58
59    /// Build part of a route from itself.
60    fn build_route_section<STATE>(self, route: &mut String) -> Option<STATE>;
61
62    /// Called when the key (the named capture group) can't be located. Instead of failing outright,
63    /// a default item can be provided instead.
64    ///
65    /// Its primary motivation for existing is to allow implementing Switch for Option.
66    /// This doesn't make sense at the moment because this only works for the individual key section
67    /// - any surrounding literals are pretty much guaranteed to make the parse step fail.
68    /// because of this, this functionality might be removed in favor of using a nested Switch enum,
69    /// or multiple variants.
70    fn key_not_available() -> Option<Self> {
71        None
72    }
73}
74
75/// Wrapper that requires that an implementor of Switch must start with a `/`.
76///
77/// This is needed for any non-derived type provided by yew-router to be used by itself.
78///
79/// This is because route strings will almost always start with `/`, so in order to get a std type
80/// with the `rest` attribute, without a specified leading `/`, this wrapper is needed.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
82pub struct LeadingSlash<T>(pub T);
83impl<U: Switch> Switch for LeadingSlash<U> {
84    fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>) {
85        if let Some(part) = part.strip_prefix('/') {
86            let (inner, state) = U::from_route_part(part.to_owned(), state);
87            (inner.map(LeadingSlash), state)
88        } else {
89            (None, None)
90        }
91    }
92
93    fn build_route_section<T>(self, route: &mut String) -> Option<T> {
94        write!(route, "/").ok()?;
95        self.0.build_route_section(route)
96    }
97}
98
99/// Successfully match even when the captured section can't be found.
100#[derive(Debug, PartialEq, Clone, Copy)]
101pub struct Permissive<U>(pub Option<U>);
102
103impl<U: Switch> Switch for Permissive<U> {
104    /// Option is very permissive in what is allowed.
105    fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>) {
106        let (inner, inner_state) = U::from_route_part(part, state);
107        if inner.is_some() {
108            (Some(Permissive(inner)), inner_state)
109        } else {
110            // The Some(None) here indicates that this will produce a None, if the wrapped value can't be parsed
111            (Some(Permissive(None)), None)
112        }
113    }
114
115    fn build_route_section<STATE>(self, route: &mut String) -> Option<STATE> {
116        if let Some(inner) = self.0 {
117            inner.build_route_section(route)
118        } else {
119            None
120        }
121    }
122
123    fn key_not_available() -> Option<Self> {
124        Some(Permissive(None))
125    }
126}
127
128// TODO the AllowMissing shim doesn't appear to offer much over Permissive.
129// Documentation should improve (need examples - to show the difference) or it should be removed.
130
131/// Allows a section to match, providing a None value,
132/// if its contents are entirely missing, or starts with a '/'.
133#[derive(Debug, PartialEq, Clone, Copy)]
134pub struct AllowMissing<U: std::fmt::Debug>(pub Option<U>);
135impl<U: Switch + std::fmt::Debug> Switch for AllowMissing<U> {
136    fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>) {
137        let route = part.clone();
138        let (inner, inner_state) = U::from_route_part(part, state);
139
140        if inner.is_some() {
141            (Some(AllowMissing(inner)), inner_state)
142        } else if route.is_empty()
143            || route.starts_with('/')
144            || route.starts_with('?')
145            || route.starts_with('&')
146            || route.starts_with('#')
147        {
148            (Some(AllowMissing(None)), inner_state)
149        } else {
150            (None, None)
151        }
152    }
153
154    fn build_route_section<STATE>(self, route: &mut String) -> Option<STATE> {
155        if let AllowMissing(Some(inner)) = self {
156            inner.build_route_section(route)
157        } else {
158            None
159        }
160    }
161}
162
163/// Builds a route from a switch.
164fn build_route_from_switch<SW: Switch, STATE: Default>(switch: SW) -> Route<STATE> {
165    // URLs are recommended to not be over 255 characters,
166    // although browsers frequently support up to about 2000.
167    // Routes, being a subset of URLs should probably be smaller than 255 characters for the vast
168    // majority of circumstances, preventing reallocation under most conditions.
169    let mut buf = String::with_capacity(255);
170    let state: STATE = switch.build_route_section(&mut buf).unwrap_or_default();
171    buf.shrink_to_fit();
172
173    Route { route: buf, state }
174}
175
176impl<SW: Switch, STATE: Default> From<SW> for Route<STATE> {
177    fn from(switch: SW) -> Self {
178        build_route_from_switch(switch)
179    }
180}
181
182impl<T: std::str::FromStr + std::fmt::Display> Switch for T {
183    fn from_route_part<U>(part: String, state: Option<U>) -> (Option<Self>, Option<U>) {
184        (::std::str::FromStr::from_str(&part).ok(), state)
185    }
186
187    fn build_route_section<U>(self, route: &mut String) -> Option<U> {
188        write!(route, "{}", self).expect("Writing to string should never fail.");
189        None
190    }
191}
192
193#[cfg(test)]
194mod test {
195    use super::*;
196    #[test]
197    fn isize_build_route() {
198        let mut route = "/".to_string();
199        let mut _state: Option<String> = None;
200        _state = _state.or_else(|| (-432isize).build_route_section(&mut route));
201        assert_eq!(route, "/-432".to_string());
202    }
203
204    #[test]
205    fn can_get_string_from_empty_str() {
206        let (s, _state) = String::from_route_part::<()>("".to_string(), Some(()));
207        assert_eq!(s, Some("".to_string()))
208    }
209
210    #[test]
211    fn uuid_from_route() {
212        let x = uuid::Uuid::switch::<()>(Route {
213            route: "5dc48134-35b5-4b8c-aa93-767bf00ae1d8".to_string(),
214            state: (),
215        });
216        assert!(x.is_some())
217    }
218    #[test]
219    fn uuid_to_route() {
220        use std::str::FromStr;
221        let id =
222            uuid::Uuid::from_str("5dc48134-35b5-4b8c-aa93-767bf00ae1d8").expect("should parse");
223        let mut buf = String::new();
224        id.build_route_section::<()>(&mut buf);
225        assert_eq!(buf, "5dc48134-35b5-4b8c-aa93-767bf00ae1d8".to_string())
226    }
227
228    #[test]
229    fn can_get_option_string_from_empty_str() {
230        let (s, _state): (Option<Permissive<String>>, Option<()>) =
231            Permissive::from_route_part("".to_string(), Some(()));
232        assert_eq!(s, Some(Permissive(Some("".to_string()))))
233    }
234}