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