diff --git a/axum-extra/src/routing/mod.rs b/axum-extra/src/routing/mod.rs index df83ff52..61e8c137 100644 --- a/axum-extra/src/routing/mod.rs +++ b/axum-extra/src/routing/mod.rs @@ -1,6 +1,6 @@ //! Additional types for defining routes. -use axum::Router; +use axum::{body::Body, Router}; mod resource; @@ -8,24 +8,55 @@ pub use self::resource::Resource; /// Extension trait that adds additional methods to [`Router`]. pub trait RouterExt: sealed::Sealed { - /// Add a [`Resource`] to the router. + /// Add the routes from `T`'s [`HasRoutes::routes`] to this router. /// - /// See [`Resource`] for more details. - fn resource(self, name: &str, f: F) -> Self + /// # Example + /// + /// Using [`Resource`] which implements [`HasRoutes`]: + /// + /// ```rust + /// use axum::{Router, routing::get}; + /// use axum_extra::routing::{RouterExt, Resource}; + /// + /// let app = Router::new() + /// .with( + /// Resource::named("users") + /// .index(|| async {}) + /// .create(|| async {}) + /// ) + /// .with( + /// Resource::named("teams").index(|| async {}) + /// ); + /// # let _: Router = app; + /// ``` + fn with(self, routes: T) -> Self where - F: FnOnce(resource::Resource) -> resource::Resource; + T: HasRoutes; } -impl RouterExt for Router { - fn resource(self, name: &str, f: F) -> Self +impl RouterExt for Router +where + B: Send + 'static, +{ + fn with(self, routes: T) -> Self where - F: FnOnce(resource::Resource) -> resource::Resource, + T: HasRoutes, { - f(resource::Resource { - name: name.to_owned(), - router: self, - }) - .router + self.merge(routes.routes()) + } +} + +/// Trait for things that can provide routes. +/// +/// Used with [`RouterExt::with`]. +pub trait HasRoutes { + /// Get the routes. + fn routes(self) -> Router; +} + +impl HasRoutes for Router { + fn routes(self) -> Router { + self } } diff --git a/axum-extra/src/routing/resource.rs b/axum-extra/src/routing/resource.rs index 636cc0db..046da840 100644 --- a/axum-extra/src/routing/resource.rs +++ b/axum-extra/src/routing/resource.rs @@ -1,3 +1,4 @@ +use super::HasRoutes; use axum::{ body::{Body, BoxBody}, handler::Handler, @@ -14,35 +15,36 @@ use tower_service::Service; /// /// ```rust /// use axum::{Router, routing::get, extract::Path}; -/// use axum_extra::routing::RouterExt; +/// use axum_extra::routing::{RouterExt, Resource}; /// -/// let app = Router::new().resource("users", |r| { +/// let users = Resource::named("users") /// // Define a route for `GET /users` -/// r.index(|| async {}) -/// // `POST /users` -/// .create(|| async {}) -/// // `GET /users/new` -/// .new(|| async {}) -/// // `GET /users/:users_id` -/// .show(|Path(user_id): Path| async {}) -/// // `GET /users/:users_id/edit` -/// .edit(|Path(user_id): Path| async {}) -/// // `PUT or PATCH /users/:users_id` -/// .update(|Path(user_id): Path| async {}) -/// // `DELETE /users/:users_id` -/// .destroy(|Path(user_id): Path| async {}) -/// // Nest another router at the "member level" -/// // This defines a route for `GET /users/:users_id/tweets` -/// .nest(Router::new().route( -/// "/tweets", -/// get(|Path(user_id): Path| async {}), -/// )) -/// // Nest another router at the "collection level" -/// // This defines a route for `GET /users/featured` -/// .nest_collection( -/// Router::new().route("/featured", get(|| async {})), -/// ) -/// }); +/// .index(|| async {}) +/// // `POST /users` +/// .create(|| async {}) +/// // `GET /users/new` +/// .new(|| async {}) +/// // `GET /users/:users_id` +/// .show(|Path(user_id): Path| async {}) +/// // `GET /users/:users_id/edit` +/// .edit(|Path(user_id): Path| async {}) +/// // `PUT or PATCH /users/:users_id` +/// .update(|Path(user_id): Path| async {}) +/// // `DELETE /users/:users_id` +/// .destroy(|Path(user_id): Path| async {}) +/// // Nest another router at the "member level" +/// // This defines a route for `GET /users/:users_id/tweets` +/// .nest(Router::new().route( +/// "/tweets", +/// get(|Path(user_id): Path| async {}), +/// )) +/// // Nest another router at the "collection level" +/// // This defines a route for `GET /users/featured` +/// .nest_collection( +/// Router::new().route("/featured", get(|| async {})), +/// ); +/// +/// let app = Router::new().with(users); /// # let _: Router = app; /// ``` #[derive(Debug)] @@ -52,27 +54,17 @@ pub struct Resource { } impl Resource { - fn index_create_path(&self) -> String { - format!("/{}", self.name) + /// Create a `Resource` with the given name. + /// + /// All routes will be nested at `/{resource_name}`. + pub fn named(resource_name: &str) -> Self { + Self { + name: resource_name.to_string(), + router: Default::default(), + } } - fn show_update_destroy_path(&self) -> String { - format!("/{0}/:{0}_id", self.name) - } - - fn route(mut self, path: &str, svc: T) -> Self - where - T: Service, Response = Response, Error = Infallible> - + Clone - + Send - + 'static, - T::Future: Send + 'static, - { - self.router = self.router.route(path, svc); - self - } - - /// Add a handler at `GET /resource_name`. + /// Add a handler at `GET /{resource_name}`. pub fn index(self, handler: H) -> Self where H: Handler, @@ -173,6 +165,32 @@ impl Resource { self.router = self.router.nest(&path, svc); self } + + fn index_create_path(&self) -> String { + format!("/{}", self.name) + } + + fn show_update_destroy_path(&self) -> String { + format!("/{0}/:{0}_id", self.name) + } + + fn route(mut self, path: &str, svc: T) -> Self + where + T: Service, Response = Response, Error = Infallible> + + Clone + + Send + + 'static, + T::Future: Send + 'static, + { + self.router = self.router.route(path, svc); + self + } +} + +impl HasRoutes for Resource { + fn routes(self) -> Router { + self.router + } } #[cfg(test)] @@ -185,22 +203,23 @@ mod tests { #[tokio::test] async fn works() { - let mut app = Router::new().resource("users", |r| { - r.index(|| async { "users#index" }) - .create(|| async { "users#create" }) - .new(|| async { "users#new" }) - .show(|Path(id): Path| async move { format!("users#show id={}", id) }) - .edit(|Path(id): Path| async move { format!("users#edit id={}", id) }) - .update(|Path(id): Path| async move { format!("users#update id={}", id) }) - .destroy(|Path(id): Path| async move { format!("users#destroy id={}", id) }) - .nest(Router::new().route( - "/tweets", - get(|Path(id): Path| async move { format!("users#tweets id={}", id) }), - )) - .nest_collection( - Router::new().route("/featured", get(|| async move { "users#featured" })), - ) - }); + let users = Resource::named("users") + .index(|| async { "users#index" }) + .create(|| async { "users#create" }) + .new(|| async { "users#new" }) + .show(|Path(id): Path| async move { format!("users#show id={}", id) }) + .edit(|Path(id): Path| async move { format!("users#edit id={}", id) }) + .update(|Path(id): Path| async move { format!("users#update id={}", id) }) + .destroy(|Path(id): Path| async move { format!("users#destroy id={}", id) }) + .nest(Router::new().route( + "/tweets", + get(|Path(id): Path| async move { format!("users#tweets id={}", id) }), + )) + .nest_collection( + Router::new().route("/featured", get(|| async move { "users#featured" })), + ); + + let mut app = Router::new().with(users); assert_eq!( call_route(&mut app, Method::GET, "/users").await,