-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8faed81
commit 6705e79
Showing
2 changed files
with
165 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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::<tower::timeout::error::Elapsed>() { | ||
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<usize>, | ||
pub limit: Option<usize>, | ||
} | ||
|
||
async fn todos_index( | ||
pagination: Option<Query<Pagination>>, | ||
Extension(db): Extension<Db>, | ||
) -> 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::<Vec<_>>(); | ||
|
||
response::Json(todos) | ||
} | ||
|
||
#[derive(Debug, Deserialize)] | ||
struct CreateTodo { | ||
text: String, | ||
} | ||
|
||
async fn todos_create( | ||
Json(input): Json<CreateTodo>, | ||
Extension(db): Extension<Db>, | ||
) -> 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<String>, | ||
completed: Option<bool>, | ||
} | ||
|
||
async fn todos_update( | ||
UrlParams((id,)): UrlParams<(Uuid,)>, | ||
Json(input): Json<UpdateTodo>, | ||
Extension(db): Extension<Db>, | ||
) -> Result<impl IntoResponse, StatusCode> { | ||
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<Db>, | ||
) -> impl IntoResponse { | ||
if db.write().unwrap().remove(&id).is_some() { | ||
StatusCode::NO_CONTENT | ||
} else { | ||
StatusCode::NOT_FOUND | ||
} | ||
} | ||
|
||
type Db = Arc<RwLock<HashMap<Uuid, Todo>>>; | ||
|
||
#[derive(Debug, Serialize, Clone)] | ||
struct Todo { | ||
id: Uuid, | ||
text: String, | ||
completed: bool, | ||
} |