use axum::{ body::Body, handler::Handler, http::Request, response::Response, routing::{delete, get, on, post, MethodFilter, MethodRouter, MissingState, WithState}, Router, }; use std::{convert::Infallible, fmt}; use tower_service::Service; /// A resource which defines a set of conventional CRUD routes. /// /// # Example /// /// ```rust /// use axum::{Router, routing::get, extract::Path}; /// use axum_extra::routing::{RouterExt, Resource}; /// /// let users = Resource::named("users") /// // Define a route for `GET /users` /// .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().merge(users); /// # let _: Router = app; /// ``` pub struct Resource { pub(crate) name: String, pub(crate) router: Router, } impl fmt::Debug for Resource where S: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Resource") .field("name", &self.name) .field("router", &self.router) .finish() } } impl Resource where B: axum::body::HttpBody + Send + 'static, { /// 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_owned(), router: Default::default(), } } } impl Resource where B: axum::body::HttpBody + Send + 'static, S: Clone + Send + Sync + 'static, R: 'static, { /// Add a handler at `GET /{resource_name}`. pub fn index(self, handler: H) -> Self where H: Handler, T: 'static, { let path = self.index_create_path(); self.route(&path, get(handler)) } /// Add a handler at `POST /{resource_name}`. pub fn create(self, handler: H) -> Self where H: Handler, T: 'static, { let path = self.index_create_path(); self.route(&path, post(handler)) } /// Add a handler at `GET /{resource_name}/new`. pub fn new(self, handler: H) -> Self where H: Handler, T: 'static, { let path = format!("/{}/new", self.name); self.route(&path, get(handler)) } /// Add a handler at `GET /{resource_name}/:{resource_name}_id`. pub fn show(self, handler: H) -> Self where H: Handler, T: 'static, { let path = self.show_update_destroy_path(); self.route(&path, get(handler)) } /// Add a handler at `GET /{resource_name}/:{resource_name}_id/edit`. pub fn edit(self, handler: H) -> Self where H: Handler, T: 'static, { let path = format!("/{0}/:{0}_id/edit", self.name); self.route(&path, get(handler)) } /// Add a handler at `PUT or PATCH /resource_name/:{resource_name}_id`. pub fn update(self, handler: H) -> Self where H: Handler, T: 'static, { let path = self.show_update_destroy_path(); self.route(&path, on(MethodFilter::PUT | MethodFilter::PATCH, handler)) } /// Add a handler at `DELETE /{resource_name}/:{resource_name}_id`. pub fn destroy(self, handler: H) -> Self where H: Handler, T: 'static, { let path = self.show_update_destroy_path(); self.route(&path, delete(handler)) } /// Nest another router at the "member level". /// /// The routes will be nested at `/{resource_name}/:{resource_name}_id`. pub fn nest(mut self, router: Router) -> Self { let path = self.show_update_destroy_path(); self.router = self.router.nest(&path, router); self } /// Nest another router at the "collection level". /// /// The routes will be nested at `/{resource_name}`. pub fn nest_collection(mut self, router: Router) -> Self { let path = self.index_create_path(); self.router = self.router.nest(&path, router); 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, method_router: MethodRouter, ) -> Self { self.router = self.router.route(path, method_router); self } } impl From> for Router { fn from(resource: Resource) -> Self { resource.router } } #[cfg(test)] mod tests { #[allow(unused_imports)] use super::*; use axum::{extract::Path, http::Method, Router}; use tower::ServiceExt; #[tokio::test] async fn works() { 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::without_state().merge(users); assert_eq!( call_route(&mut app, Method::GET, "/users").await, "users#index" ); assert_eq!( call_route(&mut app, Method::POST, "/users").await, "users#create" ); assert_eq!( call_route(&mut app, Method::GET, "/users/new").await, "users#new" ); assert_eq!( call_route(&mut app, Method::GET, "/users/1").await, "users#show id=1" ); assert_eq!( call_route(&mut app, Method::GET, "/users/1/edit").await, "users#edit id=1" ); assert_eq!( call_route(&mut app, Method::PATCH, "/users/1").await, "users#update id=1" ); assert_eq!( call_route(&mut app, Method::PUT, "/users/1").await, "users#update id=1" ); assert_eq!( call_route(&mut app, Method::DELETE, "/users/1").await, "users#destroy id=1" ); assert_eq!( call_route(&mut app, Method::GET, "/users/1/tweets").await, "users#tweets id=1" ); assert_eq!( call_route(&mut app, Method::GET, "/users/featured").await, "users#featured" ); } async fn call_route(app: &mut Router<(), WithState>, method: Method, uri: &str) -> String { let res = app .ready() .await .unwrap() .call( Request::builder() .method(method) .uri(uri) .body(Body::empty()) .unwrap(), ) .await .unwrap(); let bytes = hyper::body::to_bytes(res).await.unwrap(); String::from_utf8(bytes.to_vec()).unwrap() } }