//! Provides a RESTful web server managing some Todos. //! //! API will be: //! //! - `GET /todos`: return a JSON list of Todos. //! - `POST /todos`: create a new Todo. //! - `PUT /todos/:id`: update a specific Todo. //! - `DELETE /todos/:id`: delete a specific Todo. //! //! Run with //! //! ```not_rust //! cargo run --example todos //! ``` use axum::{ extract::{Extension, Json, Path, Query}, prelude::*, response::IntoResponse, }; use http::StatusCode; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, convert::Infallible, net::SocketAddr, sync::{Arc, RwLock}, time::Duration, }; use tower::{BoxError, ServiceBuilder}; use tower_http::{add_extension::AddExtensionLayer, trace::TraceLayer}; use uuid::Uuid; #[tokio::main] async fn main() { // Set the RUST_LOG, if it hasn't been explicitly defined if std::env::var("RUST_LOG").is_err() { std::env::set_var("RUST_LOG", "todos=debug,tower_http=debug") } tracing_subscriber::fmt::init(); let db = Db::default(); // Compose the routes let app = route("/todos", get(todos_index).post(todos_create)) .route("/todos/:id", patch(todos_update).delete(todos_delete)) // Add middleware to all routes .layer( ServiceBuilder::new() .timeout(Duration::from_secs(10)) .layer(TraceLayer::new_for_http()) .layer(AddExtensionLayer::new(db)) .into_inner(), ) // If the timeout fails, map the error to a response .handle_error(|error: BoxError| { let result = if error.is::() { Ok(StatusCode::REQUEST_TIMEOUT) } else { Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled internal error: {}", error), )) }; Ok::<_, Infallible>(result) }) // Make sure all errors have been handled .check_infallible(); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); tracing::debug!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } // The query parameters for todos index #[derive(Debug, Deserialize, Default)] pub struct Pagination { pub offset: Option, pub limit: Option, } async fn todos_index( pagination: Option>, Extension(db): Extension, ) -> impl IntoResponse { let todos = db.read().unwrap(); let Query(pagination) = pagination.unwrap_or_default(); let todos = todos .values() .cloned() .skip(pagination.offset.unwrap_or(0)) .take(pagination.limit.unwrap_or(std::usize::MAX)) .collect::>(); response::Json(todos) } #[derive(Debug, Deserialize)] struct CreateTodo { text: String, } async fn todos_create( Json(input): Json, Extension(db): Extension, ) -> impl IntoResponse { let todo = Todo { id: Uuid::new_v4(), text: input.text, completed: false, }; db.write().unwrap().insert(todo.id, todo.clone()); (StatusCode::CREATED, response::Json(todo)) } #[derive(Debug, Deserialize)] struct UpdateTodo { text: Option, completed: Option, } async fn todos_update( Path(id): Path, Json(input): Json, Extension(db): Extension, ) -> Result { let mut todo = db .read() .unwrap() .get(&id) .cloned() .ok_or(StatusCode::NOT_FOUND)?; if let Some(text) = input.text { todo.text = text; } if let Some(completed) = input.completed { todo.completed = completed; } db.write().unwrap().insert(todo.id, todo.clone()); Ok(response::Json(todo)) } async fn todos_delete(Path(id): Path, Extension(db): Extension) -> impl IntoResponse { if db.write().unwrap().remove(&id).is_some() { StatusCode::NO_CONTENT } else { StatusCode::NOT_FOUND } } type Db = Arc>>; #[derive(Debug, Serialize, Clone)] struct Todo { id: Uuid, text: String, completed: bool, }