Source code

Revision control

Copy as Markdown

Other Tools

#![deny(warnings)]
use std::env;
use warp::Filter;
/// 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.
#[tokio::main]
async fn main() {
if env::var_os("RUST_LOG").is_none() {
// Set `RUST_LOG=todos=debug` to see debug logs,
// this only shows access logs.
env::set_var("RUST_LOG", "todos=info");
}
pretty_env_logger::init();
let db = models::blank_db();
let api = filters::todos(db);
// View access logs by setting `RUST_LOG=todos`.
let routes = api.with(warp::log("todos"));
// Start up the server...
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}
mod filters {
use super::handlers;
use super::models::{Db, ListOptions, Todo};
use warp::Filter;
/// The 4 TODOs filters combined.
pub fn todos(
db: Db,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
todos_list(db.clone())
.or(todos_create(db.clone()))
.or(todos_update(db.clone()))
.or(todos_delete(db))
}
/// GET /todos?offset=3&limit=5
pub fn todos_list(
db: Db,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("todos")
.and(warp::get())
.and(warp::query::<ListOptions>())
.and(with_db(db))
.and_then(handlers::list_todos)
}
/// POST /todos with JSON body
pub fn todos_create(
db: Db,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("todos")
.and(warp::post())
.and(json_body())
.and(with_db(db))
.and_then(handlers::create_todo)
}
/// PUT /todos/:id with JSON body
pub fn todos_update(
db: Db,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("todos" / u64)
.and(warp::put())
.and(json_body())
.and(with_db(db))
.and_then(handlers::update_todo)
}
/// DELETE /todos/:id
pub fn todos_delete(
db: Db,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
// We'll make one of our endpoints admin-only to show how authentication filters are used
let admin_only = warp::header::exact("authorization", "Bearer admin");
warp::path!("todos" / u64)
// It is important to put the auth check _after_ the path filters.
// If we put the auth check before, the request `PUT /todos/invalid-string`
// would try this filter and reject because the authorization header doesn't match,
// rather because the param is wrong for that other path.
.and(admin_only)
.and(warp::delete())
.and(with_db(db))
.and_then(handlers::delete_todo)
}
fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || db.clone())
}
fn json_body() -> impl Filter<Extract = (Todo,), Error = warp::Rejection> + Clone {
// When accepting a body, we want a JSON body
// (and to reject huge payloads)...
warp::body::content_length_limit(1024 * 16).and(warp::body::json())
}
}
/// These are our API handlers, the ends of each filter chain.
/// Notice how thanks to using `Filter::and`, we can define a function
/// with the exact arguments we'd expect from each filter in the chain.
/// No tuples are needed, it's auto flattened for the functions.
mod handlers {
use super::models::{Db, ListOptions, Todo};
use std::convert::Infallible;
use warp::http::StatusCode;
pub async fn list_todos(opts: ListOptions, db: Db) -> Result<impl warp::Reply, Infallible> {
// Just return a JSON array of todos, applying the limit and offset.
let todos = db.lock().await;
let todos: Vec<Todo> = todos
.clone()
.into_iter()
.skip(opts.offset.unwrap_or(0))
.take(opts.limit.unwrap_or(std::usize::MAX))
.collect();
Ok(warp::reply::json(&todos))
}
pub async fn create_todo(create: Todo, db: Db) -> Result<impl warp::Reply, Infallible> {
log::debug!("create_todo: {:?}", create);
let mut vec = db.lock().await;
for todo in vec.iter() {
if todo.id == create.id {
log::debug!(" -> id already exists: {}", create.id);
// Todo with id already exists, return `400 BadRequest`.
return Ok(StatusCode::BAD_REQUEST);
}
}
// No existing Todo with id, so insert and return `201 Created`.
vec.push(create);
Ok(StatusCode::CREATED)
}
pub async fn update_todo(
id: u64,
update: Todo,
db: Db,
) -> Result<impl warp::Reply, Infallible> {
log::debug!("update_todo: id={}, todo={:?}", id, update);
let mut vec = db.lock().await;
// Look for the specified Todo...
for todo in vec.iter_mut() {
if todo.id == id {
*todo = update;
return Ok(StatusCode::OK);
}
}
log::debug!(" -> todo id not found!");
// If the for loop didn't return OK, then the ID doesn't exist...
Ok(StatusCode::NOT_FOUND)
}
pub async fn delete_todo(id: u64, db: Db) -> Result<impl warp::Reply, Infallible> {
log::debug!("delete_todo: id={}", id);
let mut vec = db.lock().await;
let len = vec.len();
vec.retain(|todo| {
// Retain all Todos that aren't this id...
// In other words, remove all that *are* this id...
todo.id != id
});
// If the vec is smaller, we found and deleted a Todo!
let deleted = vec.len() != len;
if deleted {
// respond with a `204 No Content`, which means successful,
// yet no body expected...
Ok(StatusCode::NO_CONTENT)
} else {
log::debug!(" -> todo id not found!");
Ok(StatusCode::NOT_FOUND)
}
}
}
mod models {
use serde_derive::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
/// So we don't have to tackle how different database work, we'll just use
/// a simple in-memory DB, a vector synchronized by a mutex.
pub type Db = Arc<Mutex<Vec<Todo>>>;
pub fn blank_db() -> Db {
Arc::new(Mutex::new(Vec::new()))
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Todo {
pub id: u64,
pub text: String,
pub completed: bool,
}
// The query parameters for list_todos.
#[derive(Debug, Deserialize)]
pub struct ListOptions {
pub offset: Option<usize>,
pub limit: Option<usize>,
}
}
#[cfg(test)]
mod tests {
use warp::http::StatusCode;
use warp::test::request;
use super::{
filters,
models::{self, Todo},
};
#[tokio::test]
async fn test_post() {
let db = models::blank_db();
let api = filters::todos(db);
let resp = request()
.method("POST")
.path("/todos")
.json(&todo1())
.reply(&api)
.await;
assert_eq!(resp.status(), StatusCode::CREATED);
}
#[tokio::test]
async fn test_post_conflict() {
let db = models::blank_db();
db.lock().await.push(todo1());
let api = filters::todos(db);
let resp = request()
.method("POST")
.path("/todos")
.json(&todo1())
.reply(&api)
.await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn test_put_unknown() {
let _ = pretty_env_logger::try_init();
let db = models::blank_db();
let api = filters::todos(db);
let resp = request()
.method("PUT")
.path("/todos/1")
.header("authorization", "Bearer admin")
.json(&todo1())
.reply(&api)
.await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
fn todo1() -> Todo {
Todo {
id: 1,
text: "test 1".into(),
completed: false,
}
}
}