Skip to main content

rainier_routing/
resource.rs

1//! Resource routes — [`ResourceController`] and the seven RESTful routes
2//! Laravel's `Route::resource` generates.
3//!
4//! | Verb | URI | Action | Name |
5//! |---|---|---|---|
6//! | GET | `/posts` | index | `posts.index` |
7//! | GET | `/posts/create` | create | `posts.create` |
8//! | POST | `/posts` | store | `posts.store` |
9//! | GET | `/posts/{post}` | show | `posts.show` |
10//! | GET | `/posts/{post}/edit` | edit | `posts.edit` |
11//! | PUT, PATCH | `/posts/{post}` | update | `posts.update` |
12//! | DELETE | `/posts/{post}` | destroy | `posts.destroy` |
13//!
14//! The parameter name is the singular of the resource name (`posts` → `post`),
15//! and `create`/`edit` are registered **before** `{post}` so the literal
16//! segments are not swallowed by the parameter.
17//!
18//! Every action defaults to `405 Method Not Allowed`, so a controller
19//! implements only what it supports and the rest answer honestly rather than
20//! panicking or 404-ing.
21
22use 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/// One of the seven RESTful actions.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum ResourceAction {
34    /// List the collection.
35    Index,
36    /// Show the creation form.
37    Create,
38    /// Persist a new member.
39    Store,
40    /// Show one member.
41    Show,
42    /// Show the edit form for one member.
43    Edit,
44    /// Update one member.
45    Update,
46    /// Delete one member.
47    Destroy,
48}
49
50impl ResourceAction {
51    /// All seven, in registration order.
52    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    /// The five an API needs — no HTML form pages.
63    pub const API: [ResourceAction; 5] = [
64        ResourceAction::Index,
65        ResourceAction::Store,
66        ResourceAction::Show,
67        ResourceAction::Update,
68        ResourceAction::Destroy,
69    ];
70
71    /// The action's name, as used in the route name suffix.
72    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    /// The URI suffix appended to the resource's base path.
97    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/// Middleware a controller applies to its own actions.
110///
111/// Laravel's `$this->middleware('auth')->only(['store', 'update'])`, spelled so
112/// that the actions are an enum and the middleware is a value.
113///
114/// Declaring it on the controller rather than at the route puts the rule next
115/// to the code it protects: someone adding a `destroy` action reads the guard
116/// in the same file, instead of having to remember that a route file three
117/// directories away is what stops it being public.
118///
119/// ```
120/// use rainier_routing::{ControllerMiddleware, ResourceAction};
121/// # use rainier_middleware::{AddHeaders, ThrottleRequests};
122///
123/// let middleware = ControllerMiddleware::new()
124///     // Every action.
125///     .always(AddHeaders::security_defaults())
126///     // Only the ones that change something.
127///     .only(
128///         [ResourceAction::Store, ResourceAction::Update, ResourceAction::Destroy],
129///         ThrottleRequests::per_minute(20),
130///     );
131///
132/// assert_eq!(middleware.for_action(ResourceAction::Index).len(), 1);
133/// assert_eq!(middleware.for_action(ResourceAction::Store).len(), 2);
134/// ```
135#[derive(Default, Clone, Debug)]
136pub struct ControllerMiddleware {
137    entries: Vec<(Vec<ResourceAction>, MiddlewareStack)>,
138}
139
140impl ControllerMiddleware {
141    /// No middleware.
142    pub fn new() -> Self {
143        Self::default()
144    }
145
146    /// Apply to every action.
147    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    /// Apply to the listed actions only.
153    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    /// Apply to every action but the listed ones.
163    ///
164    /// The safer default of the two: a new action added later is guarded unless
165    /// it is named here, rather than unguarded unless it is named in an
166    /// [`only`](Self::only).
167    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    /// The stack for one action, in declaration order.
180    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    /// Whether anything is declared.
190    pub fn is_empty(&self) -> bool {
191        self.entries.iter().all(|(_, stack)| stack.is_empty())
192    }
193}
194
195/// A controller serving a RESTful resource.
196///
197/// Implement the actions the resource supports; the rest answer `405`.
198///
199/// ```
200/// use rainier_http::{Request, Response};
201/// use rainier_routing::{ResourceController, Router};
202/// use std::sync::Arc;
203///
204/// struct PostController;
205///
206/// #[async_trait::async_trait]
207/// impl ResourceController for PostController {
208///     async fn index(&self, _request: Request) -> Response {
209///         Response::text("every post")
210///     }
211///     async fn show(&self, request: Request) -> Response {
212///         Response::text(format!("post {}", request.route_param("post").unwrap_or("?")))
213///     }
214/// }
215///
216/// let mut router = Router::new();
217/// router.resource("posts", Arc::new(PostController));
218/// assert_eq!(router.uri_for("posts.show"), Some("/posts/{post}"));
219/// ```
220#[async_trait::async_trait]
221pub trait ResourceController: Send + Sync + 'static {
222    /// `GET /resource`
223    async fn index(&self, request: Request) -> Response {
224        unsupported(request, "index")
225    }
226
227    /// `GET /resource/create`
228    async fn create(&self, request: Request) -> Response {
229        unsupported(request, "create")
230    }
231
232    /// `POST /resource`
233    async fn store(&self, request: Request) -> Response {
234        unsupported(request, "store")
235    }
236
237    /// `GET /resource/{id}`
238    async fn show(&self, request: Request) -> Response {
239        unsupported(request, "show")
240    }
241
242    /// `GET /resource/{id}/edit`
243    async fn edit(&self, request: Request) -> Response {
244        unsupported(request, "edit")
245    }
246
247    /// `PUT|PATCH /resource/{id}`
248    async fn update(&self, request: Request) -> Response {
249        unsupported(request, "update")
250    }
251
252    /// `DELETE /resource/{id}`
253    async fn destroy(&self, request: Request) -> Response {
254        unsupported(request, "destroy")
255    }
256
257    /// Middleware this controller applies to its own actions.
258    ///
259    /// Runs **inside** anything the route's group applied and outside the
260    /// action itself, so a group's session middleware still wraps a
261    /// controller's authorisation check.
262    ///
263    /// ```ignore
264    /// fn middleware(&self) -> ControllerMiddleware {
265    ///     ControllerMiddleware::new()
266    ///         .except([ResourceAction::Index, ResourceAction::Show], Authenticate::new(auth))
267    /// }
268    /// ```
269    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
287/// Routes one action to its controller method.
288struct ResourceHandler {
289    controller: Arc<dyn ResourceController>,
290    action: ResourceAction,
291}
292
293impl RouteHandler for ResourceHandler {
294    fn call(&self, request: Request) -> BoxedFuture<Response> {
295        // The `Arc` is cloned into the future so the borrow the async trait
296        // method takes of `&*controller` outlives this call.
297        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    /// Register all seven RESTful routes for `name`.
316    pub fn resource(&mut self, name: &str, controller: Arc<dyn ResourceController>) {
317        self.resource_actions(name, controller, &ResourceAction::ALL);
318    }
319
320    /// Register the five API routes for `name` — everything except the
321    /// `create` and `edit` form pages.
322    pub fn api_resource(&mut self, name: &str, controller: Arc<dyn ResourceController>) {
323        self.resource_actions(name, controller, &ResourceAction::API);
324    }
325
326    /// Register exactly the listed actions.
327    ///
328    /// They are always registered in [`ResourceAction::ALL`] order regardless
329    /// of the order given, because `/posts/create` must be declared before
330    /// `/posts/{post}` or the parameter swallows it.
331    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        // Asked once rather than per action: a controller building its stack in
341        // `middleware()` should not have that run seven times.
342        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(&param));
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    // --- controller middleware ---------------------------------------------
410
411    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    /// A controller that guards its writes and nothing else.
421    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        // The reason `except` is the safer of the two: the guard applies to
467        // everything that is not explicitly opted out, so a new action arrives
468        // protected.
469        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        // Order matters: the group is outside, so it sees the response the
486        // controller's middleware produced.
487        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        // Otherwise `/posts/create` would match `show` with post = "create".
522        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        // Registration order is canonical, not the order given.
594        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}