diff options
Diffstat (limited to 'README.md')
| -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. |
