Skip to main content

rainier_routing/
resource.rs

1//! Resource routes — [`ResourceController`] and the seven RESTful routes
2//! `router.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/// Declared with the actions as an enum and the middleware as a value.
112///
113/// Declaring it on the controller rather than at the route puts the rule next
114/// to the code it protects: someone adding a `destroy` action reads the guard
115/// in the same file, instead of having to remember that a route file three
116/// directories away is what stops it being public.
117///
118/// ```
119/// use rainier_routing::{ControllerMiddleware, ResourceAction};
120/// # use rainier_middleware::{AddHeaders, ThrottleRequests};
121///
122/// let middleware = ControllerMiddleware::new()
123///     // Every action.
124///     .always(AddHeaders::security_defaults())
125///     // Only the ones that change something.
126///     .only(
127///         [ResourceAction::Store, ResourceAction::Update, ResourceAction::Destroy],
128///         ThrottleRequests::per_minute(20),
129///     );
130///
131/// assert_eq!(middleware.for_action(ResourceAction::Index).len(), 1);
132/// assert_eq!(middleware.for_action(ResourceAction::Store).len(), 2);
133/// ```
134#[derive(Default, Clone, Debug)]
135pub struct ControllerMiddleware {
136    entries: Vec<(Vec<ResourceAction>, MiddlewareStack)>,
137}
138
139impl ControllerMiddleware {
140    /// No middleware.
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    /// Apply to every action.
146    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    /// Apply to the listed actions only.
152    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    /// Apply to every action but the listed ones.
162    ///
163    /// The safer default of the two: a new action added later is guarded unless
164    /// it is named here, rather than unguarded unless it is named in an
165    /// [`only`](Self::only).
166    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    /// The stack for one action, in declaration order.
179    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    /// Whether anything is declared.
189    pub fn is_empty(&self) -> bool {
190        self.entries.iter().all(|(_, stack)| stack.is_empty())
191    }
192}
193
194/// A controller serving a RESTful resource.
195///
196/// Implement the actions the resource supports; the rest answer `405`.
197///
198/// ```
199/// use rainier_http::{Request, Response};
200/// use rainier_routing::{ResourceController, Router};
201/// use std::sync::Arc;
202///
203/// struct PostController;
204///
205/// #[async_trait::async_trait]
206/// impl ResourceController for PostController {
207///     async fn index(&self, _request: Request) -> Response {
208///         Response::text("every post")
209///     }
210///     async fn show(&self, request: Request) -> Response {
211///         Response::text(format!("post {}", request.route_param("post").unwrap_or("?")))
212///     }
213/// }
214///
215/// let mut router = Router::new();
216/// router.resource("posts", Arc::new(PostController));
217/// assert_eq!(router.uri_for("posts.show"), Some("/posts/{post}"));
218/// ```
219#[async_trait::async_trait]
220pub trait ResourceController: Send + Sync + 'static {
221    /// `GET /resource`
222    async fn index(&self, request: Request) -> Response {
223        unsupported(request, "index")
224    }
225
226    /// `GET /resource/create`
227    async fn create(&self, request: Request) -> Response {
228        unsupported(request, "create")
229    }
230
231    /// `POST /resource`
232    async fn store(&self, request: Request) -> Response {
233        unsupported(request, "store")
234    }
235
236    /// `GET /resource/{id}`
237    async fn show(&self, request: Request) -> Response {
238        unsupported(request, "show")
239    }
240
241    /// `GET /resource/{id}/edit`
242    async fn edit(&self, request: Request) -> Response {
243        unsupported(request, "edit")
244    }
245
246    /// `PUT|PATCH /resource/{id}`
247    async fn update(&self, request: Request) -> Response {
248        unsupported(request, "update")
249    }
250
251    /// `DELETE /resource/{id}`
252    async fn destroy(&self, request: Request) -> Response {
253        unsupported(request, "destroy")
254    }
255
256    /// Middleware this controller applies to its own actions.
257    ///
258    /// Runs **inside** anything the route's group applied and outside the
259    /// action itself, so a group's session middleware still wraps a
260    /// controller's authorisation check.
261    ///
262    /// ```ignore
263    /// fn middleware(&self) -> ControllerMiddleware {
264    ///     ControllerMiddleware::new()
265    ///         .except([ResourceAction::Index, ResourceAction::Show], Authenticate::new(auth))
266    /// }
267    /// ```
268    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
286/// Routes one action to its controller method.
287struct ResourceHandler {
288    controller: Arc<dyn ResourceController>,
289    action: ResourceAction,
290}
291
292impl RouteHandler for ResourceHandler {
293    fn call(&self, request: Request) -> BoxedFuture<Response> {
294        // The `Arc` is cloned into the future so the borrow the async trait
295        // method takes of `&*controller` outlives this call.
296        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    /// Register all seven RESTful routes for `name`.
315    pub fn resource(&mut self, name: &str, controller: Arc<dyn ResourceController>) {
316        self.resource_actions(name, controller, &ResourceAction::ALL);
317    }
318
319    /// Register the five API routes for `name` — everything except the
320    /// `create` and `edit` form pages.
321    pub fn api_resource(&mut self, name: &str, controller: Arc<dyn ResourceController>) {
322        self.resource_actions(name, controller, &ResourceAction::API);
323    }
324
325    /// Register exactly the listed actions.
326    ///
327    /// They are always registered in [`ResourceAction::ALL`] order regardless
328    /// of the order given, because `/posts/create` must be declared before
329    /// `/posts/{post}` or the parameter swallows it.
330    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        // Asked once rather than per action: a controller building its stack in
340        // `middleware()` should not have that run seven times.
341        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(&param));
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    // --- controller middleware ---------------------------------------------
409
410    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    /// A controller that guards its writes and nothing else.
420    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        // The reason `except` is the safer of the two: the guard applies to
466        // everything that is not explicitly opted out, so a new action arrives
467        // protected.
468        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        // Order matters: the group is outside, so it sees the response the
485        // controller's middleware produced.
486        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        // Otherwise `/posts/create` would match `show` with post = "create".
521        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        // Registration order is canonical, not the order given.
593        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}