diff options
| author | Federico Igne <git@federicoigne.com> | 2022-07-27 23:43:46 +0100 |
|---|---|---|
| committer | Federico Igne <git@federicoigne.com> | 2022-07-27 23:43:46 +0100 |
| commit | 2ec3e5b74b61233f28de99ef36c0bed156864f32 (patch) | |
| tree | 4b6ccb90f6ac52462913480184ccd8d099d6bd0a /README.md | |
| parent | 32f54b51afe153546d87f86a0e260486dcb17355 (diff) | |
| download | joyce-2ec3e5b74b61233f28de99ef36c0bed156864f32.tar.gz joyce-2ec3e5b74b61233f28de99ef36c0bed156864f32.zip | |
feat(POST /notes): add ability to add new notes.
Notes are stored in a simple json file for now. Will implement a proper
backend later on (most likely SQLite).
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 117 |
1 files changed, 96 insertions, 21 deletions
| @@ -32,36 +32,57 @@ mod note; | |||
| 32 | 32 | ||
| 33 | <<note_impl>> | 33 | <<note_impl>> |
| 34 | 34 | ||
| 35 | <<note_request_struct>> | ||
| 36 | |||
| 35 | <<req_get_notes>> | 37 | <<req_get_notes>> |
| 38 | |||
| 39 | <<req_post_notes>> | ||
| 36 | ``` | 40 | ``` |
| 37 | 41 | ||
| 38 | ## Anatomy of a note | 42 | ## Anatomy of a note |
| 39 | 43 | ||
| 40 | A *note* is a simple structure recording a timestamp determined at creation, a list of tags, and the body of the note. | 44 | 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. |
| 45 | A set of notes is just a `Vec` of `Note`s. | ||
| 41 | 46 | ||
| 42 | ```{#note_struct .rust} | 47 | ```{#note_struct .rust} |
| 43 | #[derive(Debug, Deserialize, Serialize)] | 48 | #[derive(Debug, Deserialize, Serialize)] |
| 44 | pub struct Note { | 49 | pub struct Note { |
| 50 | uuid: Uuid, | ||
| 45 | timestamp: DateTime<Utc>, | 51 | timestamp: DateTime<Utc>, |
| 46 | tags: Vec<String>, | 52 | tags: Vec<String>, |
| 47 | body: String, | 53 | body: String, |
| 48 | } | 54 | } |
| 49 | 55 | ||
| 50 | <<note_collection>> | 56 | type Notes = Vec<Note>; |
| 51 | ``` | 57 | ``` |
| 52 | 58 | ||
| 59 | A `Note` can be automatically created from a `NoteRequest`. | ||
| 60 | |||
| 53 | ```{#note_impl .rust} | 61 | ```{#note_impl .rust} |
| 54 | impl Note { | 62 | impl Note { |
| 55 | fn new(body: String, tags: Vec<String>) -> Self { | 63 | fn new(body: String, tags: Vec<String>) -> Self { |
| 56 | Self { timestamp: Utc::now(), tags, body } | 64 | Self { uuid: Uuid::new_v4(), timestamp: Utc::now(), tags, body } |
| 57 | } | 65 | } |
| 58 | } | 66 | } |
| 67 | |||
| 68 | impl From<NoteRequest> for Note { | ||
| 69 | fn from(req: NoteRequest) -> Self { | ||
| 70 | Self::new(req.body, req.tags) | ||
| 71 | } | ||
| 72 | } | ||
| 59 | ``` | 73 | ``` |
| 60 | 74 | ||
| 61 | A set of notes is just a `Vec` of `Note`s. | 75 | Similarly, a `NoteRequest` is what a client would request at note creation. |
| 76 | It contains the same information as a `Note` without the information assigned at creation by the server. | ||
| 62 | 77 | ||
| 63 | ```{#note_collection .rust} | 78 | ```{#note_request_struct .rust} |
| 64 | type Notes = Vec<Note>; | 79 | #[derive(Debug, Deserialize, Serialize)] |
| 80 | pub struct NoteRequest { | ||
| 81 | tags: Vec<String>, | ||
| 82 | body: String, | ||
| 83 | } | ||
| 84 | |||
| 85 | type NoteRequests = Vec<NoteRequest>; | ||
| 65 | ``` | 86 | ``` |
| 66 | 87 | ||
| 67 | ### (De)serialization | 88 | ### (De)serialization |
| @@ -76,6 +97,19 @@ serde = { version = "1.0", features = ["derive"] } | |||
| 76 | use serde::{Serialize, Deserialize}; | 97 | use serde::{Serialize, Deserialize}; |
| 77 | ``` | 98 | ``` |
| 78 | 99 | ||
| 100 | ### UUIDs | ||
| 101 | |||
| 102 | 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`. | ||
| 103 | Have a look at the [Wikipedia entry](http://en.wikipedia.org/wiki/Universally_unique_identifier) for more information. | ||
| 104 | |||
| 105 | ```{#dependencies .toml} | ||
| 106 | uuid = { version = "1.1", features = ["v4","fast-rng","serde"] } | ||
| 107 | ``` | ||
| 108 | |||
| 109 | ```{#note_uses .rust} | ||
| 110 | use uuid::Uuid; | ||
| 111 | ``` | ||
| 112 | |||
| 79 | ### Timestamps | 113 | ### Timestamps |
| 80 | 114 | ||
| 81 | Timestamps adhere the *RFC 3339 date-time standard* with UTC offset. | 115 | Timestamps adhere the *RFC 3339 date-time standard* with UTC offset. |
| @@ -96,6 +130,11 @@ use chrono::prelude::{DateTime, Utc}; | |||
| 96 | actix-web = "4.1" | 130 | actix-web = "4.1" |
| 97 | ``` | 131 | ``` |
| 98 | 132 | ||
| 133 | ```{#note_uses .rust} | ||
| 134 | use actix_web::{HttpResponse,Responder,web,get,post}; | ||
| 135 | use actix_web::http::header::ContentType; | ||
| 136 | ``` | ||
| 137 | |||
| 99 | ## Resources | 138 | ## Resources |
| 100 | 139 | ||
| 101 | - [Tutorial](https://web.archive.org/web/20220710213947/https://hub.qovery.com/guides/tutorial/create-a-blazingly-fast-api-in-rust-part-1/) | 140 | - [Tutorial](https://web.archive.org/web/20220710213947/https://hub.qovery.com/guides/tutorial/create-a-blazingly-fast-api-in-rust-part-1/) |
| @@ -109,15 +148,10 @@ Each request handlers is an *async* function that accepts zero or more parameter | |||
| 109 | Here we are requesting the list of notes currently in the system. | 148 | Here we are requesting the list of notes currently in the system. |
| 110 | The function takes 0 parameters and returns a JSON object. | 149 | The function takes 0 parameters and returns a JSON object. |
| 111 | 150 | ||
| 112 | ```{#note_uses .rust} | ||
| 113 | use actix_web::{HttpResponse,get}; | ||
| 114 | use actix_web::http::header::ContentType; | ||
| 115 | ``` | ||
| 116 | |||
| 117 | ```{#req_get_notes .rust} | 151 | ```{#req_get_notes .rust} |
| 118 | #[get("/notes")] | 152 | #[get("/notes")] |
| 119 | pub async fn list() -> HttpResponse { | 153 | pub async fn list() -> HttpResponse { |
| 120 | let mut notes: Notes = vec![]; | 154 | let mut notes: Notes; |
| 121 | 155 | ||
| 122 | <<notes_retrieve>> | 156 | <<notes_retrieve>> |
| 123 | 157 | ||
| @@ -127,6 +161,25 @@ pub async fn list() -> HttpResponse { | |||
| 127 | } | 161 | } |
| 128 | ``` | 162 | ``` |
| 129 | 163 | ||
| 164 | ## POST /notes | ||
| 165 | |||
| 166 | ```{#req_post_notes .rust} | ||
| 167 | #[post("/notes")] | ||
| 168 | pub async fn add(new_notes: web::Json<NoteRequests>) -> impl Responder { | ||
| 169 | let mut new_notes: Notes = | ||
| 170 | new_notes | ||
| 171 | .into_inner() | ||
| 172 | .into_iter() | ||
| 173 | .map(|n| n.into()) | ||
| 174 | .collect(); | ||
| 175 | let count = new_notes.len(); | ||
| 176 | |||
| 177 | <<notes_add>> | ||
| 178 | |||
| 179 | format!("Successfully added {} note(s)", count) | ||
| 180 | } | ||
| 181 | ``` | ||
| 182 | |||
| 130 | # The program | 183 | # The program |
| 131 | 184 | ||
| 132 | The main program is structured as follows | 185 | The main program is structured as follows |
| @@ -155,6 +208,7 @@ async fn main() -> std::io::Result<()> { | |||
| 155 | HttpServer::new(|| { | 208 | HttpServer::new(|| { |
| 156 | App::new() | 209 | App::new() |
| 157 | .service(note::list) | 210 | .service(note::list) |
| 211 | .service(note::add) | ||
| 158 | }) | 212 | }) |
| 159 | .bind(("127.0.0.1", 8080))? | 213 | .bind(("127.0.0.1", 8080))? |
| 160 | .run() | 214 | .run() |
| @@ -164,17 +218,38 @@ async fn main() -> std::io::Result<()> { | |||
| 164 | 218 | ||
| 165 | # Testing | 219 | # Testing |
| 166 | 220 | ||
| 221 | ## Using a file as a backend | ||
| 222 | |||
| 223 | This is a temporary solution until an interface to a database (most likely SQLite) is added. | ||
| 224 | |||
| 225 | ```{#dependencies .toml} | ||
| 226 | serde_json = "1.0" | ||
| 227 | ``` | ||
| 228 | |||
| 229 | ```{#note_uses .rust} | ||
| 230 | use std::fs::File; | ||
| 231 | ``` | ||
| 232 | |||
| 233 | ### Retrieving notes | ||
| 234 | |||
| 167 | ```{#notes_retrieve .rust} | 235 | ```{#notes_retrieve .rust} |
| 168 | notes.push(Note::new( | 236 | notes = { |
| 169 | String::from("This is a first test note!"), | 237 | let db = File::open("notes.db").expect("Unable to open 'notes.db'"); |
| 170 | vec![String::from("test"), String::from("alpha"), String::from("literate")])); | 238 | serde_json::from_reader(&db).unwrap_or(vec![]) |
| 171 | notes.push(Note::new( | 239 | }; |
| 172 | String::from("This is my favourite emoji: 🦥"), | 240 | ``` |
| 173 | vec![String::from("test"), String::from("emoji")])); | 241 | |
| 174 | notes.push(Note::new( | 242 | ### Adding notes |
| 175 | String::from("Here is a link: https://www.federicoigne.com/"), | 243 | |
| 176 | vec![String::from("test"), String::from("links")])); | 244 | ```{#notes_add .rust} |
| 245 | let mut notes: Notes; | ||
| 246 | <<notes_retrieve>> | ||
| 247 | notes.append(&mut new_notes); | ||
| 248 | |||
| 249 | let db = File::create("notes.db").expect("Unable to create/open 'notes.db'"); | ||
| 250 | serde_json::to_writer(&db,¬es).expect("Unable to write to 'notes.db'"); | ||
| 177 | ``` | 251 | ``` |
| 252 | |||
| 178 | # Open questions | 253 | # Open questions |
| 179 | 254 | ||
| 180 | - Should one be able to delete notes? Or mark them as read/processed? | 255 | - Should one be able to delete notes? Or mark them as read/processed? |
