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    ///
87    /// For example when the resources are posts: `GET /post/{post_id}`.
88    pub fn show<H, T>(self, handler: H) -> Self
89    where
90        H: Handler<T, S>,
91        T: 'static,
92    {
93        let path = self.show_update_destroy_path();
94        self.route(&path, get(handler))
95    }
96
97    /// Add a handler at `GET /<resource_name>/{<resource_name>_id}/edit`.
98    ///
99    /// For example when the resources are posts: `GET /post/{post_id}/edit`.
100    pub fn edit<H, T>(self, handler: H) -> Self
101    where
102        H: Handler<T, S>,
103        T: 'static,
104    {
105        let path = format!("/{0}/{{{0}_id}}/edit", self.name);
106        self.route(&path, get(handler))
107    }
108
109    /// Add a handler at `PUT or PATCH /<resource_name>/{<resource_name>_id}`.
110    ///
111    /// For example when the resources are posts: `PUT /post/{post_id}`.
112    pub fn update<H, T>(self, handler: H) -> Self
113    where
114        H: Handler<T, S>,
115        T: 'static,
116    {
117        let path = self.show_update_destroy_path();
118        self.route(
119            &path,
120            on(MethodFilter::PUT.or(MethodFilter::PATCH), handler),
121        )
122    }
123
124    /// Add a handler at `DELETE /<resource_name>/{<resource_name>_id}`.
125    ///
126    /// For example when the resources are posts: `DELETE /post/{post_id}`.
127    pub fn destroy<H, T>(self, handler: H) -> Self
128    where
129        H: Handler<T, S>,
130        T: 'static,
131    {
132        let path = self.show_update_destroy_path();
133        self.route(&path, delete(handler))
134    }
135
136    fn index_create_path(&self) -> String {
137        format!("/{}", self.name)
138    }
139
140    fn show_update_destroy_path(&self) -> String {
141        format!("/{0}/{{{0}_id}}", self.name)
142    }
143
144    fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
145        self.router = self.router.route(path, method_router);
146        self
147    }
148}
149
150impl<S> From<Resource<S>> for Router<S> {
151    fn from(resource: Resource<S>) -> Self {
152        resource.router
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    #[allow(unused_imports)]
159    use super::*;
160    use axum::{body::Body, extract::Path, http::Method};
161    use http::Request;
162    use http_body_util::BodyExt;
163    use tower::ServiceExt;
164
165    #[tokio::test]
166    async fn works() {
167        let users = Resource::named("users")
168            .index(|| async { "users#index" })
169            .create(|| async { "users#create" })
170            .new(|| async { "users#new" })
171            .show(|Path(id): Path<u64>| async move { format!("users#show id={id}") })
172            .edit(|Path(id): Path<u64>| async move { format!("users#edit id={id}") })
173            .update(|Path(id): Path<u64>| async move { format!("users#update id={id}") })
174            .destroy(|Path(id): Path<u64>| async move { format!("users#destroy id={id}") });
175
176        let app = Router::new().merge(users);
177
178        assert_eq!(call_route(&app, Method::GET, "/users").await, "users#index");
179
180        assert_eq!(
181            call_route(&app, Method::POST, "/users").await,
182            "users#create"
183        );
184
185        assert_eq!(
186            call_route(&app, Method::GET, "/users/new").await,
187            "users#new"
188        );
189
190        assert_eq!(
191            call_route(&app, Method::GET, "/users/1").await,
192            "users#show id=1"
193        );
194
195        assert_eq!(
196            call_route(&app, Method::GET, "/users/1/edit").await,
197            "users#edit id=1"
198        );
199
200        assert_eq!(
201            call_route(&app, Method::PATCH, "/users/1").await,
202            "users#update id=1"
203        );
204
205        assert_eq!(
206            call_route(&app, Method::PUT, "/users/1").await,
207            "users#update id=1"
208        );
209
210        assert_eq!(
211            call_route(&app, Method::DELETE, "/users/1").await,
212            "users#destroy id=1"
213        );
214    }
215
216    async fn call_route(app: &Router, method: Method, uri: &str) -> String {
217        let res = app
218            .clone()
219            .oneshot(
220                Request::builder()
221                    .method(method)
222                    .uri(uri)
223                    .body(Body::empty())
224                    .unwrap(),
225            )
226            .await
227            .unwrap();
228        let bytes = res.collect().await.unwrap().to_bytes();
229        String::from_utf8(bytes.to_vec()).unwrap()
230    }
231}