1use std::sync::Arc;
23
24use rainier_http::{IntoResponse, Method, Request, Response};
25use rainier_middleware::{IntoMiddlewareStack, MiddlewareStack};
26use rainier_support::{str, BoxedFuture, Error, ErrorKind};
27
28use crate::handler::RouteHandler;
29use crate::router::Router;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum ResourceAction {
34 Index,
36 Create,
38 Store,
40 Show,
42 Edit,
44 Update,
46 Destroy,
48}
49
50impl ResourceAction {
51 pub const ALL: [ResourceAction; 7] = [
53 ResourceAction::Index,
54 ResourceAction::Create,
55 ResourceAction::Store,
56 ResourceAction::Show,
57 ResourceAction::Edit,
58 ResourceAction::Update,
59 ResourceAction::Destroy,
60 ];
61
62 pub const API: [ResourceAction; 5] = [
64 ResourceAction::Index,
65 ResourceAction::Store,
66 ResourceAction::Show,
67 ResourceAction::Update,
68 ResourceAction::Destroy,
69 ];
70
71 pub fn as_str(self) -> &'static str {
73 match self {
74 ResourceAction::Index => "index",
75 ResourceAction::Create => "create",
76 ResourceAction::Store => "store",
77 ResourceAction::Show => "show",
78 ResourceAction::Edit => "edit",
79 ResourceAction::Update => "update",
80 ResourceAction::Destroy => "destroy",
81 }
82 }
83
84 fn methods(self) -> Vec<Method> {
85 match self {
86 ResourceAction::Index
87 | ResourceAction::Create
88 | ResourceAction::Show
89 | ResourceAction::Edit => vec![Method::GET],
90 ResourceAction::Store => vec![Method::POST],
91 ResourceAction::Update => vec![Method::PUT, Method::PATCH],
92 ResourceAction::Destroy => vec![Method::DELETE],
93 }
94 }
95
96 fn uri_suffix(self, param: &str) -> String {
98 match self {
99 ResourceAction::Index | ResourceAction::Store => String::new(),
100 ResourceAction::Create => "/create".to_string(),
101 ResourceAction::Show | ResourceAction::Update | ResourceAction::Destroy => {
102 format!("/{{{param}}}")
103 }
104 ResourceAction::Edit => format!("/{{{param}}}/edit"),
105 }
106 }
107}
108
109#[derive(Default, Clone, Debug)]
136pub struct ControllerMiddleware {
137 entries: Vec<(Vec<ResourceAction>, MiddlewareStack)>,
138}
139
140impl ControllerMiddleware {
141 pub fn new() -> Self {
143 Self::default()
144 }
145
146 pub fn always(mut self, middleware: impl IntoMiddlewareStack) -> Self {
148 self.entries.push((ResourceAction::ALL.to_vec(), middleware.into_middleware_stack()));
149 self
150 }
151
152 pub fn only(
154 mut self,
155 actions: impl IntoIterator<Item = ResourceAction>,
156 middleware: impl IntoMiddlewareStack,
157 ) -> Self {
158 self.entries.push((actions.into_iter().collect(), middleware.into_middleware_stack()));
159 self
160 }
161
162 pub fn except(
168 mut self,
169 actions: impl IntoIterator<Item = ResourceAction>,
170 middleware: impl IntoMiddlewareStack,
171 ) -> Self {
172 let excluded: Vec<ResourceAction> = actions.into_iter().collect();
173 let included = ResourceAction::ALL.into_iter().filter(|a| !excluded.contains(a)).collect();
174
175 self.entries.push((included, middleware.into_middleware_stack()));
176 self
177 }
178
179 pub fn for_action(&self, action: ResourceAction) -> MiddlewareStack {
181 self.entries
182 .iter()
183 .filter(|(actions, _)| actions.contains(&action))
184 .fold(MiddlewareStack::new(), |stack, (_, middleware)| {
185 stack.with_stack(middleware.clone())
186 })
187 }
188
189 pub fn is_empty(&self) -> bool {
191 self.entries.iter().all(|(_, stack)| stack.is_empty())
192 }
193}
194
195#[async_trait::async_trait]
221pub trait ResourceController: Send + Sync + 'static {
222 async fn index(&self, request: Request) -> Response {
224 unsupported(request, "index")
225 }
226
227 async fn create(&self, request: Request) -> Response {
229 unsupported(request, "create")
230 }
231
232 async fn store(&self, request: Request) -> Response {
234 unsupported(request, "store")
235 }
236
237 async fn show(&self, request: Request) -> Response {
239 unsupported(request, "show")
240 }
241
242 async fn edit(&self, request: Request) -> Response {
244 unsupported(request, "edit")
245 }
246
247 async fn update(&self, request: Request) -> Response {
249 unsupported(request, "update")
250 }
251
252 async fn destroy(&self, request: Request) -> Response {
254 unsupported(request, "destroy")
255 }
256
257 fn middleware(&self) -> ControllerMiddleware {
270 ControllerMiddleware::new()
271 }
272}
273
274fn unsupported(request: Request, action: &str) -> Response {
275 Error::new(
276 ErrorKind::MethodNotAllowed,
277 format!("This resource does not support the `{action}` action."),
278 )
279 .with_details(serde_json_path(request.path()))
280 .into_response()
281}
282
283fn serde_json_path(path: &str) -> serde_json::Value {
284 serde_json::json!({ "path": path })
285}
286
287struct ResourceHandler {
289 controller: Arc<dyn ResourceController>,
290 action: ResourceAction,
291}
292
293impl RouteHandler for ResourceHandler {
294 fn call(&self, request: Request) -> BoxedFuture<Response> {
295 let controller = Arc::clone(&self.controller);
298 let action = self.action;
299
300 Box::pin(async move {
301 match action {
302 ResourceAction::Index => controller.index(request).await,
303 ResourceAction::Create => controller.create(request).await,
304 ResourceAction::Store => controller.store(request).await,
305 ResourceAction::Show => controller.show(request).await,
306 ResourceAction::Edit => controller.edit(request).await,
307 ResourceAction::Update => controller.update(request).await,
308 ResourceAction::Destroy => controller.destroy(request).await,
309 }
310 })
311 }
312}
313
314impl Router {
315 pub fn resource(&mut self, name: &str, controller: Arc<dyn ResourceController>) {
317 self.resource_actions(name, controller, &ResourceAction::ALL);
318 }
319
320 pub fn api_resource(&mut self, name: &str, controller: Arc<dyn ResourceController>) {
323 self.resource_actions(name, controller, &ResourceAction::API);
324 }
325
326 pub fn resource_actions(
332 &mut self,
333 name: &str,
334 controller: Arc<dyn ResourceController>,
335 actions: &[ResourceAction],
336 ) {
337 let base = format!("/{}", name.trim_matches('/'));
338 let param = str::singular(name.rsplit('/').next().unwrap_or(name));
339
340 let middleware = controller.middleware();
343
344 for action in ResourceAction::ALL {
345 if !actions.contains(&action) {
346 continue;
347 }
348
349 let handler: Arc<dyn RouteHandler> =
350 Arc::new(ResourceHandler { controller: Arc::clone(&controller), action });
351
352 let uri = format!("{base}{}", action.uri_suffix(¶m));
353 let route_name = format!("{}.{}", name.replace('/', "."), action.as_str());
354
355 let route = self.add_erased(action.methods(), uri, handler);
356 route.name(route_name);
357
358 let for_action = middleware.for_action(action);
359 if !for_action.is_empty() {
360 route.middleware(for_action);
361 }
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::router::CompiledRouter;
370 use rainier_http::StatusCode;
371
372 struct Posts;
373
374 #[async_trait::async_trait]
375 impl ResourceController for Posts {
376 async fn index(&self, _: Request) -> Response {
377 Response::text("index")
378 }
379 async fn show(&self, request: Request) -> Response {
380 Response::text(format!("show:{}", request.route_param("post").unwrap_or("?")))
381 }
382 async fn store(&self, _: Request) -> Response {
383 Response::text("store")
384 }
385 async fn destroy(&self, _: Request) -> Response {
386 Response::text("destroy")
387 }
388 }
389
390 fn compiled(router: Router) -> CompiledRouter {
391 router.compile(&rainier_container::Container::new()).expect("compiles")
392 }
393
394 async fn body_of(response: Response) -> String {
395 String::from_utf8(response.into_http().into_body().collect().await.unwrap().to_vec())
396 .unwrap()
397 }
398
399 fn request(method: Method, uri: &str) -> Request {
400 Request::builder().method(method).uri(uri).build()
401 }
402
403 fn posts_router() -> Router {
404 let mut router = Router::new();
405 router.resource("posts", Arc::new(Posts));
406 router
407 }
408
409 struct Tag(&'static str);
412
413 #[async_trait::async_trait]
414 impl rainier_middleware::Middleware for Tag {
415 async fn handle(&self, request: Request, next: rainier_middleware::Next) -> Response {
416 next.run(request).await.with_added_header("x-tag", self.0)
417 }
418 }
419
420 struct Guarded;
422
423 #[async_trait::async_trait]
424 impl ResourceController for Guarded {
425 async fn index(&self, _: Request) -> Response {
426 Response::text("index")
427 }
428 async fn store(&self, _: Request) -> Response {
429 Response::text("store")
430 }
431 async fn destroy(&self, _: Request) -> Response {
432 Response::text("destroy")
433 }
434
435 fn middleware(&self) -> ControllerMiddleware {
436 ControllerMiddleware::new()
437 .always(Tag("every"))
438 .only([ResourceAction::Store, ResourceAction::Destroy], Tag("writes"))
439 }
440 }
441
442 fn tags(response: &Response) -> Vec<String> {
443 response
444 .headers()
445 .get_all("x-tag")
446 .iter()
447 .map(|value| value.to_str().unwrap().to_string())
448 .collect()
449 }
450
451 #[tokio::test]
452 async fn controller_middleware_applies_only_to_the_actions_it_names() {
453 let mut router = Router::new();
454 router.resource("posts", Arc::new(Guarded));
455 let compiled = compiled(router);
456
457 let read = compiled.dispatch(request(Method::GET, "/posts")).await;
458 assert_eq!(tags(&read), vec!["every"]);
459
460 let write = compiled.dispatch(request(Method::POST, "/posts")).await;
461 assert_eq!(tags(&write), vec!["writes", "every"]);
462 }
463
464 #[test]
465 fn except_covers_an_action_added_later() {
466 let middleware = ControllerMiddleware::new().except([ResourceAction::Index], Tag("guard"));
470
471 for action in ResourceAction::ALL {
472 let expected = usize::from(action != ResourceAction::Index);
473 assert_eq!(middleware.for_action(action).len(), expected, "{action:?}");
474 }
475 }
476
477 #[test]
478 fn a_controller_declares_no_middleware_by_default() {
479 assert!(ControllerMiddleware::new().is_empty());
480 assert!(Posts.middleware().is_empty());
481 }
482
483 #[tokio::test]
484 async fn a_group_wraps_a_controllers_own_middleware() {
485 use crate::router::GroupAttributes;
488
489 let mut router = Router::new();
490 router.group(GroupAttributes::new().middleware(Tag("group")), |router| {
491 router.resource("posts", Arc::new(Guarded));
492 });
493
494 let response = compiled(router).dispatch(request(Method::GET, "/posts")).await;
495 assert_eq!(tags(&response), vec!["every", "group"]);
496 }
497
498 #[test]
499 fn registers_all_seven_routes_with_conventional_names() {
500 let router = posts_router();
501 assert_eq!(router.len(), 7);
502
503 assert_eq!(router.uri_for("posts.index"), Some("/posts"));
504 assert_eq!(router.uri_for("posts.create"), Some("/posts/create"));
505 assert_eq!(router.uri_for("posts.store"), Some("/posts"));
506 assert_eq!(router.uri_for("posts.show"), Some("/posts/{post}"));
507 assert_eq!(router.uri_for("posts.edit"), Some("/posts/{post}/edit"));
508 assert_eq!(router.uri_for("posts.update"), Some("/posts/{post}"));
509 assert_eq!(router.uri_for("posts.destroy"), Some("/posts/{post}"));
510 }
511
512 #[test]
513 fn the_parameter_is_the_singular_of_the_resource() {
514 let mut router = Router::new();
515 router.resource("categories", Arc::new(Posts));
516 assert_eq!(router.uri_for("categories.show"), Some("/categories/{category}"));
517 }
518
519 #[tokio::test]
520 async fn create_is_declared_before_the_parameter_route() {
521 let compiled = compiled(posts_router());
523 let response = compiled.dispatch(request(Method::GET, "/posts/create")).await;
524 assert_eq!(
525 response.status(),
526 StatusCode::METHOD_NOT_ALLOWED,
527 "should reach the unimplemented `create`, not `show`"
528 );
529 }
530
531 #[tokio::test]
532 async fn dispatches_each_action() {
533 let compiled = compiled(posts_router());
534
535 assert_eq!(body_of(compiled.dispatch(request(Method::GET, "/posts")).await).await, "index");
536 assert_eq!(
537 body_of(compiled.dispatch(request(Method::POST, "/posts")).await).await,
538 "store"
539 );
540 assert_eq!(
541 body_of(compiled.dispatch(request(Method::GET, "/posts/7")).await).await,
542 "show:7"
543 );
544 assert_eq!(
545 body_of(compiled.dispatch(request(Method::DELETE, "/posts/7")).await).await,
546 "destroy"
547 );
548 }
549
550 #[tokio::test]
551 async fn update_answers_both_put_and_patch() {
552 let compiled = compiled(posts_router());
553 for method in [Method::PUT, Method::PATCH] {
554 let response = compiled.dispatch(request(method.clone(), "/posts/7")).await;
555 assert_eq!(
556 response.status(),
557 StatusCode::METHOD_NOT_ALLOWED,
558 "{method} should reach the unimplemented `update` action"
559 );
560 }
561 }
562
563 #[tokio::test]
564 async fn an_unimplemented_action_is_a_405_naming_itself() {
565 let compiled = compiled(posts_router());
566 let response = compiled.dispatch(request(Method::GET, "/posts/7/edit")).await;
567 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
568 assert!(body_of(response).await.contains("edit"));
569 }
570
571 #[test]
572 fn an_api_resource_omits_the_form_pages() {
573 let mut router = Router::new();
574 router.api_resource("posts", Arc::new(Posts));
575
576 assert_eq!(router.len(), 5);
577 assert_eq!(router.uri_for("posts.create"), None);
578 assert_eq!(router.uri_for("posts.edit"), None);
579 assert_eq!(router.uri_for("posts.index"), Some("/posts"));
580 }
581
582 #[test]
583 fn only_the_listed_actions_are_registered() {
584 let mut router = Router::new();
585 router.resource_actions(
586 "posts",
587 Arc::new(Posts),
588 &[ResourceAction::Show, ResourceAction::Index],
589 );
590
591 assert_eq!(router.len(), 2);
592 assert_eq!(router.uri_for("posts.store"), None);
593 assert_eq!(router.routes()[0].route_name(), Some("posts.index"));
595 }
596
597 #[test]
598 fn nested_resource_names_use_dots() {
599 let mut router = Router::new();
600 router.resource("admin/posts", Arc::new(Posts));
601 assert_eq!(router.uri_for("admin.posts.index"), Some("/admin/posts"));
602 assert_eq!(router.uri_for("admin.posts.show"), Some("/admin/posts/{post}"));
603 }
604}