1 #![deny(warnings)]
2 
3 use std::env;
4 use warp::Filter;
5 
6 /// Provides a RESTful web server managing some Todos.
7 ///
8 /// API will be:
9 ///
10 /// - `GET /todos`: return a JSON list of Todos.
11 /// - `POST /todos`: create a new Todo.
12 /// - `PUT /todos/:id`: update a specific Todo.
13 /// - `DELETE /todos/:id`: delete a specific Todo.
14 #[tokio::main]
main()15 async fn main() {
16     if env::var_os("RUST_LOG").is_none() {
17         // Set `RUST_LOG=todos=debug` to see debug logs,
18         // this only shows access logs.
19         env::set_var("RUST_LOG", "todos=info");
20     }
21     pretty_env_logger::init();
22 
23     let db = models::blank_db();
24 
25     let api = filters::todos(db);
26 
27     // View access logs by setting `RUST_LOG=todos`.
28     let routes = api.with(warp::log("todos"));
29     // Start up the server...
30     warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
31 }
32 
33 mod filters {
34     use super::handlers;
35     use super::models::{Db, ListOptions, Todo};
36     use warp::Filter;
37 
38     /// The 4 TODOs filters combined.
todos( db: Db, ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone39     pub fn todos(
40         db: Db,
41     ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
42         todos_list(db.clone())
43             .or(todos_create(db.clone()))
44             .or(todos_update(db.clone()))
45             .or(todos_delete(db))
46     }
47 
48     /// GET /todos?offset=3&limit=5
todos_list( db: Db, ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone49     pub fn todos_list(
50         db: Db,
51     ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
52         warp::path!("todos")
53             .and(warp::get())
54             .and(warp::query::<ListOptions>())
55             .and(with_db(db))
56             .and_then(handlers::list_todos)
57     }
58 
59     /// POST /todos with JSON body
todos_create( db: Db, ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone60     pub fn todos_create(
61         db: Db,
62     ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
63         warp::path!("todos")
64             .and(warp::post())
65             .and(json_body())
66             .and(with_db(db))
67             .and_then(handlers::create_todo)
68     }
69 
70     /// PUT /todos/:id with JSON body
todos_update( db: Db, ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone71     pub fn todos_update(
72         db: Db,
73     ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
74         warp::path!("todos" / u64)
75             .and(warp::put())
76             .and(json_body())
77             .and(with_db(db))
78             .and_then(handlers::update_todo)
79     }
80 
81     /// DELETE /todos/:id
todos_delete( db: Db, ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone82     pub fn todos_delete(
83         db: Db,
84     ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
85         // We'll make one of our endpoints admin-only to show how authentication filters are used
86         let admin_only = warp::header::exact("authorization", "Bearer admin");
87 
88         warp::path!("todos" / u64)
89             // It is important to put the auth check _after_ the path filters.
90             // If we put the auth check before, the request `PUT /todos/invalid-string`
91             // would try this filter and reject because the authorization header doesn't match,
92             // rather because the param is wrong for that other path.
93             .and(admin_only)
94             .and(warp::delete())
95             .and(with_db(db))
96             .and_then(handlers::delete_todo)
97     }
98 
with_db(db: Db) -> impl Filter<Extract = (Db,), Error = std::convert::Infallible> + Clone99     fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = std::convert::Infallible> + Clone {
100         warp::any().map(move || db.clone())
101     }
102 
json_body() -> impl Filter<Extract = (Todo,), Error = warp::Rejection> + Clone103     fn json_body() -> impl Filter<Extract = (Todo,), Error = warp::Rejection> + Clone {
104         // When accepting a body, we want a JSON body
105         // (and to reject huge payloads)...
106         warp::body::content_length_limit(1024 * 16).and(warp::body::json())
107     }
108 }
109 
110 /// These are our API handlers, the ends of each filter chain.
111 /// Notice how thanks to using `Filter::and`, we can define a function
112 /// with the exact arguments we'd expect from each filter in the chain.
113 /// No tuples are needed, it's auto flattened for the functions.
114 mod handlers {
115     use super::models::{Db, ListOptions, Todo};
116     use std::convert::Infallible;
117     use warp::http::StatusCode;
118 
list_todos(opts: ListOptions, db: Db) -> Result<impl warp::Reply, Infallible>119     pub async fn list_todos(opts: ListOptions, db: Db) -> Result<impl warp::Reply, Infallible> {
120         // Just return a JSON array of todos, applying the limit and offset.
121         let todos = db.lock().await;
122         let todos: Vec<Todo> = todos
123             .clone()
124             .into_iter()
125             .skip(opts.offset.unwrap_or(0))
126             .take(opts.limit.unwrap_or(std::usize::MAX))
127             .collect();
128         Ok(warp::reply::json(&todos))
129     }
130 
create_todo(create: Todo, db: Db) -> Result<impl warp::Reply, Infallible>131     pub async fn create_todo(create: Todo, db: Db) -> Result<impl warp::Reply, Infallible> {
132         log::debug!("create_todo: {:?}", create);
133 
134         let mut vec = db.lock().await;
135 
136         for todo in vec.iter() {
137             if todo.id == create.id {
138                 log::debug!("    -> id already exists: {}", create.id);
139                 // Todo with id already exists, return `400 BadRequest`.
140                 return Ok(StatusCode::BAD_REQUEST);
141             }
142         }
143 
144         // No existing Todo with id, so insert and return `201 Created`.
145         vec.push(create);
146 
147         Ok(StatusCode::CREATED)
148     }
149 
update_todo( id: u64, update: Todo, db: Db, ) -> Result<impl warp::Reply, Infallible>150     pub async fn update_todo(
151         id: u64,
152         update: Todo,
153         db: Db,
154     ) -> Result<impl warp::Reply, Infallible> {
155         log::debug!("update_todo: id={}, todo={:?}", id, update);
156         let mut vec = db.lock().await;
157 
158         // Look for the specified Todo...
159         for todo in vec.iter_mut() {
160             if todo.id == id {
161                 *todo = update;
162                 return Ok(StatusCode::OK);
163             }
164         }
165 
166         log::debug!("    -> todo id not found!");
167 
168         // If the for loop didn't return OK, then the ID doesn't exist...
169         Ok(StatusCode::NOT_FOUND)
170     }
171 
delete_todo(id: u64, db: Db) -> Result<impl warp::Reply, Infallible>172     pub async fn delete_todo(id: u64, db: Db) -> Result<impl warp::Reply, Infallible> {
173         log::debug!("delete_todo: id={}", id);
174 
175         let mut vec = db.lock().await;
176 
177         let len = vec.len();
178         vec.retain(|todo| {
179             // Retain all Todos that aren't this id...
180             // In other words, remove all that *are* this id...
181             todo.id != id
182         });
183 
184         // If the vec is smaller, we found and deleted a Todo!
185         let deleted = vec.len() != len;
186 
187         if deleted {
188             // respond with a `204 No Content`, which means successful,
189             // yet no body expected...
190             Ok(StatusCode::NO_CONTENT)
191         } else {
192             log::debug!("    -> todo id not found!");
193             Ok(StatusCode::NOT_FOUND)
194         }
195     }
196 }
197 
198 mod models {
199     use serde_derive::{Deserialize, Serialize};
200     use std::sync::Arc;
201     use tokio::sync::Mutex;
202 
203     /// So we don't have to tackle how different database work, we'll just use
204     /// a simple in-memory DB, a vector synchronized by a mutex.
205     pub type Db = Arc<Mutex<Vec<Todo>>>;
206 
blank_db() -> Db207     pub fn blank_db() -> Db {
208         Arc::new(Mutex::new(Vec::new()))
209     }
210 
211     #[derive(Debug, Deserialize, Serialize, Clone)]
212     pub struct Todo {
213         pub id: u64,
214         pub text: String,
215         pub completed: bool,
216     }
217 
218     // The query parameters for list_todos.
219     #[derive(Debug, Deserialize)]
220     pub struct ListOptions {
221         pub offset: Option<usize>,
222         pub limit: Option<usize>,
223     }
224 }
225 
226 #[cfg(test)]
227 mod tests {
228     use warp::http::StatusCode;
229     use warp::test::request;
230 
231     use super::{
232         filters,
233         models::{self, Todo},
234     };
235 
236     #[tokio::test]
test_post()237     async fn test_post() {
238         let db = models::blank_db();
239         let api = filters::todos(db);
240 
241         let resp = request()
242             .method("POST")
243             .path("/todos")
244             .json(&Todo {
245                 id: 1,
246                 text: "test 1".into(),
247                 completed: false,
248             })
249             .reply(&api)
250             .await;
251 
252         assert_eq!(resp.status(), StatusCode::CREATED);
253     }
254 
255     #[tokio::test]
test_post_conflict()256     async fn test_post_conflict() {
257         let db = models::blank_db();
258         db.lock().await.push(todo1());
259         let api = filters::todos(db);
260 
261         let resp = request()
262             .method("POST")
263             .path("/todos")
264             .json(&todo1())
265             .reply(&api)
266             .await;
267 
268         assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
269     }
270 
271     #[tokio::test]
test_put_unknown()272     async fn test_put_unknown() {
273         let _ = pretty_env_logger::try_init();
274         let db = models::blank_db();
275         let api = filters::todos(db);
276 
277         let resp = request()
278             .method("PUT")
279             .path("/todos/1")
280             .header("authorization", "Bearer admin")
281             .json(&todo1())
282             .reply(&api)
283             .await;
284 
285         assert_eq!(resp.status(), StatusCode::NOT_FOUND);
286     }
287 
todo1() -> Todo288     fn todo1() -> Todo {
289         Todo {
290             id: 1,
291             text: "test 1".into(),
292             completed: false,
293         }
294     }
295 }
296