//! 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. //! - `PATCH /todos/:id`: update a specific Todo. //! - `DELETE /todos/:id`: delete a specific Todo. //! //! Run with //! //! ```not_rust //! cargo run -p example-todos //! ``` use axum::{ error_handling::HandleErrorLayer, extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::{get, patch}, Json, Router, }; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, sync::{Arc, RwLock}, time::Duration, }; use tower::{BoxError, ServiceBuilder}; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use uuid::Uuid; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { format!("{}=debug,tower_http=debug", env!("CARGO_CRATE_NAME")).into() }), ) .with(tracing_subscriber::fmt::layer()) .init(); let db = Db::default(); // Compose the routes let app = Router::new() .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() .layer(HandleErrorLayer::new(|error: BoxError| async move { if error.is::() { Ok(StatusCode::REQUEST_TIMEOUT) } else { Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled internal error: {error}"), )) } })) .timeout(Duration::from_secs(10)) .layer(TraceLayer::new_for_http()) .into_inner(), ) .with_state(db); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).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>, State(db): State, ) -> impl IntoResponse { let todos = db.read().unwrap(); let Query(pagination) = pagination.unwrap_or_default(); let todos = todos .values() .skip(pagination.offset.unwrap_or(0)) .take(pagination.limit.unwrap_or(usize::MAX)) .cloned() .collect::>(); Json(todos) } #[derive(Debug, Deserialize)] struct CreateTodo { text: String, } async fn todos_create(State(db): State, Json(input): Json) -> impl IntoResponse { let todo = Todo { id: Uuid::new_v4(), text: input.text, completed: false, }; db.write().unwrap().insert(todo.id, todo.clone()); (StatusCode::CREATED, Json(todo)) } #[derive(Debug, Deserialize)] struct UpdateTodo { text: Option, completed: Option, } async fn todos_update( Path(id): Path, State(db): State, Json(input): Json, ) -> 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(Json(todo)) } async fn todos_delete(Path(id): Path, State(db): State) -> 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, }