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
129#[cfg(feature = "fake")]
130impl fake::Dummy<fake::Faker> for ParameterizedRoute {
131 fn dummy_with_rng<R: fake::Rng + ?Sized>(config: &fake::Faker, rng: &mut R) -> Self {
132 use fake::Fake;
133
134 let segments: Vec<ParameterizedSegment> = config.fake_with_rng(rng);
135 Self::new(segments)
136 }
137}
138
139/// Convert `segments` into their normalized [`String`] route representation.
140fn evaluate_segments(segments: Vec<ParameterizedSegment>) -> String {
141 let evaluated_segments = segments
142 .iter()
143 .map(ParameterizedSegment::to_template)
144 .collect::<Vec<_>>();
145
146 format!("/{}", evaluated_segments.join("/"))
147}
148
149#[cfg(test)]
150mod parameterized_route_tests {
151 use super::*;
152
153 mod to_web_route {
154 use std::ops::Deref;
155
156 use super::*;
157
158 #[test]
159 fn should_normalize_double_forward_slashes() {
160 // Arrange
161 #[derive(serde::Serialize)]
162 struct RouteParams {
163 param: String,
164 }
165
166 let parameterized_route = ParameterizedRoute::new("/some/route/{param}");
167
168 // Act
169 let web_route = parameterized_route
170 .to_web_route(&RouteParams {
171 param: "/value".to_owned(),
172 })
173 .unwrap();
174
175 // Assert
176 assert_eq!(web_route.deref(), "/some/route/value")
177 }
178 }
179}