1use viz_core::{
4 BoxHandler, Handler, HandlerExt, IntoResponse, Method, Next, Request, Response, Result,
5 Transform,
6};
7
8use crate::Route;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12enum Kind {
13 Empty,
15 New,
17 Id,
19 Edit,
21 Custom(String),
23}
24
25#[derive(Clone, Debug, Default)]
27pub struct Resources {
28 name: String,
29 singular: bool,
30 routes: Vec<(Kind, Route)>,
31}
32
33impl Resources {
34 #[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 #[must_use]
46 pub const fn singular(mut self) -> Self {
47 self.singular = true;
48 self
49 }
50
51 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}