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