-
Notifications
You must be signed in to change notification settings - Fork 202
/
todo.rs
273 lines (249 loc) · 8.29 KB
/
todo.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
use std::sync::Mutex;
use actix_web::{
delete, get, post, put,
web::{Data, Json, Path, Query},
HttpResponse, Responder,
};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use utoipa_actix_web::service_config::ServiceConfig;
use crate::{LogApiKey, RequireApiKey};
#[derive(Default)]
pub(super) struct TodoStore {
todos: Mutex<Vec<Todo>>,
}
const TODO: &str = "todo";
pub(super) fn configure(store: Data<TodoStore>) -> impl FnOnce(&mut ServiceConfig) {
|config: &mut ServiceConfig| {
config
.app_data(store)
.service(search_todos)
.service(get_todos)
.service(create_todo)
.service(delete_todo)
.service(get_todo_by_id)
.service(update_todo);
}
}
/// Task to do.
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
struct Todo {
/// Unique id for the todo item.
#[schema(example = 1)]
id: i32,
/// Description of the tasks to do.
#[schema(example = "Remember to buy groceries")]
value: String,
/// Mark is the task done or not
checked: bool,
}
/// Request to update existing `Todo` item.
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
struct TodoUpdateRequest {
/// Optional new value for the `Todo` task.
#[schema(example = "Dentist at 14.00")]
value: Option<String>,
/// Optional check status to mark is the task done or not.
checked: Option<bool>,
}
/// Todo endpoint error responses
#[derive(Serialize, Deserialize, Clone, ToSchema)]
pub(super) enum ErrorResponse {
/// When Todo is not found by search term.
NotFound(String),
/// When there is a conflict storing a new todo.
Conflict(String),
/// When todo endpoint was called without correct credentials
Unauthorized(String),
}
/// Get list of todos.
///
/// List todos from in-memory todo store.
///
/// One could call the api endpoint with following curl.
/// ```text
/// curl localhost:8080/todo
/// ```
#[utoipa::path(
tag = TODO,
responses(
(status = 200, description = "List current todo items", body = [Todo])
)
)]
#[get("")]
async fn get_todos(todo_store: Data<TodoStore>) -> impl Responder {
let todos = todo_store.todos.lock().unwrap();
HttpResponse::Ok().json(todos.clone())
}
/// Create new Todo to shared in-memory storage.
///
/// Post a new `Todo` in request body as json to store it. Api will return
/// created `Todo` on success or `ErrorResponse::Conflict` if todo with same id already exists.
///
/// One could call the api with.
/// ```text
/// curl localhost:8080/todo -d '{"id": 1, "value": "Buy movie ticket", "checked": false}'
/// ```
#[utoipa::path(
tag = TODO,
responses(
(status = 201, description = "Todo created successfully", body = Todo),
(status = 409, description = "Todo with id already exists", body = ErrorResponse, example = json!(ErrorResponse::Conflict(String::from("id = 1"))))
)
)]
#[post("")]
async fn create_todo(todo: Json<Todo>, todo_store: Data<TodoStore>) -> impl Responder {
let mut todos = todo_store.todos.lock().unwrap();
let todo = &todo.into_inner();
todos
.iter()
.find(|existing| existing.id == todo.id)
.map(|existing| {
HttpResponse::Conflict().json(ErrorResponse::Conflict(format!("id = {}", existing.id)))
})
.unwrap_or_else(|| {
todos.push(todo.clone());
HttpResponse::Ok().json(todo)
})
}
/// Delete Todo by given path variable id.
///
/// This endpoint needs `api_key` authentication in order to call. Api key can be found from README.md.
///
/// Api will delete todo from shared in-memory storage by the provided id and return success 200.
/// If storage does not contain `Todo` with given id 404 not found will be returned.
#[utoipa::path(
tag = TODO,
responses(
(status = 200, description = "Todo deleted successfully"),
(status = 401, description = "Unauthorized to delete Todo", body = ErrorResponse, example = json!(ErrorResponse::Unauthorized(String::from("missing api key")))),
(status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1"))))
),
params(
("id", description = "Unique storage id of Todo")
),
security(
("api_key" = [])
)
)]
#[delete("/{id}", wrap = "RequireApiKey")]
async fn delete_todo(id: Path<i32>, todo_store: Data<TodoStore>) -> impl Responder {
let mut todos = todo_store.todos.lock().unwrap();
let id = id.into_inner();
let new_todos = todos
.iter()
.filter(|todo| todo.id != id)
.cloned()
.collect::<Vec<_>>();
if new_todos.len() == todos.len() {
HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}")))
} else {
*todos = new_todos;
HttpResponse::Ok().finish()
}
}
/// Get Todo by given todo id.
///
/// Return found `Todo` with status 200 or 404 not found if `Todo` is not found from shared in-memory storage.
#[utoipa::path(
tag = TODO,
responses(
(status = 200, description = "Todo found from storage", body = Todo),
(status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1"))))
),
params(
("id", description = "Unique storage id of Todo")
)
)]
#[get("/{id}")]
async fn get_todo_by_id(id: Path<i32>, todo_store: Data<TodoStore>) -> impl Responder {
let todos = todo_store.todos.lock().unwrap();
let id = id.into_inner();
todos
.iter()
.find(|todo| todo.id == id)
.map(|todo| HttpResponse::Ok().json(todo))
.unwrap_or_else(|| {
HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}")))
})
}
/// Update Todo with given id.
///
/// This endpoint supports optional authentication.
///
/// Tries to update `Todo` by given id as path variable. If todo is found by id values are
/// updated according `TodoUpdateRequest` and updated `Todo` is returned with status 200.
/// If todo is not found then 404 not found is returned.
#[utoipa::path(
tag = TODO,
responses(
(status = 200, description = "Todo updated successfully", body = Todo),
(status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1"))))
),
params(
("id", description = "Unique storage id of Todo")
),
security(
(),
("api_key" = [])
)
)]
#[put("/{id}", wrap = "LogApiKey")]
async fn update_todo(
id: Path<i32>,
todo: Json<TodoUpdateRequest>,
todo_store: Data<TodoStore>,
) -> impl Responder {
let mut todos = todo_store.todos.lock().unwrap();
let id = id.into_inner();
let todo = todo.into_inner();
todos
.iter_mut()
.find_map(|todo| if todo.id == id { Some(todo) } else { None })
.map(|existing_todo| {
if let Some(checked) = todo.checked {
existing_todo.checked = checked;
}
if let Some(value) = todo.value {
existing_todo.value = value;
}
HttpResponse::Ok().json(existing_todo)
})
.unwrap_or_else(|| {
HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}")))
})
}
/// Search todos Query
#[derive(Deserialize, Debug, IntoParams)]
struct SearchTodos {
/// Content that should be found from Todo's value field
value: String,
}
/// Search Todos with by value
///
/// Perform search from `Todo`s present in in-memory storage by matching Todo's value to
/// value provided as query parameter. Returns 200 and matching `Todo` items.
#[utoipa::path(
tag = TODO,
params(
SearchTodos
),
responses(
(status = 200, description = "Search Todos did not result error", body = [Todo]),
)
)]
#[get("/search")]
async fn search_todos(query: Query<SearchTodos>, todo_store: Data<TodoStore>) -> impl Responder {
let todos = todo_store.todos.lock().unwrap();
HttpResponse::Ok().json(
todos
.iter()
.filter(|todo| {
todo.value
.to_lowercase()
.contains(&query.value.to_lowercase())
})
.cloned()
.collect::<Vec<_>>(),
)
}