axum_extra/routing/
resource.rs

1use axum::{
2    handler::Handler,
3    routing::{delete, get, on, post, MethodFilter, MethodRouter},
4    Router,
5};
6
7/// A resource which defines a set of conventional CRUD routes.
8///
9/// # Example
10///
11/// ```rust
12/// use axum::{Router, routing::get, extract::Path};
13/// use axum_extra::routing::{RouterExt, Resource};
14///
15/// let users = Resource::named("users")
16///     // Define a route for `GET /users`
17///     .index(|| async {})
18///     // `POST /users`
19///     .create(|| async {})
20///     // `GET /users/new`
21///     .new(|| async {})
22///     // `GET /users/:users_id`
23///     .show(|Path(user_id): Path<u64>| async {})
24///     // `GET /users/:users_id/edit`
25///     .edit(|Path(user_id): Path<u64>| async {})
26///     // `PUT or PATCH /users/:users_id`
27///     .update(|Path(user_id): Path<u64>| async {})
28///     // `DELETE /users/:users_id`
29///     .destroy(|Path(user_id): Path<u64>| async {});
30///
31/// let app = Router::new().merge(users);
32/// # let _: Router = app;
33/// ```
34#[derive(Debug)]
35#[must_use]
36pub struct Resource<S = ()> {
37    pub(crate) name: String,
38    pub(crate) router: Router<S>,
39}
40
41impl<S> Resource<S>
42where
43    S: Clone + Send + Sync + 'static,
44{
45    /// Create a `Resource` with the given name.
46    ///
47    /// All routes will be nested at `/{resource_name}`.
48    pub fn named(resource_name: &str) -> Self {
49        Self {
50            name: resource_name.to_owned(),
51            router: Router::new(),
52        }
53    }
54
55    /// Add a handler at `GET /{resource_name}`.
56    pub fn index<H, T>(self, handler: H) -> Self
57    where
58        H: Handler<T, S>,
59        T: 'static,
60    {
61        let path = self.index_create_path();
62        self.route(&path, get(handler))
63    }
64
65    /// Add a handler at `POST /{resource_name}`.
66    pub fn create<H, T>(self, handler: H) -> Self
67    where
68        H: Handler<T, S>,
69        T: 'static,
70    {
71        let path = self.index_create_path();
72        self.route(&path, post(handler))
73    }
74
75    /// Add a handler at `GET /{resource_name}/new`.
76    pub fn new<H, T>(self, handler: H) -> Self
77    where
78        H: Handler<T, S>,
79        T: 'static,
80    {
81        let path = format!("/{}/new", self.name);
82        self.route(&path, get(handler))
83    }
84
85    /// Add a handler at `GET /{resource_name}/:{resource_name}_id`.
86    pub fn show<H, T>(self, handler: H) -> Self
87    where
88        H: Handler<T, S>,
89        T: 'static,
90    {
91        let path = self.show_update_destroy_path();
92        self.route(&path, get(handler))
93    }
94
95    /// Add a handler at `GET /{resource_name}/:{resource_name}_id/edit`.
96    pub fn edit<H, T>(self, handler: H) -> Self
97    where
98        H: Handler<T, S>,
99        T: 'static,
100    {
101        let path = format!("/{0}/:{0}_id/edit", self.name);
102        self.route(&path, get(handler))
103    }
104
105    /// Add a handler at `PUT or PATCH /resource_name/:{resource_name}_id`.
106    pub fn update<H, T>(self, handler: H) -> Self
107    where
108        H: Handler<T, S>,
109        T: 'static,
110    {
111        let path = self.show_update_destroy_path();
112        self.route(
113            &path,
114            on(MethodFilter::PUT.or(MethodFilter::PATCH), handler),
115        )
116    }
117
118    /// Add a handler at `DELETE /{resource_name}/:{resource_name}_id`.
119    pub fn destroy<H, T>(self, handler: H) -> Self
120    where
121        H: Handler<T, S>,
122        T: 'static,
123    {
124        let path = self.show_update_destroy_path();
125        self.route(&path, delete(handler))
126    }
127
128    fn index_create_path(&self) -> String {
129        format!("/{}", self.name)
130    }
131
132    fn show_update_destroy_path(&self) -> String {
133        format!("/{0}/:{0}_id", self.name)
134    }
135
136    fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
137        self.router = self.router.route(path, method_router);
138        self
139    }
140}
141
142impl<S> From<Resource<S>> for Router<S> {
143    fn from(resource: Resource<S>) -> Self {
144        resource.router
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    #[allow(unused_imports)]
151    use super::*;
152    use axum::{body::Body, extract::Path, http::Method};
153    use http::Request;
154    use http_body_util::BodyExt;
155    use tower::ServiceExt;
156
157    #[tokio::test]
158    async fn works() {
159        let users = Resource::named("users")
160            .index(|| async { "users#index" })
161            .create(|| async { "users#create" })
162            .new(|| async { "users#new" })
163            .show(|Path(id): Path<u64>| async move { format!("users#show id={id}") })
164            .edit(|Path(id): Path<u64>| async move { format!("users#edit id={id}") })
165            .update(|Path(id): Path<u64>| async move { format!("users#update id={id}") })
166            .destroy(|Path(id): Path<u64>| async move { format!("users#destroy id={id}") });
167
168        let app = Router::new().merge(users);
169
170        assert_eq!(call_route(&app, Method::GET, "/users").await, "users#index");
171
172        assert_eq!(
173            call_route(&app, Method::POST, "/users").await,
174            "users#create"
175        );
176
177        assert_eq!(
178            call_route(&app, Method::GET, "/users/new").await,
179            "users#new"
180        );
181
182        assert_eq!(
183            call_route(&app, Method::GET, "/users/1").await,
184            "users#show id=1"
185        );
186
187        assert_eq!(
188            call_route(&app, Method::GET, "/users/1/edit").await,
189            "users#edit id=1"
190        );
191
192        assert_eq!(
193            call_route(&app, Method::PATCH, "/users/1").await,
194            "users#update id=1"
195        );
196
197        assert_eq!(
198            call_route(&app, Method::PUT, "/users/1").await,
199            "users#update id=1"
200        );
201
202        assert_eq!(
203            call_route(&app, Method::DELETE, "/users/1").await,
204            "users#destroy id=1"
205        );
206    }
207
208    async fn call_route(app: &Router, method: Method, uri: &str) -> String {
209        let res = app
210            .clone()
211            .oneshot(
212                Request::builder()
213                    .method(method)
214                    .uri(uri)
215                    .body(Body::empty())
216                    .unwrap(),
217            )
218            .await
219            .unwrap();
220        let bytes = res.collect().await.unwrap().to_bytes();
221        String::from_utf8(bytes.to_vec()).unwrap()
222    }
223}