diff options
author | Federico Igne <git@federicoigne.com> | 2022-08-06 13:03:55 +0100 |
---|---|---|
committer | Federico Igne <git@federicoigne.com> | 2022-08-06 13:03:55 +0100 |
commit | 9b7077a7696d0ac9e649abeff8e6f469807402aa (patch) | |
tree | 4e616ab8350f861a11cf7e76af0944c24b558150 | |
parent | a922aa28908d83c8ecead51e6d63c7c549568b5c (diff) | |
download | joyce-9b7077a7696d0ac9e649abeff8e6f469807402aa.tar.gz joyce-9b7077a7696d0ac9e649abeff8e6f469807402aa.zip |
refactor: rework structure
-rw-r--r-- | README.md | 208 |
1 files changed, 89 insertions, 119 deletions
@@ -1,8 +1,9 @@ | |||
1 | --- | 1 | --- |
2 | title: joyce - record your thoughts as they come | 2 | title: joyce |
3 | subtitle: Record your thoughts as they come. | ||
3 | author: Federico Igne | 4 | author: Federico Igne |
4 | date: \today | 5 | date: \today |
5 | --- | 6 | ... |
6 | 7 | ||
7 | `joyce` is an attempt at building a tool for rapid notetaking, i.e., quick collection of short thoughts that can come to mind at any point. | 8 | `joyce` is an attempt at building a tool for rapid notetaking, i.e., quick collection of short thoughts that can come to mind at any point. |
8 | 9 | ||
@@ -19,28 +20,6 @@ Clients interface themselves with the server through a simple REST API. | |||
19 | 20 | ||
20 | Notes are the first-class citizen of `joyce` and are the main content exchanged between server and clients. | 21 | Notes are the first-class citizen of `joyce` and are the main content exchanged between server and clients. |
21 | 22 | ||
22 | Notes, along with their REST API are defined in their own module | ||
23 | |||
24 | ```{#main_mods .rust} | ||
25 | mod note; | ||
26 | ``` | ||
27 | |||
28 | ```{#note.rs .rust path="src/"} | ||
29 | <<note_uses>> | ||
30 | |||
31 | <<note_struct>> | ||
32 | |||
33 | <<note_impl>> | ||
34 | |||
35 | <<note_request_struct>> | ||
36 | |||
37 | <<note_request_impl>> | ||
38 | |||
39 | <<req_get_notes>> | ||
40 | |||
41 | <<req_post_notes>> | ||
42 | ``` | ||
43 | |||
44 | ## Anatomy of a note | 23 | ## Anatomy of a note |
45 | 24 | ||
46 | A *note* is a simple structure recording a unique ID and a timestamp, both assigned at creation, a list of tags, and the body of the note. | 25 | A *note* is a simple structure recording a unique ID and a timestamp, both assigned at creation, a list of tags, and the body of the note. |
@@ -111,10 +90,11 @@ impl From<NoteRequest> for NoteParams { | |||
111 | 90 | ||
112 | ### (De)serialization | 91 | ### (De)serialization |
113 | 92 | ||
114 | Since notes need to be sent and received via HTTP, the structure needs to be *serializable*. | 93 | Since notes need to be sent and received via HTTP, the structure needs to be *serializable* (from/to JSON format). |
115 | 94 | ||
116 | ```{#dependencies .toml} | 95 | ```{#dependencies .toml} |
117 | serde = { version = "1.0", features = ["derive"] } | 96 | serde = { version = "1.0", features = ["derive"] } |
97 | serde_json = "1.0" | ||
118 | ``` | 98 | ``` |
119 | 99 | ||
120 | ```{#note_uses .rust} | 100 | ```{#note_uses .rust} |
@@ -155,31 +135,19 @@ Each request handlers is an *async* function that accepts zero or more parameter | |||
155 | 135 | ||
156 | Internally requests will be carried out by querying the underlying SQLite database. | 136 | Internally requests will be carried out by querying the underlying SQLite database. |
157 | 137 | ||
158 | ```{#main_mods .rust} | ||
159 | mod db; | ||
160 | ``` | ||
161 | |||
162 | ```{#note_uses .rust} | 138 | ```{#note_uses .rust} |
163 | use super::db; | 139 | use super::db; |
164 | ``` | 140 | ``` |
165 | 141 | ||
166 | ## Resources | ||
167 | |||
168 | - [Tutorial](https://web.archive.org/web/20220710213947/https://hub.qovery.com/guides/tutorial/create-a-blazingly-fast-api-in-rust-part-1/) | ||
169 | - [Introduction to `actix`](https://actix.rs/docs/getting-started/) | ||
170 | |||
171 | ## GET /notes | 142 | ## GET /notes |
172 | 143 | ||
173 | This handler allows to request the full list of notes currently in the system. | 144 | This handler allows to request the full list of notes currently in the system. |
174 | The function takes 0 parameters and returns a JSON object. | 145 | The function takes 1 parameters (the connection pool to the underlying SQLite database) and returns a collection of notes as a JSON array. |
175 | 146 | ||
176 | ```{#req_get_notes .rust} | 147 | ```{#req_get_notes .rust} |
177 | #[get("/notes")] | 148 | #[get("/notes")] |
178 | pub async fn get_notes(pool: web::Data<Pool>) -> HttpResponse { | 149 | pub async fn get_notes(pool: web::Data<Pool>) -> HttpResponse { |
179 | let mut conn = web::block(move || pool.get()) | 150 | <<get_connection>> |
180 | .await | ||
181 | .expect("Blocking error") | ||
182 | .expect("Error getting from connection pool"); | ||
183 | let notes: Vec<Note> = db::get_notes(&mut conn); | 151 | let notes: Vec<Note> = db::get_notes(&mut conn); |
184 | HttpResponse::Ok() | 152 | HttpResponse::Ok() |
185 | .content_type(ContentType::json()) | 153 | .content_type(ContentType::json()) |
@@ -198,13 +166,16 @@ New notes can be added by POSTing a JSON array of `NoteRequest`s of the form | |||
198 | } | 166 | } |
199 | ``` | 167 | ``` |
200 | 168 | ||
169 | The function takes 2 parameters: | ||
170 | |||
171 | - the connection pool, | ||
172 | - the collection of `NoteRequests` as a JSON object. | ||
173 | |||
174 | |||
201 | ```{#req_post_notes .rust} | 175 | ```{#req_post_notes .rust} |
202 | #[post("/notes")] | 176 | #[post("/notes")] |
203 | pub async fn post_notes(pool: web::Data<Pool>, req: web::Json<Vec<NoteRequest>>) -> impl Responder { | 177 | pub async fn post_notes(pool: web::Data<Pool>, req: web::Json<Vec<NoteRequest>>) -> impl Responder { |
204 | let mut conn = web::block(move || pool.get()) | 178 | <<get_connection>> |
205 | .await | ||
206 | .expect("Blocking error") | ||
207 | .expect("Error getting from connection pool"); | ||
208 | let res = db::post_notes(&mut conn, req.into_inner()); | 179 | let res = db::post_notes(&mut conn, req.into_inner()); |
209 | format!("Successfully added {res} note(s)") | 180 | format!("Successfully added {res} note(s)") |
210 | } | 181 | } |
@@ -212,6 +183,8 @@ pub async fn post_notes(pool: web::Data<Pool>, req: web::Json<Vec<NoteRequest>>) | |||
212 | 183 | ||
213 | ## GET /tag/{tags} | 184 | ## GET /tag/{tags} |
214 | 185 | ||
186 | **Deprecated:** this is currently not implemented for the new SQLite backend. | ||
187 | |||
215 | This handler allows to query the set of notes for specific tags. | 188 | This handler allows to query the set of notes for specific tags. |
216 | One or more tags separated by `+` can be passed to the request. | 189 | One or more tags separated by `+` can be passed to the request. |
217 | 190 | ||
@@ -220,9 +193,7 @@ One or more tags separated by `+` can be passed to the request. | |||
220 | pub async fn get_tags(tags: web::Path<String>) -> HttpResponse { | 193 | pub async fn get_tags(tags: web::Path<String>) -> HttpResponse { |
221 | let tags = tags.split('+').map(|t| t.to_string()).collect::<Vec<_>>(); | 194 | let tags = tags.split('+').map(|t| t.to_string()).collect::<Vec<_>>(); |
222 | 195 | ||
223 | let notes: Vec<Note>; | 196 | todo(); |
224 | <<notes_retrieve>> | ||
225 | let tagged = notes.into_iter().filter(|n| tags.iter().all(|t| n.tags.contains(t))).collect::<Vec<_>>(); | ||
226 | 197 | ||
227 | HttpResponse::Ok() | 198 | HttpResponse::Ok() |
228 | .content_type(ContentType::json()) | 199 | .content_type(ContentType::json()) |
@@ -230,48 +201,10 @@ pub async fn get_tags(tags: web::Path<String>) -> HttpResponse { | |||
230 | } | 201 | } |
231 | ``` | 202 | ``` |
232 | 203 | ||
233 | # The program | ||
234 | |||
235 | The main program is structured as follows | ||
236 | |||
237 | ```{#main.rs .rust path="src/"} | ||
238 | <<main_uses>> | ||
239 | |||
240 | <<main_mods>> | ||
241 | |||
242 | <<main_service>> | ||
243 | ``` | ||
244 | |||
245 | # Main service | ||
246 | |||
247 | The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080. | ||
248 | |||
249 | ```{#main_uses .rust} | ||
250 | use actix_web::{App, HttpServer, web}; | ||
251 | ``` | ||
252 | |||
253 | The `App` will register all request handlers defined above. | ||
254 | |||
255 | ```{#main_service .rust} | ||
256 | #[actix_web::main] | ||
257 | async fn main() -> std::io::Result<()> { | ||
258 | let db_pool = db::get_connection_pool("notes.db"); | ||
259 | HttpServer::new(move || { | ||
260 | App::new() | ||
261 | .app_data(web::Data::new(db_pool.clone())) | ||
262 | .service(note::get_notes) | ||
263 | .service(note::post_notes) | ||
264 | }) | ||
265 | .bind(("127.0.0.1", 8080))? | ||
266 | .run() | ||
267 | .await | ||
268 | } | ||
269 | ``` | ||
270 | |||
271 | # SQLite backend | 204 | # SQLite backend |
272 | 205 | ||
273 | `Note`s are saved into a [SQLite](https://sqlite.org/) database. | 206 | `Note`s are saved into a [SQLite](https://sqlite.org/) database. |
274 | The `notes` database contains a single table mirroring the `Note`'s structure. | 207 | The `notes` database contains a single table mirroring the [`Note`'s structure](#anatomy-of-a-note). |
275 | 208 | ||
276 | ```{#notes.sql .sql} | 209 | ```{#notes.sql .sql} |
277 | CREATE TABLE notes ( | 210 | CREATE TABLE notes ( |
@@ -282,7 +215,7 @@ CREATE TABLE notes ( | |||
282 | ); | 215 | ); |
283 | ``` | 216 | ``` |
284 | 217 | ||
285 | Note that, apart from [standard SQLite types](https://docs.rs/rusqlite/latest/rusqlite/types/index.html#), `DateTime<Utc>` is converted to/from `TEXT`, `Vec<String>` is first wrapped in a `Value` (from the `serde_json` crate) and then converted from/to `TEXT`. | 218 | Note that, apart from [standard SQLite types](https://docs.rs/rusqlite/latest/rusqlite/types/index.html#), `DateTime<Utc>` is converted to/from `TEXT`, `Vec<String>` is first wrapped in a JSON `Value` (from the `serde_json` crate) and then converted from/to `TEXT`. |
286 | 219 | ||
287 | `id`s are handled automatically by SQLite and are not set on the Rust side. | 220 | `id`s are handled automatically by SQLite and are not set on the Rust side. |
288 | 221 | ||
@@ -319,16 +252,14 @@ pub fn get_connection_pool(db: &str) -> Pool { | |||
319 | } | 252 | } |
320 | ``` | 253 | ``` |
321 | 254 | ||
322 | For the sake of convenience, all operations on the database are stored on a separate file. | 255 | When needed, one can get a connection from the pool. |
256 | This is a *blocking function* as as such is wrapped in a `web::block` to offload the task to one of the `actix` thread workers. | ||
323 | 257 | ||
324 | ```{#db.rs .rust path="src/"} | 258 | ```{#get_connection .rust} |
325 | <<db_uses>> | 259 | let mut conn = web::block(move || pool.get()) |
326 | 260 | .await | |
327 | <<db_types>> | 261 | .expect("Blocking error") |
328 | 262 | .expect("Error getting from connection pool"); | |
329 | <<db_connection_pool>> | ||
330 | |||
331 | <<db_operations>> | ||
332 | ``` | 263 | ``` |
333 | 264 | ||
334 | ### Retrieving notes | 265 | ### Retrieving notes |
@@ -356,8 +287,7 @@ pub fn get_notes(conn: &mut Connection) -> Vec<Note> { | |||
356 | 287 | ||
357 | ### Creating notes | 288 | ### Creating notes |
358 | 289 | ||
359 | When inserting new `Note`s in the database, we loop over the requested notes, attaching a timestamp and executing an `INSERT` SQL query. | 290 | When inserting new `Note`s in the database, we loop over the requests, attaching a timestamp and executing an `INSERT` SQL query. |
360 | |||
361 | SQLite will take care of attaching an ID to the new entry. | 291 | SQLite will take care of attaching an ID to the new entry. |
362 | 292 | ||
363 | Operations are executed into a single transaction [to achieve better performances](https://github.com/rusqlite/rusqlite/issues/262#issuecomment-294895051). | 293 | Operations are executed into a single transaction [to achieve better performances](https://github.com/rusqlite/rusqlite/issues/262#issuecomment-294895051). |
@@ -368,11 +298,10 @@ use super::note::{NoteParams,NoteRequest}; | |||
368 | 298 | ||
369 | ```{#db_operations .rust} | 299 | ```{#db_operations .rust} |
370 | pub fn post_notes(conn: &mut Connection, reqs: Vec<NoteRequest>) -> usize { | 300 | pub fn post_notes(conn: &mut Connection, reqs: Vec<NoteRequest>) -> usize { |
301 | let insert = "INSERT INTO notes (timestamp, tags, body) VALUES (?, ?, ?)"; | ||
371 | let tx = conn.transaction().expect("Failed to start transaction"); | 302 | let tx = conn.transaction().expect("Failed to start transaction"); |
372 | { | 303 | { |
373 | let mut stmt = tx.prepare_cached( | 304 | let mut stmt = tx.prepare_cached(insert).expect("Failed to prepare INSERT query"); |
374 | "INSERT INTO notes (timestamp, tags, body) VALUES (?, ?, ?)" | ||
375 | ).expect("Failed to prepare INSERT query"); | ||
376 | reqs.into_iter().for_each(|req| { stmt.execute::<NoteParams>(req.into()).expect("Failed to execute INSERT query"); }); | 305 | reqs.into_iter().for_each(|req| { stmt.execute::<NoteParams>(req.into()).expect("Failed to execute INSERT query"); }); |
377 | } | 306 | } |
378 | tx.commit().expect("Commit failed"); | 307 | tx.commit().expect("Commit failed"); |
@@ -381,42 +310,83 @@ pub fn post_notes(conn: &mut Connection, reqs: Vec<NoteRequest>) -> usize { | |||
381 | 310 | ||
382 | ``` | 311 | ``` |
383 | 312 | ||
384 | # Testing | 313 | # Main service |
385 | 314 | ||
386 | ## Using a file as a backend | 315 | The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080. |
387 | 316 | ||
388 | This is a temporary solution until an interface to a database (most likely SQLite) is added. | 317 | ```{#main_uses .rust} |
318 | use actix_web::{App, HttpServer, web}; | ||
319 | ``` | ||
389 | 320 | ||
390 | ```{#dependencies .toml} | 321 | The `App` will register all request handlers defined above as *services*. |
391 | serde_json = "1.0" | 322 | |
323 | ```{#main_service .rust} | ||
324 | #[actix_web::main] | ||
325 | async fn main() -> std::io::Result<()> { | ||
326 | let db_pool = db::get_connection_pool("notes.db"); | ||
327 | HttpServer::new(move || { | ||
328 | App::new() | ||
329 | .app_data(web::Data::new(db_pool.clone())) | ||
330 | .service(note::get_notes) | ||
331 | .service(note::post_notes) | ||
332 | }) | ||
333 | .bind(("127.0.0.1", 8080))? | ||
334 | .run() | ||
335 | .await | ||
336 | } | ||
392 | ``` | 337 | ``` |
393 | 338 | ||
394 | ### Retrieving notes | 339 | # The program structure |
395 | 340 | ||
396 | ```{#notes_retrieve .rust} | 341 | The main program is structured as follows |
397 | notes = { | 342 | |
398 | let db = File::open("notes.db").expect("Unable to open 'notes.db'"); | 343 | ```{#main.rs .rust path="src/"} |
399 | serde_json::from_reader(&db).unwrap_or(vec![]) | 344 | mod note; |
400 | }; | 345 | mod db; |
346 | |||
347 | <<main_uses>> | ||
348 | |||
349 | <<main_service>> | ||
401 | ``` | 350 | ``` |
402 | 351 | ||
403 | ### Adding notes | 352 | Notes, along with their REST API are defined in their own `note` module. |
404 | 353 | ||
405 | ```{#notes_add .rust} | 354 | ```{#note.rs .rust path="src/"} |
406 | let mut notes: Vec<Note>; | 355 | <<note_uses>> |
407 | <<notes_retrieve>> | 356 | |
408 | notes.append(&mut new_notes); | 357 | <<note_struct>> |
358 | |||
359 | <<note_impl>> | ||
360 | |||
361 | <<note_request_struct>> | ||
409 | 362 | ||
410 | let db = File::create("notes.db").expect("Unable to create/open 'notes.db'"); | 363 | <<note_request_impl>> |
411 | serde_json::to_writer(&db,¬es).expect("Unable to write to 'notes.db'"); | 364 | |
365 | <<req_get_notes>> | ||
366 | |||
367 | <<req_post_notes>> | ||
368 | ``` | ||
369 | |||
370 | Communication with SQLite is grouped under the `db` module. | ||
371 | |||
372 | ```{#db.rs .rust path="src/"} | ||
373 | <<db_uses>> | ||
374 | |||
375 | <<db_types>> | ||
376 | |||
377 | <<db_connection_pool>> | ||
378 | |||
379 | <<db_operations>> | ||
412 | ``` | 380 | ``` |
413 | 381 | ||
414 | # TODOs | 382 | # TODOs |
415 | 383 | ||
416 | - Better error handling | 384 | - Better error handling with `anyhow` |
385 | - CLI with `clap` | ||
417 | 386 | ||
418 | ## Open questions | 387 | ## Open questions |
419 | 388 | ||
389 | - logging capabilities | ||
420 | - Should one be able to delete notes? Or mark them as read/processed? | 390 | - Should one be able to delete notes? Or mark them as read/processed? |
421 | - Authentication method? | 391 | - Authentication method? |
422 | - Custom filters on retrieval. | 392 | - Custom filters on retrieval. |