--- title: joyce - 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. On a high-level, this system should be: - *Hubiquitous*, needs to be available (read/write) whenever one has internet connection (falling back to pen&paper, otherwise). - *Searchable*, one has to be able to search and filter the pile of notes. - *Out of the way*, sophistication will only get in the way of recording one's thoughts. `joyce` is structured as a small tool written in Rust running on a server, loosely inspired by [`twtxt`](https://github.com/buckket/twtxt). Clients interface themselves with the server through a simple REST API. # Notes 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 UUID and a timestamp, both assigned at creation, a list of tags, and the body of the note. A set of notes is just a `Vec` of `Note`s. ```{#note_struct .rust} #[derive(Debug, Deserialize, Serialize)] pub struct Note { uuid: Uuid, timestamp: DateTime, tags: Vec, body: String, } type Notes = Vec; ``` A `Note` can be automatically created from a `NoteRequest`. ```{#note_impl .rust} impl Note { fn new(body: String, tags: Vec) -> Self { Self { uuid: Uuid::new_v4(), timestamp: Utc::now(), tags, body } } } impl From for Note { fn from(req: NoteRequest) -> Self { Self::new(req.body, req.tags) } } ``` Similarly, a `NoteRequest` is what a client would request at note creation. It contains the same information as a `Note` without the information assigned at creation by the server. ```{#note_request_struct .rust} #[derive(Debug, Deserialize, Serialize)] pub struct NoteRequest { tags: Vec, body: String, } type NoteRequests = Vec; ``` ### (De)serialization Since notes need to be sent and received via HTTP, the structure needs to be *serializable*. ```{#dependencies .toml} serde = { version = "1.0", features = ["derive"] } ``` ```{#note_uses .rust} use serde::{Serialize, Deserialize}; ``` ### UUIDs UUIDs are unique 128-bit identifiers, stored as 16 octets, and formatted as a hex string in five groups, e.g., `67e55044-10b1-426f-9247-bb680e5fe0c8`. Have a look at the [Wikipedia entry](http://en.wikipedia.org/wiki/Universally_unique_identifier) for more information. ```{#dependencies .toml} uuid = { version = "1.1", features = ["v4","fast-rng","serde"] } ``` ```{#note_uses .rust} use uuid::Uuid; ``` ### Timestamps Timestamps adhere the *RFC 3339 date-time standard* with UTC offset. ```{#dependencies .toml} chrono = { version = "0.4", features = ["serde"] } ``` ```{#note_uses .rust} use chrono::prelude::{DateTime, Utc}; ``` # The REST API `joyce` uses [`actix-web`](https://actix.rs/) to handle HTTP requests and responses. ```{#dependencies .toml} actix-web = "4.1" ``` ```{#note_uses .rust} use actix_web::{HttpResponse,Responder,web,get,post}; use actix_web::http::header::ContentType; ``` Each request handlers is an *async* function that accepts zero or more parameters, extracted from a request (see [`FromRequest`](https://docs.rs/actix-web/latest/actix_web/trait.FromRequest.html) trait), and returns an [`HttpResponse`](https://docs.rs/actix-web/latest/actix_web/struct.HttpResponse.html). ## 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. ```{#req_get_notes .rust} #[get("/notes")] pub async fn list() -> HttpResponse { let notes: Notes; <> HttpResponse::Ok() .content_type(ContentType::json()) .json(notes) } ``` ## POST /notes New notes can be added by POSTing a JSON array of `NoteRequest`s of the form ```json { "body": "This is a funny note", "tags": [ "joyce", "funny", "example" ] } ``` ```{#req_post_notes .rust} #[post("/notes")] pub async fn add(new_notes: web::Json) -> impl Responder { let mut new_notes: Notes = new_notes .into_inner() .into_iter() .map(|n| n.into()) .collect(); let count = new_notes.len(); <> format!("Successfully added {} note(s)", count) } ``` ## GET /tag/{tags} This handler allows to query the set of notes for specific tags. One or more tags separated by `+` can be passed to the request. ```{#req_get_tags .rust} #[get("/tag/{tags}")] pub async fn get_tags(tags: web::Path) -> HttpResponse { let tags = tags.split('+').map(|t| t.to_string()).collect::>(); let notes: Notes; <> let tagged = notes.into_iter().filter(|n| tags.iter().all(|t| n.tags.contains(t))).collect::>(); HttpResponse::Ok() .content_type(ContentType::json()) .json(tagged) } ``` # 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}; ``` The `App` will register all request handlers defined above. ```{#main_service .rust} #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(note::list) .service(note::add) .service(note::get_tags) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` # Testing ## Using a file as a backend This is a temporary solution until an interface to a database (most likely SQLite) is added. ```{#dependencies .toml} serde_json = "1.0" ``` ```{#note_uses .rust} use std::fs::File; ``` ### Retrieving notes ```{#notes_retrieve .rust} notes = { let db = File::open("notes.db").expect("Unable to open 'notes.db'"); serde_json::from_reader(&db).unwrap_or(vec![]) }; ``` ### Adding notes ```{#notes_add .rust} let mut notes: Notes; <> notes.append(&mut new_notes); 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'"); ``` # Open questions - Should one be able to delete notes? Or mark them as read/processed? - Authentication method? - Custom filters on retrieval. # Credits `joyce v0.1.0` was created by Federico Igne ([git@federicoigne.com](mailto:git@federicoigne.com)) and available at [`https://git.dyamon.me/projects/joyce`](https://git.dyamon.me/projects/joyce). ```{#Cargo.toml .toml} [package] name = "joyce" version = "0.1.0" edition = "2021" [dependencies] <> ```