From 9b7077a7696d0ac9e649abeff8e6f469807402aa Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Sat, 6 Aug 2022 13:03:55 +0100 Subject: refactor: rework structure --- README.md | 208 +++++++++++++++++++++++++++----------------------------------- 1 file changed, 89 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 2d6b444..4366979 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ --- -title: joyce - record your thoughts as they come +title: joyce +subtitle: Record your thoughts as they come. author: Federico Igne date: \today ---- +... `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. @@ -19,28 +20,6 @@ Clients interface themselves with the server through a simple REST API. Notes are the first-class citizen of `joyce` and are the main content exchanged between server and clients. -Notes, along with their REST API are defined in their own module - -```{#main_mods .rust} -mod note; -``` - -```{#note.rs .rust path="src/"} -<> - -<> - -<> - -<> - -<> - -<> - -<> -``` - ## Anatomy of a note 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 for NoteParams { ### (De)serialization -Since notes need to be sent and received via HTTP, the structure needs to be *serializable*. +Since notes need to be sent and received via HTTP, the structure needs to be *serializable* (from/to JSON format). ```{#dependencies .toml} serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" ``` ```{#note_uses .rust} @@ -155,31 +135,19 @@ Each request handlers is an *async* function that accepts zero or more parameter Internally requests will be carried out by querying the underlying SQLite database. -```{#main_mods .rust} -mod db; -``` - ```{#note_uses .rust} use super::db; ``` -## Resources - -- [Tutorial](https://web.archive.org/web/20220710213947/https://hub.qovery.com/guides/tutorial/create-a-blazingly-fast-api-in-rust-part-1/) -- [Introduction to `actix`](https://actix.rs/docs/getting-started/) - ## GET /notes This handler allows to request the full list of notes currently in the system. -The function takes 0 parameters and returns a JSON object. +The function takes 1 parameters (the connection pool to the underlying SQLite database) and returns a collection of notes as a JSON array. ```{#req_get_notes .rust} #[get("/notes")] pub async fn get_notes(pool: web::Data) -> HttpResponse { - let mut conn = web::block(move || pool.get()) - .await - .expect("Blocking error") - .expect("Error getting from connection pool"); + <> let notes: Vec = db::get_notes(&mut conn); HttpResponse::Ok() .content_type(ContentType::json()) @@ -198,13 +166,16 @@ New notes can be added by POSTing a JSON array of `NoteRequest`s of the form } ``` +The function takes 2 parameters: + +- the connection pool, +- the collection of `NoteRequests` as a JSON object. + + ```{#req_post_notes .rust} #[post("/notes")] pub async fn post_notes(pool: web::Data, req: web::Json>) -> impl Responder { - let mut conn = web::block(move || pool.get()) - .await - .expect("Blocking error") - .expect("Error getting from connection pool"); + <> let res = db::post_notes(&mut conn, req.into_inner()); format!("Successfully added {res} note(s)") } @@ -212,6 +183,8 @@ pub async fn post_notes(pool: web::Data, req: web::Json>) ## GET /tag/{tags} +**Deprecated:** this is currently not implemented for the new SQLite backend. + This handler allows to query the set of notes for specific tags. One or more tags separated by `+` can be passed to the request. @@ -220,9 +193,7 @@ One or more tags separated by `+` can be passed to the request. pub async fn get_tags(tags: web::Path) -> HttpResponse { let tags = tags.split('+').map(|t| t.to_string()).collect::>(); - let notes: Vec; - <> - let tagged = notes.into_iter().filter(|n| tags.iter().all(|t| n.tags.contains(t))).collect::>(); + todo(); HttpResponse::Ok() .content_type(ContentType::json()) @@ -230,48 +201,10 @@ pub async fn get_tags(tags: web::Path) -> HttpResponse { } ``` -# The program - -The main program is structured as follows - -```{#main.rs .rust path="src/"} -<> - -<> - -<> -``` - -# Main service - -The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080. - -```{#main_uses .rust} -use actix_web::{App, HttpServer, web}; -``` - -The `App` will register all request handlers defined above. - -```{#main_service .rust} -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let db_pool = db::get_connection_pool("notes.db"); - HttpServer::new(move || { - App::new() - .app_data(web::Data::new(db_pool.clone())) - .service(note::get_notes) - .service(note::post_notes) - }) - .bind(("127.0.0.1", 8080))? - .run() - .await -} -``` - # SQLite backend `Note`s are saved into a [SQLite](https://sqlite.org/) database. -The `notes` database contains a single table mirroring the `Note`'s structure. +The `notes` database contains a single table mirroring the [`Note`'s structure](#anatomy-of-a-note). ```{#notes.sql .sql} CREATE TABLE notes ( @@ -282,7 +215,7 @@ CREATE TABLE notes ( ); ``` -Note that, apart from [standard SQLite types](https://docs.rs/rusqlite/latest/rusqlite/types/index.html#), `DateTime` is converted to/from `TEXT`, `Vec` is first wrapped in a `Value` (from the `serde_json` crate) and then converted from/to `TEXT`. +Note that, apart from [standard SQLite types](https://docs.rs/rusqlite/latest/rusqlite/types/index.html#), `DateTime` is converted to/from `TEXT`, `Vec` is first wrapped in a JSON `Value` (from the `serde_json` crate) and then converted from/to `TEXT`. `id`s are handled automatically by SQLite and are not set on the Rust side. @@ -319,16 +252,14 @@ pub fn get_connection_pool(db: &str) -> Pool { } ``` -For the sake of convenience, all operations on the database are stored on a separate file. +When needed, one can get a connection from the pool. +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. -```{#db.rs .rust path="src/"} -<> - -<> - -<> - -<> +```{#get_connection .rust} +let mut conn = web::block(move || pool.get()) + .await + .expect("Blocking error") + .expect("Error getting from connection pool"); ``` ### Retrieving notes @@ -356,8 +287,7 @@ pub fn get_notes(conn: &mut Connection) -> Vec { ### Creating notes -When inserting new `Note`s in the database, we loop over the requested notes, attaching a timestamp and executing an `INSERT` SQL query. - +When inserting new `Note`s in the database, we loop over the requests, attaching a timestamp and executing an `INSERT` SQL query. SQLite will take care of attaching an ID to the new entry. 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}; ```{#db_operations .rust} pub fn post_notes(conn: &mut Connection, reqs: Vec) -> usize { + let insert = "INSERT INTO notes (timestamp, tags, body) VALUES (?, ?, ?)"; let tx = conn.transaction().expect("Failed to start transaction"); { - let mut stmt = tx.prepare_cached( - "INSERT INTO notes (timestamp, tags, body) VALUES (?, ?, ?)" - ).expect("Failed to prepare INSERT query"); + let mut stmt = tx.prepare_cached(insert).expect("Failed to prepare INSERT query"); reqs.into_iter().for_each(|req| { stmt.execute::(req.into()).expect("Failed to execute INSERT query"); }); } tx.commit().expect("Commit failed"); @@ -381,42 +310,83 @@ pub fn post_notes(conn: &mut Connection, reqs: Vec) -> usize { ``` -# Testing +# Main service -## Using a file as a backend +The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080. -This is a temporary solution until an interface to a database (most likely SQLite) is added. +```{#main_uses .rust} +use actix_web::{App, HttpServer, web}; +``` -```{#dependencies .toml} -serde_json = "1.0" +The `App` will register all request handlers defined above as *services*. + +```{#main_service .rust} +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let db_pool = db::get_connection_pool("notes.db"); + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(db_pool.clone())) + .service(note::get_notes) + .service(note::post_notes) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await +} ``` -### Retrieving notes +# The program structure -```{#notes_retrieve .rust} -notes = { - let db = File::open("notes.db").expect("Unable to open 'notes.db'"); - serde_json::from_reader(&db).unwrap_or(vec![]) -}; +The main program is structured as follows + +```{#main.rs .rust path="src/"} +mod note; +mod db; + +<> + +<> ``` -### Adding notes +Notes, along with their REST API are defined in their own `note` module. -```{#notes_add .rust} -let mut notes: Vec; -<> -notes.append(&mut new_notes); +```{#note.rs .rust path="src/"} +<> + +<> + +<> + +<> -let db = File::create("notes.db").expect("Unable to create/open 'notes.db'"); -serde_json::to_writer(&db,¬es).expect("Unable to write to 'notes.db'"); +<> + +<> + +<> +``` + +Communication with SQLite is grouped under the `db` module. + +```{#db.rs .rust path="src/"} +<> + +<> + +<> + +<> ``` # TODOs -- Better error handling +- Better error handling with `anyhow` +- CLI with `clap` ## Open questions +- logging capabilities - Should one be able to delete notes? Or mark them as read/processed? - Authentication method? - Custom filters on retrieval. -- cgit v1.2.3