add full support for POST /answer
This commit is contained in:
parent
ff4b5c7fd6
commit
167d34731c
1
answers.json
Normal file
1
answers.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
26
src/api.rs
26
src/api.rs
@ -14,7 +14,7 @@ pub async fn read_question(
|
|||||||
State(store): State<Arc<RwLock<Store>>>,
|
State(store): State<Arc<RwLock<Store>>>,
|
||||||
Path(id): Path<u8>,
|
Path(id): Path<u8>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match store.read().await.fetch_one(id) {
|
match store.read().await.fetch_one_question(id) {
|
||||||
Ok(question) => question.to_dto(id).into_response(),
|
Ok(question) => question.to_dto(id).into_response(),
|
||||||
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ pub async fn read_questions(
|
|||||||
let start: usize = page * size;
|
let start: usize = page * size;
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(store.read().await.fetch_many(start, size)),
|
Json(store.read().await.fetch_many_questions(start, size)),
|
||||||
)
|
)
|
||||||
.into_response()
|
.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.
|
//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
|
//QuestionDTO id then would be an option, but that makes to/from entity conversion more tricky.. todo
|
||||||
let (id, question) = question_dto.to_entity();
|
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(),
|
Ok(question) => (StatusCode::CREATED, Json(&question.to_dto(id))).into_response(),
|
||||||
Err(e) => (StatusCode::CONFLICT, e).into_response(),
|
Err(e) => (StatusCode::CONFLICT, e).into_response(),
|
||||||
}
|
}
|
||||||
@ -72,7 +72,7 @@ pub async fn update_question(
|
|||||||
Json(question_dto): Json<QuestionDTO>,
|
Json(question_dto): Json<QuestionDTO>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let (id, question) = question_dto.to_entity();
|
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(),
|
Ok(question) => question.to_dto(id).into_response(),
|
||||||
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
||||||
}
|
}
|
||||||
@ -88,20 +88,28 @@ pub async fn delete_question(
|
|||||||
State(store): State<Arc<RwLock<Store>>>,
|
State(store): State<Arc<RwLock<Store>>>,
|
||||||
Path(id): Path<u8>,
|
Path(id): Path<u8>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match store.write().await.remove(id) {
|
match store.write().await.remove_question(id) {
|
||||||
Ok(question) => question.to_dto(id).into_response(),
|
Ok(question) => question.to_dto(id).into_response(),
|
||||||
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Create an Answer - WIP
|
/// Create an Answer
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
/// `answer_dto` Form URL encoded answer DTO
|
/// `answer_dto` Form URL encoded answer DTO
|
||||||
/// # Returns
|
/// # 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(
|
pub async fn create_answer(
|
||||||
State(_store): State<Arc<RwLock<Store>>>,
|
State(store): State<Arc<RwLock<Store>>>,
|
||||||
Form(_answer_dto): Form<AnswerDTO>,
|
Form(answer_dto): Form<AnswerDTO>,
|
||||||
) -> Response {
|
) -> 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)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -40,7 +40,7 @@ async fn main() {
|
|||||||
.route("/question", post(api::create_question))
|
.route("/question", post(api::create_question))
|
||||||
.route("/question", put(api::update_question))
|
.route("/question", put(api::update_question))
|
||||||
.route("/question/:id", delete(api::delete_question))
|
.route("/question/:id", delete(api::delete_question))
|
||||||
.route("/answers", post(api::create_answer))
|
.route("/answer", post(api::create_answer))
|
||||||
.with_state(store)
|
.with_state(store)
|
||||||
.fallback(handler_404);
|
.fallback(handler_404);
|
||||||
|
|
||||||
|
105
src/store.rs
105
src/store.rs
@ -1,24 +1,30 @@
|
|||||||
/// Store is responsible for manageing the in-memory hashmap of questions by providing initialization read/write functions,
|
/// 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
|
/// and file I/O operations to persist these questions
|
||||||
/// TODO - Results returning errors should use specified types, not strings
|
/// 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::*;
|
use crate::*;
|
||||||
|
|
||||||
const QUESTIONS_DB_PATH: &str = "./questions.json";
|
const QUESTIONS_DB_PATH: &str = "./questions.json";
|
||||||
|
const ANSWERS_DB_PATH: &str = "./answers.json";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
file: File,
|
answers_file: File,
|
||||||
|
questions_file: File,
|
||||||
|
answers: HashMap<u8, Answer>,
|
||||||
questions: HashMap<u8, Question>,
|
questions: HashMap<u8, Question>,
|
||||||
// answers: HashMap<u8, Answer>, //WIP, add answers to store
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
// Upon initialization, we need to read a questions.json if it exists and populate our questions hashmap from it.
|
// Upon initialization, we need to read a questions.json ans anwers.json if they exist and populate our hashmaps from them.
|
||||||
// Otherwise we create questions.json.
|
// 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
|
// 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 {
|
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| {
|
.or_else(|e| {
|
||||||
if e.kind() == ErrorKind::AlreadyExists {
|
if e.kind() == ErrorKind::AlreadyExists {
|
||||||
File::options()
|
File::options()
|
||||||
@ -30,29 +36,53 @@ impl Store {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.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.
|
// 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<QuestionDTO> =
|
let questions_vec: Vec<QuestionDTO> =
|
||||||
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<u8, Question> = questions_vec
|
let questions: HashMap<u8, Question> = questions_vec
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|question_dto: QuestionDTO| question_dto.to_entity())
|
.map(|question_dto: QuestionDTO| question_dto.to_entity())
|
||||||
.collect();
|
.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<AnswerDTO> =
|
||||||
|
serde_json::from_str(&answers_json).expect("can't read answers.json");
|
||||||
|
let answers: HashMap<u8, Answer> = 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
|
// 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
|
// 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
|
// TODO - pretty print before writing
|
||||||
fn flush(&mut self) {
|
fn write_questions_file(&mut self) {
|
||||||
let questions: &HashMap<u8, Question> = &self.questions;
|
let questions: &HashMap<u8, Question> = &self.questions;
|
||||||
let questions_vec: Vec<QuestionDTO> = questions
|
let questions_vec: Vec<QuestionDTO> = questions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|q: (&u8, &Question)| q.1.to_dto(*q.0))
|
.map(|q: (&u8, &Question)| q.1.to_dto(*q.0))
|
||||||
.collect();
|
.collect();
|
||||||
let json: String = serde_json::to_string(&questions_vec).unwrap();
|
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
|
match f
|
||||||
.rewind()
|
.rewind()
|
||||||
.and(f.write_all(json.as_bytes()))
|
.and(f.write_all(json.as_bytes()))
|
||||||
@ -60,39 +90,59 @@ impl Store {
|
|||||||
.and(f.set_len(f.stream_position().unwrap()))
|
.and(f.set_len(f.stream_position().unwrap()))
|
||||||
{
|
{
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
_ => panic!("Could not flush file"),
|
_ => panic!("Could not write file"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, id: u8, question: Question) -> Result<Question, String> {
|
// 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<u8, Answer> = &self.answers;
|
||||||
|
let answers_vec: Vec<AnswerDTO> = 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<Question, String> {
|
||||||
if self.questions.contains_key(&id) {
|
if self.questions.contains_key(&id) {
|
||||||
return Err(format!("Question with id {} already exists", id));
|
return Err(format!("Question with id {} already exists", id));
|
||||||
}
|
}
|
||||||
match self.questions.insert(id, question.clone()) {
|
match self.questions.insert(id, question.clone()) {
|
||||||
None => {
|
None => {
|
||||||
self.flush();
|
self.write_questions_file();
|
||||||
Ok(question)
|
Ok(question)
|
||||||
} //Looks backwards, but insert must return none since key cannot already exist
|
} //Looks backwards, but insert must return none since key cannot already exist
|
||||||
_ => Err("Server Error".to_string()),
|
_ => Err("Server Error".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn remove(&mut self, id: u8) -> Result<Question, String> {
|
pub fn remove_question(&mut self, id: u8) -> Result<Question, String> {
|
||||||
match self.questions.remove(&id) {
|
match self.questions.remove(&id) {
|
||||||
Some(question) => {
|
Some(question) => {
|
||||||
self.flush();
|
self.write_questions_file();
|
||||||
Ok(question)
|
Ok(question)
|
||||||
}
|
}
|
||||||
None => Err(format!("Question with id {} does not exist", id)),
|
None => Err(format!("Question with id {} does not exist", id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn fetch_one(&self, id: u8) -> Result<Question, String> {
|
pub fn fetch_one_question(&self, id: u8) -> Result<Question, String> {
|
||||||
match self.questions.get(&id) {
|
match self.questions.get(&id) {
|
||||||
Some(question) => Ok(question.clone()),
|
Some(question) => Ok(question.clone()),
|
||||||
None => Err(format!("Question with id {} does not exists", id)),
|
None => Err(format!("Question with id {} does not exists", id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//by nature of the hashmap, pagination does not follow id order
|
//by nature of the hashmap, pagination does not follow id order
|
||||||
pub fn fetch_many(&self, start: usize, size: usize) -> Vec<QuestionDTO> {
|
pub fn fetch_many_questions(&self, start: usize, size: usize) -> Vec<QuestionDTO> {
|
||||||
self.questions
|
self.questions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|q| q.1.to_dto(*q.0))
|
.map(|q| q.1.to_dto(*q.0))
|
||||||
@ -100,16 +150,29 @@ impl Store {
|
|||||||
.take(size)
|
.take(size)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
pub fn update(&mut self, id: u8, question: Question) -> Result<Question, String> {
|
pub fn update_question(&mut self, id: u8, question: Question) -> Result<Question, String> {
|
||||||
if !self.questions.contains_key(&id) {
|
if !self.questions.contains_key(&id) {
|
||||||
return Err(format!("Question with id {} does not exists", id));
|
return Err(format!("Question with id {} does not exists", id));
|
||||||
}
|
}
|
||||||
match self.questions.insert(id, question) {
|
match self.questions.insert(id, question) {
|
||||||
Some(question) => {
|
Some(question) => {
|
||||||
self.flush();
|
self.write_questions_file();
|
||||||
Ok(question)
|
Ok(question)
|
||||||
}
|
}
|
||||||
None => Err("Server Error".to_string()),
|
None => Err("Server Error".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_answer(&mut self, id: u8, answer: Answer) -> Result<Answer, String> {
|
||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user