web_route/parameterized_route/
route.rs

1use std::{fmt, ops};
2
3use crate::{
4    WebRoute, error::WebRouteError, parameterized_route::segment::ParameterizedSegment,
5    to_segments::ToParameterizedSegments, utils::struct_to_map,
6};
7
8/// Defines a route structure that can be used to define routes for a webserver.
9///
10/// Its templated sections can be easily populated to create a [`WebRoute`]
11/// which can be used to make requests against the webserver routes that the
12/// [`ParameterizedRoute`] was used to define.
13#[derive(Clone, PartialEq)]
14pub struct ParameterizedRoute(String);
15
16impl ParameterizedRoute {
17    /// Creates a new [`ParameterizedRoute`].
18    ///
19    /// # Examples
20    ///
21    /// ```
22    /// use web_route::ParameterizedRoute;
23    ///
24    /// let route = ParameterizedRoute::new("/some/route/{param}");
25    /// ```
26    pub fn new<R: ToParameterizedSegments>(route: R) -> Self {
27        let segments = route.to_segments();
28
29        Self(evaluate_segments(segments))
30    }
31
32    /// Joins a route onto an existing [`ParameterizedRoute`] returning the
33    /// joined route.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use web_route::ParameterizedRoute;
39    ///
40    /// let route = ParameterizedRoute::new("/some/route/{param}");
41    /// let nested_route = ParameterizedRoute::new("/a/nested/route");
42    /// let joined_route = route.join(&nested_route);
43    ///
44    /// assert_eq!(joined_route, route.join("/a/nested/route"))
45    /// ```
46    pub fn join<R: ToParameterizedSegments>(&self, route: R) -> Self {
47        let joined_segments = [self.to_segments(), route.to_segments()].concat();
48
49        Self(evaluate_segments(joined_segments))
50    }
51
52    /// Attempts to populate the parameters of the route with their `values` and
53    /// returns a [`WebRoute`].
54    ///
55    /// `values` needs to implement `serde::Serialize` and be of an "Object"
56    /// style (with key-value pairs).
57    ///
58    /// This would be used when making a request to an endpoint represented by
59    /// the route.
60    ///
61    /// # Errors
62    ///
63    /// - [`WebRouteError::UnpopulatedParam`] if no matching entry was found in
64    ///   `values` for a particular parameter.
65    /// - [`WebRouteError::InvalidValue`] if `values` does not contain key-value
66    ///   pairs.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use web_route::ParameterizedRoute;
72    ///
73    /// #[derive(serde::Serialize)]
74    /// struct RouteParams {
75    ///     param: String,
76    /// }
77    ///
78    /// let parameterized_route = ParameterizedRoute::new("/some/route/{param}");
79    /// let web_route = parameterized_route
80    ///     .to_web_route(&RouteParams {
81    ///         param: "value".to_owned(),
82    ///     })
83    ///     .unwrap();
84    ///     
85    /// assert_eq!(&web_route.to_string(), "/some/route/value")
86    /// ```
87    pub fn to_web_route<V: serde::Serialize>(&self, values: &V) -> Result<WebRoute, WebRouteError> {
88        let values = struct_to_map(values).ok_or(WebRouteError::InvalidValue)?;
89
90        let populated_segments = self
91            .to_segments()
92            .iter()
93            .map(|segment| segment.to_populated(&values))
94            .collect::<Result<Vec<_>, _>>()?;
95
96        let web_route = WebRoute::new(format!("/{}", populated_segments.join("/")));
97
98        Ok(web_route)
99    }
100
101    pub(crate) fn to_segments(&self) -> Vec<ParameterizedSegment> {
102        ToParameterizedSegments::to_segments(&self.0)
103    }
104}
105
106impl fmt::Display for ParameterizedRoute {
107    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108        write!(f, "{}", self.0)
109    }
110}
111
112impl fmt::Debug for ParameterizedRoute {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.debug_tuple("ParameterizedRoute")
115            .field(&self.to_string())
116            .finish()
117    }
118}
119
120/// Allows one to deref for usage with external crates. Makes for neater code.
121impl ops::Deref for ParameterizedRoute {
122    type Target = str;
123
124    fn deref(&self) -> &Self::Target {
125        &self.0
126    }
127}
128
129impl AsRef<str> for ParameterizedRoute {
130    fn as_ref(&self) -> &str {
131        &self.0
132    }
133}
134
135#[cfg(feature = "fake")]
136impl fake::Dummy<fake::Faker> for ParameterizedRoute {
137    fn dummy_with_rng<R: fake::Rng + ?Sized>(config: &fake::Faker, rng: &mut R) -> Self {
138        use fake::Fake;
139
140        let segments: Vec<ParameterizedSegment> = config.fake_with_rng(rng);
141        Self::new(segments)
142    }
143}
144
145/// Convert `segments` into their normalized [`String`] route representation.
146fn evaluate_segments(segments: Vec<ParameterizedSegment>) -> String {
147    let evaluated_segments = segments
148        .iter()
149        .map(ParameterizedSegment::to_template)
150        .collect::<Vec<_>>();
151
152    format!("/{}", evaluated_segments.join("/"))
153}
154
155#[cfg(test)]
156mod parameterized_route_tests {
157    use super::*;
158
159    mod to_web_route {
160        use std::ops::Deref;
161
162        use super::*;
163
164        #[test]
165        fn should_normalize_double_forward_slashes() {
166            // Arrange
167            #[derive(serde::Serialize)]
168            struct RouteParams {
169                param: String,
170            }
171
172            let parameterized_route = ParameterizedRoute::new("/some/route/{param}");
173
174            // Act
175            let web_route = parameterized_route
176                .to_web_route(&RouteParams {
177                    param: "/value".to_owned(),
178                })
179                .unwrap();
180
181            // Assert
182            assert_eq!(web_route.deref(), "/some/route/value")
183        }
184    }
185}