diff --git a/Cargo.toml b/Cargo.toml index e60c19fc..5f756002 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ tokio = { version = "1.6.1", features = ["macros", "rt", "rt-multi-thread"] } tokio-postgres = "0.7.2" tracing = "0.1" tracing-subscriber = "0.2" -uuid = { version = "0.8", features = ["serde"] } +uuid = { version = "0.8", features = ["serde", "v4"] } [dev-dependencies.tower] version = "0.4" diff --git a/examples/todos.rs b/examples/todos.rs new file mode 100644 index 00000000..ac74f174 --- /dev/null +++ b/examples/todos.rs @@ -0,0 +1,164 @@ +//! 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. + +use axum::{ + extract::{Extension, Json, Query, UrlParams}, + prelude::*, + response::IntoResponse, + service::ServiceExt, +}; +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() { + 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); + hyper::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( + UrlParams((id,)): UrlParams<(Uuid,)>, + 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( + UrlParams((id,)): UrlParams<(Uuid,)>, + 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, +}