Basic todo example with lru cache and in-memory db
This commit is contained in:
commit
d4d604f779
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
tags
|
1473
Cargo.lock
generated
Normal file
1473
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "warp-caching"
|
||||
version = "0.1.0"
|
||||
authors = ["Tyler Hallada <tyler@hallada.net>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
http = "0.2"
|
||||
http-api-problem = { version = "0.17", features = ["with-warp"] }
|
||||
hyper = "0.13"
|
||||
lru = "0.5"
|
||||
tokio = { version = "0.2", features = ["macros", "rt-threaded", "sync"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.2"
|
||||
tracing-futures = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
warp = "0.2"
|
135
src/caches/cache.rs
Normal file
135
src/caches/cache.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use anyhow::Result;
|
||||
use lru::LruCache;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::debug;
|
||||
use warp::http::StatusCode;
|
||||
use warp::{reject, Rejection, Reply};
|
||||
|
||||
use crate::problem::{reject_anyhow, unpack_problem};
|
||||
|
||||
use super::CachedResponse;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cache<K, V>
|
||||
where
|
||||
K: Eq + Hash + Debug,
|
||||
V: Clone,
|
||||
{
|
||||
pub name: String,
|
||||
pub lru_mutex: Arc<Mutex<LruCache<K, V>>>,
|
||||
pub log_keys: bool,
|
||||
}
|
||||
|
||||
impl<K, V> Cache<K, V>
|
||||
where
|
||||
K: Eq + Hash + Debug,
|
||||
V: Clone,
|
||||
{
|
||||
pub fn new(name: &str, capacity: usize) -> Self {
|
||||
Cache {
|
||||
name: name.to_string(),
|
||||
lru_mutex: Arc::new(Mutex::new(LruCache::new(capacity))),
|
||||
log_keys: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_keys(mut self, value: bool) -> Self {
|
||||
self.log_keys = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_with_key(&self, key: &K, message: &str) {
|
||||
if self.log_keys {
|
||||
debug!(cache = %self.name, key = ?key, message);
|
||||
} else {
|
||||
debug!(cache = %self.name, message);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get<G, F>(&self, key: K, getter: G) -> Result<V>
|
||||
where
|
||||
G: Fn() -> F,
|
||||
F: Future<Output = Result<V>>,
|
||||
{
|
||||
let mut guard = self.lru_mutex.lock().await;
|
||||
if let Some(value) = guard.get(&key) {
|
||||
self.log_with_key(&key, "get: hit");
|
||||
return Ok(value.clone());
|
||||
}
|
||||
drop(guard);
|
||||
|
||||
self.log_with_key(&key, "get: miss");
|
||||
let value = getter().await?;
|
||||
let mut guard = self.lru_mutex.lock().await;
|
||||
guard.put(key, value.clone());
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, key: K) -> Result<Option<V>> {
|
||||
let mut guard = self.lru_mutex.lock().await;
|
||||
let value = guard.pop(&key);
|
||||
self.log_with_key(&key, "delete");
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn clear(&self) {
|
||||
let mut guard = self.lru_mutex.lock().await;
|
||||
guard.clear();
|
||||
debug!(cache = %self.name, "cache clear");
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Cache<K, CachedResponse>
|
||||
where
|
||||
K: Eq + Hash + Debug,
|
||||
{
|
||||
pub async fn get_response<G, F, R>(
|
||||
&self,
|
||||
key: K,
|
||||
getter: G,
|
||||
) -> Result<CachedResponse, Rejection>
|
||||
where
|
||||
G: Fn() -> F,
|
||||
F: Future<Output = Result<R>>,
|
||||
R: Reply,
|
||||
{
|
||||
let mut guard = self.lru_mutex.lock().await;
|
||||
if let Some(value) = guard.get(&key) {
|
||||
self.log_with_key(&key, "get_response: hit");
|
||||
return Ok(value.clone());
|
||||
}
|
||||
drop(guard);
|
||||
|
||||
self.log_with_key(&key, "get_response: miss");
|
||||
let reply = getter().await.map_err(reject_anyhow);
|
||||
let cached_response = match reply {
|
||||
Ok(reply) => CachedResponse::from_reply(reply)
|
||||
.await
|
||||
.map_err(reject_anyhow)?,
|
||||
Err(rejection) => {
|
||||
let reply = unpack_problem(rejection).await?;
|
||||
CachedResponse::from_reply(reply)
|
||||
.await
|
||||
.map_err(reject_anyhow)?
|
||||
}
|
||||
};
|
||||
let mut guard = self.lru_mutex.lock().await;
|
||||
guard.put(key, cached_response.clone());
|
||||
|
||||
Ok(cached_response)
|
||||
}
|
||||
|
||||
pub async fn delete_response(&self, key: K) -> Result<Option<CachedResponse>> {
|
||||
let mut guard = self.lru_mutex.lock().await;
|
||||
let cached_response = guard.pop(&key);
|
||||
self.log_with_key(&key, "delete_response");
|
||||
|
||||
Ok(cached_response)
|
||||
}
|
||||
}
|
46
src/caches/cached_response.rs
Normal file
46
src/caches/cached_response.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use anyhow::Result;
|
||||
use http::{HeaderMap, HeaderValue, Response, StatusCode, Version};
|
||||
use hyper::body::{to_bytes, Body, Bytes};
|
||||
use warp::Reply;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedResponse {
|
||||
pub status: StatusCode,
|
||||
pub version: Version,
|
||||
pub headers: HeaderMap<HeaderValue>,
|
||||
pub body: Bytes,
|
||||
}
|
||||
|
||||
impl CachedResponse {
|
||||
pub async fn from_reply<T>(reply: T) -> Result<Self>
|
||||
where
|
||||
T: Reply,
|
||||
{
|
||||
let mut response = reply.into_response();
|
||||
Ok(CachedResponse {
|
||||
status: response.status(),
|
||||
version: response.version(),
|
||||
headers: response.headers().clone(),
|
||||
body: to_bytes(response.body_mut()).await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Reply for CachedResponse {
|
||||
fn into_response(self) -> warp::reply::Response {
|
||||
match Response::builder()
|
||||
.status(self.status)
|
||||
.version(self.version)
|
||||
.body(Body::from(self.body))
|
||||
{
|
||||
Ok(mut response) => {
|
||||
let headers = response.headers_mut();
|
||||
for (header, value) in self.headers.iter() {
|
||||
headers.insert(header, value.clone());
|
||||
}
|
||||
response
|
||||
}
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
24
src/caches/mod.rs
Normal file
24
src/caches/mod.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::models::ListOptions;
|
||||
|
||||
mod cache;
|
||||
mod cached_response;
|
||||
|
||||
pub use cache::Cache;
|
||||
pub use cached_response::CachedResponse;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Caches {
|
||||
pub todos: Cache<u64, CachedResponse>,
|
||||
pub list_todos: Cache<ListOptions, CachedResponse>,
|
||||
}
|
||||
|
||||
impl Caches {
|
||||
pub fn initialize() -> Self {
|
||||
Caches {
|
||||
todos: Cache::new("todos", 100),
|
||||
list_todos: Cache::new("list_todos", 100),
|
||||
}
|
||||
}
|
||||
}
|
90
src/lru_cache/filters.rs
Normal file
90
src/lru_cache/filters.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use warp::Filter;
|
||||
|
||||
use super::handlers;
|
||||
use crate::models::{Environment, ListOptions, Todo};
|
||||
|
||||
/// The 4 TODOs filters combined.
|
||||
pub fn todos(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
|
||||
warp::path("lru_cache").and(
|
||||
todos_list(env.clone())
|
||||
.or(todos_create(env.clone()))
|
||||
.or(todos_update(env.clone()))
|
||||
.or(todos_get(env.clone()))
|
||||
.or(todos_delete(env)),
|
||||
)
|
||||
}
|
||||
|
||||
/// GET /todos?offset=3&limit=5
|
||||
pub fn todos_list(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path!("todos")
|
||||
.and(warp::get())
|
||||
.and(warp::query::<ListOptions>())
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::list_todos)
|
||||
}
|
||||
|
||||
/// GET /todos/:id
|
||||
pub fn todos_get(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path!("todos" / u64)
|
||||
.and(warp::get())
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::get_todo)
|
||||
}
|
||||
|
||||
/// POST /todos with JSON body
|
||||
pub fn todos_create(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path!("todos")
|
||||
.and(warp::post())
|
||||
.and(json_body())
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::create_todo)
|
||||
}
|
||||
|
||||
/// PUT /todos/:id with JSON body
|
||||
pub fn todos_update(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path!("todos" / u64)
|
||||
.and(warp::put())
|
||||
.and(json_body())
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::update_todo)
|
||||
}
|
||||
|
||||
/// DELETE /todos/:id
|
||||
pub fn todos_delete(
|
||||
env: Environment,
|
||||
) -> 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_env(env))
|
||||
.and_then(handlers::delete_todo)
|
||||
}
|
||||
|
||||
fn with_env(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = (Environment,), Error = std::convert::Infallible> + Clone {
|
||||
warp::any().map(move || env.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())
|
||||
}
|
137
src/lru_cache/handlers.rs
Normal file
137
src/lru_cache/handlers.rs
Normal file
@ -0,0 +1,137 @@
|
||||
/// 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.
|
||||
use anyhow::anyhow;
|
||||
use http_api_problem::HttpApiProblem;
|
||||
use std::convert::Infallible;
|
||||
use tracing::debug;
|
||||
use warp::http::StatusCode;
|
||||
|
||||
use crate::models::{Environment, ListOptions, Todo};
|
||||
use crate::problem::reject_anyhow;
|
||||
|
||||
pub async fn list_todos(
|
||||
opts: ListOptions,
|
||||
env: Environment,
|
||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
env.caches
|
||||
.list_todos
|
||||
.get_response(opts.clone(), || async {
|
||||
// Just return a JSON array of todos, applying the limit and offset.
|
||||
let todos = env.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))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_todo(id: u64, env: Environment) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
env.caches
|
||||
.todos
|
||||
.get_response(id, || async {
|
||||
debug!("get_todo: id={}", id);
|
||||
let mut vec = env.db.lock().await;
|
||||
|
||||
// Look for the specified Todo...
|
||||
for todo in vec.iter_mut() {
|
||||
if todo.id == id {
|
||||
return Ok(warp::reply::json(&todo));
|
||||
}
|
||||
}
|
||||
|
||||
debug!(" -> todo id not found!");
|
||||
|
||||
// If the for loop didn't return OK, then the ID doesn't exist...
|
||||
Err(anyhow!(HttpApiProblem::new(format!(
|
||||
"Todo {} not found",
|
||||
id
|
||||
))
|
||||
.set_status(StatusCode::NOT_FOUND)))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_todo(create: Todo, env: Environment) -> Result<impl warp::Reply, Infallible> {
|
||||
debug!("create_todo: {:?}", create);
|
||||
|
||||
let mut vec = env.db.lock().await;
|
||||
|
||||
for todo in vec.iter() {
|
||||
if todo.id == create.id {
|
||||
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);
|
||||
|
||||
env.caches.list_todos.clear().await;
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
pub async fn update_todo(
|
||||
id: u64,
|
||||
update: Todo,
|
||||
env: Environment,
|
||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
debug!("update_todo: id={}, todo={:?}", id, update);
|
||||
let mut vec = env.db.lock().await;
|
||||
|
||||
// Look for the specified Todo...
|
||||
for todo in vec.iter_mut() {
|
||||
if todo.id == id {
|
||||
*todo = update;
|
||||
env.caches
|
||||
.todos
|
||||
.delete_response(id)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
env.caches.list_todos.clear().await;
|
||||
return Ok(StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
||||
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, env: Environment) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
debug!("delete_todo: id={}", id);
|
||||
|
||||
let mut vec = env.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 {
|
||||
env.caches
|
||||
.todos
|
||||
.delete_response(id)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
env.caches.list_todos.clear().await;
|
||||
// respond with a `204 No Content`, which means successful,
|
||||
// yet no body expected...
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
debug!(" -> todo id not found!");
|
||||
Ok(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
2
src/lru_cache/mod.rs
Normal file
2
src/lru_cache/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod filters;
|
||||
pub mod handlers;
|
116
src/main.rs
Normal file
116
src/main.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use std::env;
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
use warp::Filter;
|
||||
|
||||
mod caches;
|
||||
mod lru_cache;
|
||||
mod models;
|
||||
mod no_cache;
|
||||
mod problem;
|
||||
|
||||
/// 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() {
|
||||
let env_log_filter =
|
||||
env::var("RUST_LOG").unwrap_or_else(|_| "warp=info,warp_caching=debug".to_owned());
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(env_log_filter)
|
||||
.with_span_events(FmtSpan::CLOSE)
|
||||
.init();
|
||||
|
||||
let env = models::Environment {
|
||||
db: models::blank_db(),
|
||||
caches: models::blank_caches(),
|
||||
};
|
||||
|
||||
let api = no_cache::filters::todos(env.clone()).or(lru_cache::filters::todos(env));
|
||||
|
||||
// View access logs by setting `RUST_LOG=warp-caching`.
|
||||
let routes = api.with(warp::log("warp_caching"));
|
||||
// Start up the server...
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use warp::http::StatusCode;
|
||||
use warp::test::request;
|
||||
|
||||
use crate::models::{self, Todo};
|
||||
use crate::no_cache::filters;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post() {
|
||||
let env = models::Environment {
|
||||
db: models::blank_db(),
|
||||
caches: models::blank_caches(),
|
||||
};
|
||||
let api = filters::todos(env);
|
||||
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/todos")
|
||||
.json(&Todo {
|
||||
id: 1,
|
||||
text: "test 1".into(),
|
||||
completed: false,
|
||||
})
|
||||
.reply(&api)
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_conflict() {
|
||||
let env = models::Environment {
|
||||
db: models::blank_db(),
|
||||
caches: models::blank_caches(),
|
||||
};
|
||||
env.db.lock().await.push(todo1());
|
||||
let api = filters::todos(env);
|
||||
|
||||
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 env = models::Environment {
|
||||
db: models::blank_db(),
|
||||
caches: models::blank_caches(),
|
||||
};
|
||||
let api = filters::todos(env);
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
37
src/models.rs
Normal file
37
src/models.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::caches::Caches;
|
||||
|
||||
/// 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()))
|
||||
}
|
||||
|
||||
pub fn blank_caches() -> Caches {
|
||||
Caches::initialize()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Environment {
|
||||
pub db: Db,
|
||||
pub caches: Caches,
|
||||
}
|
||||
|
||||
#[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, Clone, Hash, Eq, PartialEq)]
|
||||
pub struct ListOptions {
|
||||
pub offset: Option<usize>,
|
||||
pub limit: Option<usize>,
|
||||
}
|
90
src/no_cache/filters.rs
Normal file
90
src/no_cache/filters.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use warp::Filter;
|
||||
|
||||
use super::handlers;
|
||||
use crate::models::{Environment, ListOptions, Todo};
|
||||
|
||||
/// The 4 TODOs filters combined.
|
||||
pub fn todos(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
|
||||
warp::path("no_cache").and(
|
||||
todos_list(env.clone())
|
||||
.or(todos_create(env.clone()))
|
||||
.or(todos_update(env.clone()))
|
||||
.or(todos_get(env.clone()))
|
||||
.or(todos_delete(env)),
|
||||
)
|
||||
}
|
||||
|
||||
/// GET /todos?offset=3&limit=5
|
||||
pub fn todos_list(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path!("todos")
|
||||
.and(warp::get())
|
||||
.and(warp::query::<ListOptions>())
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::list_todos)
|
||||
}
|
||||
|
||||
/// GET /todos/:id
|
||||
pub fn todos_get(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path!("todos" / u64)
|
||||
.and(warp::get())
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::get_todo)
|
||||
}
|
||||
|
||||
/// POST /todos with JSON body
|
||||
pub fn todos_create(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path!("todos")
|
||||
.and(warp::post())
|
||||
.and(json_body())
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::create_todo)
|
||||
}
|
||||
|
||||
/// PUT /todos/:id with JSON body
|
||||
pub fn todos_update(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||
warp::path!("todos" / u64)
|
||||
.and(warp::put())
|
||||
.and(json_body())
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::update_todo)
|
||||
}
|
||||
|
||||
/// DELETE /todos/:id
|
||||
pub fn todos_delete(
|
||||
env: Environment,
|
||||
) -> 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_env(env))
|
||||
.and_then(handlers::delete_todo)
|
||||
}
|
||||
|
||||
fn with_env(
|
||||
env: Environment,
|
||||
) -> impl Filter<Extract = (Environment,), Error = std::convert::Infallible> + Clone {
|
||||
warp::any().map(move || env.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())
|
||||
}
|
107
src/no_cache/handlers.rs
Normal file
107
src/no_cache/handlers.rs
Normal file
@ -0,0 +1,107 @@
|
||||
/// 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.
|
||||
use std::convert::Infallible;
|
||||
use tracing::debug;
|
||||
use warp::http::StatusCode;
|
||||
|
||||
use crate::models::{Environment, ListOptions, Todo};
|
||||
|
||||
pub async fn list_todos(
|
||||
opts: ListOptions,
|
||||
env: Environment,
|
||||
) -> Result<impl warp::Reply, Infallible> {
|
||||
// Just return a JSON array of todos, applying the limit and offset.
|
||||
let todos = env.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 get_todo(id: u64, env: Environment) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
debug!("get_todo: id={}", id);
|
||||
let mut vec = env.db.lock().await;
|
||||
|
||||
// Look for the specified Todo...
|
||||
for todo in vec.iter_mut() {
|
||||
if todo.id == id {
|
||||
return Ok(warp::reply::json(&todo));
|
||||
}
|
||||
}
|
||||
|
||||
debug!(" -> todo id not found!");
|
||||
|
||||
// If the for loop didn't return OK, then the ID doesn't exist...
|
||||
Err(warp::reject::not_found())
|
||||
}
|
||||
|
||||
pub async fn create_todo(create: Todo, env: Environment) -> Result<impl warp::Reply, Infallible> {
|
||||
debug!("create_todo: {:?}", create);
|
||||
|
||||
let mut vec = env.db.lock().await;
|
||||
|
||||
for todo in vec.iter() {
|
||||
if todo.id == create.id {
|
||||
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,
|
||||
env: Environment,
|
||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
debug!("update_todo: id={}, todo={:?}", id, update);
|
||||
let mut vec = env.db.lock().await;
|
||||
|
||||
// Look for the specified Todo...
|
||||
for todo in vec.iter_mut() {
|
||||
if todo.id == id {
|
||||
*todo = update;
|
||||
return Ok(StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
||||
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, env: Environment) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
debug!("delete_todo: id={}", id);
|
||||
|
||||
let mut vec = env.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 {
|
||||
debug!(" -> todo id not found!");
|
||||
Ok(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
2
src/no_cache/mod.rs
Normal file
2
src/no_cache/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod filters;
|
||||
pub mod handlers;
|
36
src/problem/mod.rs
Normal file
36
src/problem/mod.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use http::StatusCode;
|
||||
use http_api_problem::HttpApiProblem;
|
||||
use tracing::error;
|
||||
use warp::{reject, Rejection, Reply};
|
||||
|
||||
pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem {
|
||||
let error = match error.downcast::<HttpApiProblem>() {
|
||||
Ok(problem) => return problem,
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
error!("Recovering unhandled error: {:?}", error);
|
||||
HttpApiProblem::new("Internal Server Error: 500").set_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
pub async fn unpack_problem(rejection: Rejection) -> Result<impl Reply, Rejection> {
|
||||
if let Some(problem) = rejection.find::<HttpApiProblem>() {
|
||||
let code = problem.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
|
||||
let reply = warp::reply::json(problem);
|
||||
let reply = warp::reply::with_status(reply, code);
|
||||
let reply = warp::reply::with_header(
|
||||
reply,
|
||||
warp::http::header::CONTENT_TYPE,
|
||||
http_api_problem::PROBLEM_JSON_MEDIA_TYPE,
|
||||
);
|
||||
|
||||
return Ok(reply);
|
||||
}
|
||||
|
||||
Err(rejection)
|
||||
}
|
||||
|
||||
pub fn reject_anyhow(error: anyhow::Error) -> Rejection {
|
||||
reject::custom(from_anyhow(error))
|
||||
}
|
Loading…
Reference in New Issue
Block a user