From 167d34731c1dc8b5937849dc1aac45f69907046f Mon Sep 17 00:00:00 2001 From: David Westgate Date: Sat, 27 Apr 2024 13:52:26 -0700 Subject: [PATCH] add full support for POST /answer --- answers.json | 1 + src/api.rs | 26 ++++++++----- src/main.rs | 2 +- src/store.rs | 105 ++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 answers.json diff --git a/answers.json b/answers.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/answers.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/api.rs b/src/api.rs index 676bb4f..9a53dcf 100644 --- a/src/api.rs +++ b/src/api.rs @@ -14,7 +14,7 @@ pub async fn read_question( State(store): State>>, Path(id): Path, ) -> Response { - match store.read().await.fetch_one(id) { + match store.read().await.fetch_one_question(id) { Ok(question) => question.to_dto(id).into_response(), Err(e) => (StatusCode::NOT_FOUND, e).into_response(), } @@ -34,7 +34,7 @@ pub async fn read_questions( let start: usize = page * size; ( StatusCode::OK, - Json(store.read().await.fetch_many(start, size)), + Json(store.read().await.fetch_many_questions(start, size)), ) .into_response() } @@ -53,7 +53,7 @@ pub async fn create_question( //Normally, the server should generate the id, user provided id's (and the whole request) should be rejected. //QuestionDTO id then would be an option, but that makes to/from entity conversion more tricky.. todo let (id, question) = question_dto.to_entity(); - match store.write().await.add(id, question) { + match store.write().await.add_question(id, question) { Ok(question) => (StatusCode::CREATED, Json(&question.to_dto(id))).into_response(), Err(e) => (StatusCode::CONFLICT, e).into_response(), } @@ -72,7 +72,7 @@ pub async fn update_question( Json(question_dto): Json, ) -> Response { let (id, question) = question_dto.to_entity(); - match store.write().await.update(id, question) { + match store.write().await.update_question(id, question) { Ok(question) => question.to_dto(id).into_response(), Err(e) => (StatusCode::NOT_FOUND, e).into_response(), } @@ -88,20 +88,28 @@ pub async fn delete_question( State(store): State>>, Path(id): Path, ) -> Response { - match store.write().await.remove(id) { + match store.write().await.remove_question(id) { Ok(question) => question.to_dto(id).into_response(), Err(e) => (StatusCode::NOT_FOUND, e).into_response(), } } -/// Create an Answer - WIP +/// Create an Answer /// # Parameters /// `answer_dto` Form URL encoded answer DTO /// # Returns +/// Status Created 201 and the created answer upon success +/// Status Conflict 409 and message if answer with ID already exists +/// Status Unprocessable Entity 422 is returned (implicitlly) for a malformed body + pub async fn create_answer( - State(_store): State>>, - Form(_answer_dto): Form, + State(store): State>>, + Form(answer_dto): Form, ) -> Response { - todo!() + let (id, answer) = answer_dto.to_entity(); + match store.write().await.add_answer(id, answer) { + Ok(answer) => (StatusCode::CREATED, Json(&answer.to_dto(id))).into_response(), + Err(e) => (StatusCode::CONFLICT, e).into_response(), + } } #[derive(Debug, Deserialize)] diff --git a/src/main.rs b/src/main.rs index e1c5516..c055b5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ async fn main() { .route("/question", post(api::create_question)) .route("/question", put(api::update_question)) .route("/question/:id", delete(api::delete_question)) - .route("/answers", post(api::create_answer)) + .route("/answer", post(api::create_answer)) .with_state(store) .fallback(handler_404); diff --git a/src/store.rs b/src/store.rs index 74950d0..92769bf 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,24 +1,30 @@ /// Store is responsible for manageing the in-memory hashmap of questions by providing initialization read/write functions, /// and file I/O operations to persist these questions /// TODO - Results returning errors should use specified types, not strings -use self::question::{Question, QuestionDTO}; +use self::{ + answer::{Answer, AnswerDTO}, + question::{Question, QuestionDTO}, +}; use crate::*; const QUESTIONS_DB_PATH: &str = "./questions.json"; +const ANSWERS_DB_PATH: &str = "./answers.json"; #[derive(Debug)] pub struct Store { - file: File, + answers_file: File, + questions_file: File, + answers: HashMap, questions: HashMap, - // answers: HashMap, //WIP, add answers to store } impl Store { - // Upon initialization, we need to read a questions.json if it exists and populate our questions hashmap from it. - // Otherwise we create questions.json. + // Upon initialization, we need to read a questions.json ans anwers.json if they exist and populate our hashmaps from them. + // Otherwise we create both files. // JSON formatting and I/O errors possible here are semi-handled with a message, but ultimetly we will panic in those cases + // TODO - make this less copy/paste like pub fn new() -> Self { - let file: File = File::create_new(QUESTIONS_DB_PATH) + let questions_file: File = File::create_new(QUESTIONS_DB_PATH) .or_else(|e| { if e.kind() == ErrorKind::AlreadyExists { File::options() @@ -30,29 +36,53 @@ impl Store { } }) .unwrap(); - let json = std::io::read_to_string(&file).expect("could not get json from file"); + let questions_json = std::io::read_to_string(&questions_file) + .expect("could not get json from questions file"); // perhaps there is a more efficient/clever way aside from reading the json to a vector and mapping the vector to a hashmap. let questions_vec: Vec = - serde_json::from_str(&json).expect("can't read questions.json"); + serde_json::from_str(&questions_json).expect("can't read questions.json"); let questions: HashMap = questions_vec .into_iter() .map(|question_dto: QuestionDTO| question_dto.to_entity()) .collect(); - Store { questions, file } + + let answers_file: File = File::create_new(ANSWERS_DB_PATH) + .or_else(|e| { + if e.kind() == ErrorKind::AlreadyExists { + File::options().read(true).write(true).open(ANSWERS_DB_PATH) + } else { + Err(e) + } + }) + .unwrap(); + let answers_json = + std::io::read_to_string(&answers_file).expect("could not get json from answers file"); + let answers_vec: Vec = + serde_json::from_str(&answers_json).expect("can't read answers.json"); + let answers: HashMap = answers_vec + .into_iter() + .map(|answer_dto: AnswerDTO| answer_dto.to_entity()) + .collect(); + + Store { + questions, + answers, + answers_file, + questions_file, + } } // Take the content of the questions hashmap, convert it to a vector of question DTOs and overwrite the file with these contents // Not the most efficient approach if we are just adding or deleting a single question, but it does the job at our current scale - // 'flush' is also probably a misnomer // TODO - pretty print before writing - fn flush(&mut self) { + fn write_questions_file(&mut self) { let questions: &HashMap = &self.questions; let questions_vec: Vec = questions .iter() .map(|q: (&u8, &Question)| q.1.to_dto(*q.0)) .collect(); let json: String = serde_json::to_string(&questions_vec).unwrap(); - let mut f: &File = &self.file; + let mut f: &File = &self.questions_file; match f .rewind() .and(f.write_all(json.as_bytes())) @@ -60,39 +90,59 @@ impl Store { .and(f.set_len(f.stream_position().unwrap())) { Ok(()) => (), - _ => panic!("Could not flush file"), + _ => panic!("Could not write file"), } } - pub fn add(&mut self, id: u8, question: Question) -> Result { + // Take the content of the answers hashmap, convert it to a vector of answer DTOs and overwrite the file with these contents + fn write_answers_file(&mut self) { + let answers: &HashMap = &self.answers; + let answers_vec: Vec = answers + .iter() + .map(|q: (&u8, &Answer)| q.1.to_dto(*q.0)) + .collect(); + let json: String = serde_json::to_string(&answers_vec).unwrap(); + let mut f: &File = &self.answers_file; + match f + .rewind() + .and(f.write_all(json.as_bytes())) + .and(f.sync_all()) + .and(f.set_len(f.stream_position().unwrap())) + { + Ok(()) => (), + _ => panic!("Could not write file"), + } + } + + pub fn add_question(&mut self, id: u8, question: Question) -> Result { if self.questions.contains_key(&id) { return Err(format!("Question with id {} already exists", id)); } match self.questions.insert(id, question.clone()) { None => { - self.flush(); + self.write_questions_file(); Ok(question) } //Looks backwards, but insert must return none since key cannot already exist _ => Err("Server Error".to_string()), } } - pub fn remove(&mut self, id: u8) -> Result { + pub fn remove_question(&mut self, id: u8) -> Result { match self.questions.remove(&id) { Some(question) => { - self.flush(); + self.write_questions_file(); Ok(question) } None => Err(format!("Question with id {} does not exist", id)), } } - pub fn fetch_one(&self, id: u8) -> Result { + pub fn fetch_one_question(&self, id: u8) -> Result { match self.questions.get(&id) { Some(question) => Ok(question.clone()), None => Err(format!("Question with id {} does not exists", id)), } } //by nature of the hashmap, pagination does not follow id order - pub fn fetch_many(&self, start: usize, size: usize) -> Vec { + pub fn fetch_many_questions(&self, start: usize, size: usize) -> Vec { self.questions .iter() .map(|q| q.1.to_dto(*q.0)) @@ -100,16 +150,29 @@ impl Store { .take(size) .collect() } - pub fn update(&mut self, id: u8, question: Question) -> Result { + pub fn update_question(&mut self, id: u8, question: Question) -> Result { if !self.questions.contains_key(&id) { return Err(format!("Question with id {} does not exists", id)); } match self.questions.insert(id, question) { Some(question) => { - self.flush(); + self.write_questions_file(); Ok(question) } None => Err("Server Error".to_string()), } } + + pub fn add_answer(&mut self, id: u8, answer: Answer) -> Result { + if self.answers.contains_key(&id) { + return Err(format!("Answer with id {} already exists", id)); + } + match self.answers.insert(id, answer.clone()) { + None => { + self.write_answers_file(); + Ok(answer) + } + _ => Err("Server Error".to_string()), + } + } }