viz_router/
resources.rs

1//! Resource
2
3use viz_core::{
4    BoxHandler, Handler, HandlerExt, IntoResponse, Method, Next, Request, Response, Result,
5    Transform,
6};
7
8use crate::Route;
9
10/// A Kind for generating Resources path.
11#[derive(Clone, Debug, PartialEq, Eq)]
12enum Kind {
13    /// index | create
14    Empty,
15    /// new: `new`
16    New,
17    /// show | update | destroy: `{}_id`
18    Id,
19    /// edit: `{}_id/edit`
20    Edit,
21    /// `String`
22    Custom(String),
23}
24
25/// A resourceful route provides a mapping between HTTP verbs and URLs to handlers.
26#[derive(Clone, Debug, Default)]
27pub struct Resources {
28    name: String,
29    singular: bool,
30    routes: Vec<(Kind, Route)>,
31}
32
33impl Resources {
34    /// Named for the resources.
35    #[must_use]
36    pub fn named<S>(mut self, name: S) -> Self
37    where
38        S: AsRef<str>,
39    {
40        name.as_ref().clone_into(&mut self.name);
41        self
42    }
43
44    /// Without referencing an ID for a resource.
45    #[must_use]
46    pub const fn singular(mut self) -> Self {
47        self.singular = true;
48        self
49    }
50
51    /// Inserts a path-route pair into the resources.
52    #[must_use]
53    pub fn route<S>(mut self, path: S, route: Route) -> Self
54    where
55        S: AsRef<str>,
56    {
57        let kind = Kind::Custom(path.as_ref().to_owned());
58        match self
59            .routes
60            .iter_mut()
61            .find(|(p, _)| p == &kind)
62            .map(|(_, r)| r)
63        {
64            Some(r) => *r = route.into_iter().fold(r.clone(), |r, (m, h)| r.on(m, h)),
65            None => {
66                self.routes.push((kind, route));
67            }
68        }
69        self
70    }
71
72    fn on<H, O>(mut self, kind: Kind, method: Method, handler: H) -> Self
73    where
74        H: Handler<Request, Output = Result<O>> + Clone,
75        O: IntoResponse,
76    {
77        match self
78            .routes
79            .iter_mut()
80            .find(|(p, _)| p == &kind)
81            .map(|(_, r)| r)
82        {
83            Some(r) => {
84                *r = r.clone().on(method, handler);
85            }
86            None => {
87                self.routes.push((kind, Route::new().on(method, handler)));
88            }
89        }
90        self
91    }
92
93    /// Displays a list of the resources.
94    #[must_use]
95    pub fn index<H, O>(self, handler: H) -> Self
96    where
97        H: Handler<Request, Output = Result<O>> + Clone,
98        O: IntoResponse,
99    {
100        self.on(Kind::Empty, Method::GET, handler)
101    }
102
103    /// Returens an HTML form for creating the resources.
104    #[must_use]
105    pub fn new<H, O>(self, handler: H) -> Self
106    where
107        H: Handler<Request, Output = Result<O>> + Clone,
108        O: IntoResponse,
109    {
110        self.on(Kind::New, Method::GET, handler)
111    }
112
113    /// Creates the resources.
114    #[must_use]
115    pub fn create<H, O>(self, handler: H) -> Self
116    where
117        H: Handler<Request, Output = Result<O>> + Clone,
118        O: IntoResponse,
119    {
120        self.on(Kind::Empty, Method::POST, handler)
121    }
122
123    /// Displays the resources.
124    #[must_use]
125    pub fn show<H, O>(self, handler: H) -> Self
126    where
127        H: Handler<Request, Output = Result<O>> + Clone,
128        O: IntoResponse,
129    {
130        self.on(Kind::Id, Method::GET, handler)
131    }
132
133    /// Returens an HTML form for editing the resources.
134    #[must_use]
135    pub fn edit<H, O>(self, handler: H) -> Self
136    where
137        H: Handler<Request, Output = Result<O>> + Clone,
138        O: IntoResponse,
139    {
140        self.on(Kind::Edit, Method::GET, handler)
141    }
142
143    /// Updates the resources, by default the `PUT` verb.
144    #[must_use]
145    pub fn update<H, O>(self, handler: H) -> Self
146    where
147        H: Handler<Request, Output = Result<O>> + Clone,
148        O: IntoResponse,
149    {
150        self.on(Kind::Id, Method::PUT, handler)
151    }
152
153    /// Updates the resources, by the `PATCH` verb.
154    #[must_use]
155    pub fn update_with_patch<H, O>(self, handler: H) -> Self
156    where
157        H: Handler<Request, Output = Result<O>> + Clone,
158        O: IntoResponse,
159    {
160        self.on(Kind::Id, Method::PATCH, handler)
161    }
162
163    /// Deletes the resources.
164    #[must_use]
165    pub fn destroy<H, O>(self, handler: H) -> Self
166    where
167        H: Handler<Request, Output = Result<O>> + Clone,
168        O: IntoResponse,
169    {
170        self.on(Kind::Id, Method::DELETE, handler)
171    }
172
173    /// Takes a closure and creates an iterator which calls that closure on each handler.
174    #[must_use]
175    pub fn map_handler<F>(self, f: F) -> Self
176    where
177        F: Fn(BoxHandler) -> BoxHandler,
178    {
179        Self {
180            name: self.name,
181            singular: self.singular,
182            routes: self
183                .routes
184                .into_iter()
185                .map(|(path, route)| {
186                    (
187                        path,
188                        route
189                            .into_iter()
190                            .map(|(method, handler)| (method, f(handler)))
191                            .collect(),
192                    )
193                })
194                .collect(),
195        }
196    }
197
198    /// Transforms the types to a middleware and adds it.
199    #[must_use]
200    pub fn with<T>(self, t: T) -> Self
201    where
202        T: Transform<BoxHandler>,
203        T::Output: Handler<Request, Output = Result<Response>> + Clone,
204    {
205        self.map_handler(|handler| t.transform(handler).boxed())
206    }
207
208    /// Adds a middleware for the resources.
209    #[must_use]
210    pub fn with_handler<H>(self, f: H) -> Self
211    where
212        H: Handler<Next<Request, BoxHandler>, Output = Result<Response>> + Clone,
213    {
214        self.map_handler(|handler| handler.around(f.clone()).boxed())
215    }
216}
217
218impl IntoIterator for Resources {
219    type Item = (String, Route);
220
221    type IntoIter = std::vec::IntoIter<Self::Item>;
222
223    fn into_iter(self) -> Self::IntoIter {
224        self.routes
225            .into_iter()
226            .map(|(kind, route)| {
227                (
228                    match kind {
229                        Kind::Empty => String::new(),
230                        Kind::New => "new".to_string(),
231                        Kind::Id => {
232                            if self.singular {
233                                String::new()
234                            } else {
235                                format!(":{}_id", &self.name)
236                            }
237                        }
238                        Kind::Edit => {
239                            if self.singular {
240                                "edit".to_string()
241                            } else {
242                                format!(":{}_id/edit", &self.name)
243                            }
244                        }
245                        Kind::Custom(path) => path,
246                    },
247                    route,
248                )
249            })
250            .collect::<Vec<Self::Item>>()
251            .into_iter()
252    }
253}
254
255#[cfg(test)]
256#[allow(clippy::unused_async)]
257mod tests {
258    use super::Kind;
259    use crate::{get, Resources};
260    use http_body_util::BodyExt;
261    use viz_core::{
262        async_trait, Handler, HandlerExt, IntoResponse, Method, Next, Request, Response,
263        ResponseExt, Result, Transform,
264    };
265
266    #[tokio::test]
267    async fn resource() -> anyhow::Result<()> {
268        #[derive(Clone)]
269        struct Logger;
270
271        impl Logger {
272            const fn new() -> Self {
273                Self
274            }
275        }
276
277        impl<H: Clone> Transform<H> for Logger {
278            type Output = LoggerHandler<H>;
279
280            fn transform(&self, h: H) -> Self::Output {
281                LoggerHandler(h)
282            }
283        }
284
285        #[derive(Clone)]
286        struct LoggerHandler<H>(H);
287
288        #[async_trait]
289        impl<H> Handler<Request> for LoggerHandler<H>
290        where
291            H: Handler<Request>,
292        {
293            type Output = H::Output;
294
295            async fn call(&self, req: Request) -> Self::Output {
296                self.0.call(req).await
297            }
298        }
299
300        async fn before(req: Request) -> Result<Request> {
301            Ok(req)
302        }
303
304        async fn after(res: Result<Response>) -> Result<Response> {
305            res
306        }
307
308        async fn around<H, O>((req, handler): Next<Request, H>) -> Result<Response>
309        where
310            H: Handler<Request, Output = Result<O>>,
311            O: IntoResponse,
312        {
313            handler.call(req).await.map(IntoResponse::into_response)
314        }
315
316        async fn index(_: Request) -> Result<impl IntoResponse> {
317            Ok("index")
318        }
319
320        async fn any(_: Request) -> Result<&'static str> {
321            Ok("any")
322        }
323
324        async fn index_posts(_: Request) -> Result<Vec<u8>> {
325            Ok(b"index posts".to_vec())
326        }
327
328        async fn create_post(_: Request) -> Result<impl IntoResponse> {
329            Ok("create post")
330        }
331
332        async fn new_post(_: Request) -> Result<Response> {
333            Ok(Response::text("new post"))
334        }
335
336        async fn show_post(_: Request) -> Result<Response> {
337            Ok(Response::text("show post"))
338        }
339
340        async fn edit_post(_: Request) -> Result<Response> {
341            Ok(Response::text("edit post"))
342        }
343
344        async fn delete_post(_: Request) -> Result<Response> {
345            Ok(Response::text("delete post"))
346        }
347
348        async fn update_post(_: Request) -> Result<Response> {
349            Ok(Response::text("update post"))
350        }
351
352        async fn any_posts(_: Request) -> Result<Response> {
353            Ok(Response::text("any posts"))
354        }
355
356        async fn search_posts(_: Request) -> Result<Response> {
357            Ok(Response::text("search posts"))
358        }
359
360        let resource = Resources::default()
361            .index(index)
362            .update_with_patch(any_posts);
363
364        assert_eq!(2, resource.into_iter().count());
365
366        let resource = Resources::default()
367            .named("post")
368            .route("search", get(search_posts))
369            .index(index_posts.before(before))
370            .new(new_post)
371            .create(create_post)
372            .show(show_post.after(after))
373            .edit(edit_post.around(around))
374            .update(update_post)
375            .destroy(delete_post)
376            .update_with_patch(any)
377            .with(Logger::new())
378            .map_handler(|handler| {
379                handler
380                    .before(before)
381                    .after(after)
382                    .around(around)
383                    .with(Logger::new())
384                    .boxed()
385            });
386
387        assert_eq!(5, resource.clone().into_iter().count());
388        assert_eq!(
389            9,
390            resource
391                .clone()
392                .into_iter()
393                .fold(0, |sum, (_, r)| sum + r.into_iter().count())
394        );
395
396        let (_, h) = resource
397            .routes
398            .iter()
399            .find(|(p, _)| p == &Kind::Id)
400            .and_then(|(_, r)| r.methods.iter().find(|(m, _)| m == Method::GET))
401            .unwrap();
402
403        let res = h.call(Request::default()).await?;
404        assert_eq!(res.into_body().collect().await?.to_bytes(), "show post");
405
406        let handler = |_| async { Ok(()) };
407        let geocoder = Resources::default()
408            .singular()
409            .new(handler)
410            .create(handler)
411            .show(handler)
412            .edit(handler)
413            .update(handler)
414            .destroy(handler);
415
416        assert_eq!(
417            6,
418            geocoder
419                .into_iter()
420                .fold(0, |sum, (_, r)| sum + r.into_iter().count())
421        );
422
423        Ok(())
424    }
425}