rama_http/layer/
normalize_path.rs1use crate::{Request, Response};
41use rama_core::{Layer, Service};
42use rama_utils::macros::define_inner_service_accessors;
43
44#[derive(Debug, Copy, Clone)]
46enum NormalizeMode {
47 Trim,
49 Append,
51}
52
53#[derive(Debug, Clone)]
57pub struct NormalizePathLayer {
58 mode: NormalizeMode,
59}
60
61impl Default for NormalizePathLayer {
62 fn default() -> Self {
63 Self {
64 mode: NormalizeMode::Trim,
65 }
66 }
67}
68
69impl NormalizePathLayer {
70 #[must_use]
75 pub fn trim_trailing_slash() -> Self {
76 Self {
77 mode: NormalizeMode::Trim,
78 }
79 }
80
81 #[must_use]
86 pub fn append_trailing_slash() -> Self {
87 Self {
88 mode: NormalizeMode::Append,
89 }
90 }
91}
92
93impl<S> Layer<S> for NormalizePathLayer {
94 type Service = NormalizePath<S>;
95
96 fn layer(&self, inner: S) -> Self::Service {
97 NormalizePath {
98 mode: self.mode,
99 inner,
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
108pub struct NormalizePath<S> {
109 mode: NormalizeMode,
110 inner: S,
111}
112
113impl<S> NormalizePath<S> {
114 #[inline]
118 pub fn new(inner: S) -> Self {
119 Self::trim_trailing_slash(inner)
120 }
121
122 pub fn trim_trailing_slash(inner: S) -> Self {
127 Self {
128 inner,
129 mode: NormalizeMode::Trim,
130 }
131 }
132
133 pub fn append_trailing_slash(inner: S) -> Self {
138 Self {
139 inner,
140 mode: NormalizeMode::Append,
141 }
142 }
143
144 define_inner_service_accessors!();
145}
146
147impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for NormalizePath<S>
148where
149 S: Service<Request<ReqBody>, Output = Response<ResBody>>,
150 ReqBody: Send + 'static,
151 ResBody: Send + 'static,
152{
153 type Output = S::Output;
154 type Error = S::Error;
155
156 fn serve(
157 &self,
158 mut req: Request<ReqBody>,
159 ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + '_ {
160 match self.mode {
161 NormalizeMode::Trim => {
162 req.uri_mut().path_mut().trim_trailing_slash();
163 }
164 NormalizeMode::Append => {
165 req.uri_mut().path_mut().append_trailing_slash();
166 }
167 }
168 self.inner.serve(req)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use rama_core::Layer;
176 use rama_core::service::service_fn;
177 use rama_net::uri::Uri;
178 use std::convert::Infallible;
179
180 #[tokio::test]
181 async fn works() {
182 async fn handle(request: Request<()>) -> Result<Response<String>, Infallible> {
183 Ok(Response::new(request.uri().to_string()))
184 }
185
186 let svc = NormalizePathLayer::trim_trailing_slash().into_layer(service_fn(handle));
187
188 let body = svc
189 .serve(Request::builder().uri("/foo/").body(()).unwrap())
190 .await
191 .unwrap()
192 .into_body();
193
194 assert_eq!(body, "/foo");
195 }
196
197 #[test]
198 fn is_noop_if_no_trailing_slash() {
199 let mut uri = "/foo".parse::<Uri>().unwrap();
200 uri.path_mut().trim_trailing_slash();
201 assert_eq!(uri.as_str(), "/foo");
202 }
203
204 #[test]
205 fn maintains_query() {
206 let mut uri = "/foo/?a=a".parse::<Uri>().unwrap();
207 uri.path_mut().trim_trailing_slash();
208 assert_eq!(uri.as_str(), "/foo?a=a");
209 }
210
211 #[test]
212 fn removes_multiple_trailing_slashes() {
213 let mut uri = "/foo////".parse::<Uri>().unwrap();
214 uri.path_mut().trim_trailing_slash();
215 assert_eq!(uri.as_str(), "/foo");
216 }
217
218 #[test]
219 fn removes_multiple_trailing_slashes_even_with_query() {
220 let mut uri = "/foo////?a=a".parse::<Uri>().unwrap();
221 uri.path_mut().trim_trailing_slash();
222 assert_eq!(uri.as_str(), "/foo?a=a");
223 }
224
225 #[test]
226 fn is_noop_on_index() {
227 let mut uri = "/".parse::<Uri>().unwrap();
228 uri.path_mut().trim_trailing_slash();
229 assert_eq!(uri.as_str(), "/");
230 }
231
232 #[test]
233 fn removes_multiple_trailing_slashes_on_index() {
234 let mut uri = "////".parse::<Uri>().unwrap();
235 uri.path_mut().trim_trailing_slash();
236 assert_eq!(uri.as_str(), "/");
237 }
238
239 #[test]
240 fn removes_multiple_trailing_slashes_on_index_even_with_query() {
241 let mut uri = "////?a=a".parse::<Uri>().unwrap();
242 uri.path_mut().trim_trailing_slash();
243 assert_eq!(uri.as_str(), "/?a=a");
244 }
245
246 #[test]
247 fn removes_multiple_preceding_slashes_even_with_query() {
248 let mut uri = "///foo//?a=a".parse::<Uri>().unwrap();
249 uri.path_mut().trim_trailing_slash();
250 assert_eq!(uri.as_str(), "/foo?a=a");
251 }
252
253 #[test]
254 fn removes_multiple_preceding_slashes() {
255 let mut uri = "///foo".parse::<Uri>().unwrap();
256 uri.path_mut().trim_trailing_slash();
257 assert_eq!(uri.as_str(), "/foo");
258 }
259
260 #[tokio::test]
261 async fn append_works() {
262 async fn handle(request: Request<()>) -> Result<Response<String>, Infallible> {
263 Ok(Response::new(request.uri().to_string()))
264 }
265
266 let svc = NormalizePathLayer::append_trailing_slash().into_layer(service_fn(handle));
267
268 let body = svc
269 .serve(Request::builder().uri("/foo").body(()).unwrap())
270 .await
271 .unwrap()
272 .into_body();
273
274 assert_eq!(body, "/foo/");
275 }
276
277 #[test]
278 fn is_noop_if_trailing_slash() {
279 let mut uri = "/foo/".parse::<Uri>().unwrap();
280 uri.path_mut().append_trailing_slash();
281 assert_eq!(uri.as_str(), "/foo/");
282 }
283
284 #[test]
285 fn append_maintains_query() {
286 let mut uri = "/foo?a=a".parse::<Uri>().unwrap();
287 uri.path_mut().append_trailing_slash();
288 assert_eq!(uri.as_str(), "/foo/?a=a");
289 }
290
291 #[test]
292 fn append_only_keeps_one_slash() {
293 let mut uri = "/foo////".parse::<Uri>().unwrap();
294 uri.path_mut().append_trailing_slash();
295 assert_eq!(uri.as_str(), "/foo/");
296 }
297
298 #[test]
299 fn append_only_keeps_one_slash_even_with_query() {
300 let mut uri = "/foo////?a=a".parse::<Uri>().unwrap();
301 uri.path_mut().append_trailing_slash();
302 assert_eq!(uri.as_str(), "/foo/?a=a");
303 }
304
305 #[test]
306 fn append_is_noop_on_index() {
307 let mut uri = "/".parse::<Uri>().unwrap();
308 uri.path_mut().append_trailing_slash();
309 assert_eq!(uri.as_str(), "/");
310 }
311
312 #[test]
313 fn append_removes_multiple_trailing_slashes_on_index() {
314 let mut uri = "////".parse::<Uri>().unwrap();
315 uri.path_mut().append_trailing_slash();
316 assert_eq!(uri.as_str(), "/");
317 }
318
319 #[test]
320 fn append_removes_multiple_trailing_slashes_on_index_even_with_query() {
321 let mut uri = "////?a=a".parse::<Uri>().unwrap();
322 uri.path_mut().append_trailing_slash();
323 assert_eq!(uri.as_str(), "/?a=a");
324 }
325
326 #[test]
327 fn append_removes_multiple_preceding_slashes_even_with_query() {
328 let mut uri = "///foo//?a=a".parse::<Uri>().unwrap();
329 uri.path_mut().append_trailing_slash();
330 assert_eq!(uri.as_str(), "/foo/?a=a");
331 }
332
333 #[test]
334 fn append_removes_multiple_preceding_slashes() {
335 let mut uri = "///foo".parse::<Uri>().unwrap();
336 uri.path_mut().append_trailing_slash();
337 assert_eq!(uri.as_str(), "/foo/");
338 }
339}