/// All API route handlers of the application use self::{ answer::NewAnswer, pg_store::Store, question::{NewQuestion, QuestionDTO}, }; use crate::*; const DEFAULT_PAGE: usize = 0; const DEFAULT_PAGE_SIZE: usize = 10; /// Fetches a single question using a provided id. First we fetch the question, then the questions tags, then build that into a response DTO /// # Parameters /// `id`: Path parmater Id of the question to lookup /// # Returns /// If the question is found, it converts the question to its Data Transfer Object (DTO) format and returns it. /// If the question is not found, it returns a 404 NOT_FOUND response with the error message. pub async fn read_question( State(store): State>>, Path(id): Path, ) -> Response { //First, fetch the question let question_result = store.read().await.fetch_one_question_by_id(id).await; match question_result { Some(question) => { //Then fetch the tags for this question, if there are any let tags_result = store.read().await.get_tags_for_question(question.id).await; match tags_result { Some(tags) => QuestionDTO::new(question, tags).into_response(), None => QuestionDTO::new(question, vec![]).into_response(), } } None => (StatusCode::NOT_EXTENDED).into_response(), } } /// Fetches all questions /// Load the questions, then load the tags for each question and build the json response /// # Parameters /// 'page' and 'size' as query paramateres /// # Returns /// A list of questions, empty if none exist pub async fn read_questions( State(store): State>>, Query(pagination): Query, ) -> Response { let page: usize = pagination.page.unwrap_or(DEFAULT_PAGE); let size: usize = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE); let start: usize = page * size; let questions_option = store.read().await.fetch_many_questions(start, size).await; match questions_option { Some(questions) => { let mut response_vec_dto: Vec = vec![]; for question in questions { //Not ideal - hitting the database serially for the tags of each invidual question. Can be optimized with more complex sql let tags = store .read() .await .fetch_tags_by_property(question.id, "question_id") .await .unwrap_or(vec![]); let question_dto: QuestionDTO = QuestionDTO::new(question, tags); response_vec_dto.push(question_dto) } (StatusCode::OK, Json(response_vec_dto)).into_response() } None => StatusCode::NO_CONTENT.into_response(), } } /// Creates a new question /// # Parameters /// `QuestionDTO` A JSON representation of a Question including its id /// # Returns /// Http Created 201 and the newly created question are return upon success /// Http Confilct 409 and a message are returned if a question with that `id`` already exists /// Http Unprocessable Entity 422 is returned (implicitlly) for a malformed body pub async fn create_question( State(store): State>>, Json(new_question): Json, ) -> Response { match store.write().await.add_question(new_question).await { Ok(question) => (StatusCode::CREATED, Json(&question)).into_response(), Err(e) => (StatusCode::CONFLICT, e).into_response(), } } /// Updates an existing question /// TODO: Better handle errors, responses in the tag updating flow - it is getting complex /// # Parameters /// `QuestionDTO` A JSON representation of a Question including its id /// # Returns /// Http Ok 200 and the updated question are return upon success /// Http Not Found 404 and a message are returned if a question with that `id` does not already exist /// Http Unprocessable Entity 422 is returned (implicitlly) for a malformed body pub async fn update_question( State(store): State>>, Json(question): Json, ) -> Response { // 1: Update the question entity let updated_question_result = store .write() .await .update_question(question.id, question.title, question.content) .await; match updated_question_result { Ok(updated_question) => { // 2: Get the list of tags currently associated with this question let current_tags_option = store .read() .await .get_tags_for_question(updated_question.id) .await; match current_tags_option { Some(current_tags) => { let incoming_tag_labels = question.tags; // Create (ignored if they already exist, new tags for the incoming tags) let incoming_tags = store .write() .await .add_tags(incoming_tag_labels) .await .unwrap(); // Unassociated all current tags with this question let _remove_tags = store .write() .await .unassociate_tags(updated_question.id, current_tags) .await .unwrap(); // Associated all of the incoming tags (now newly created if needed) with the question let _updated_tags = store .write() .await .associate_tags(updated_question.id, &incoming_tags) .await .unwrap(); //Return the updated question with the updated tags QuestionDTO::new(updated_question, incoming_tags).into_response() } None => (StatusCode::OK).into_response(), } } Err(e) => (StatusCode::CONFLICT, e).into_response(), } } /// Delete an existing question /// # Parameters /// `id`: Path parmater Id of the question to delete /// # Returns /// Http Ok 200 and the deleted question are return upon success /// Http Not Found 404 and a message are returned if a question with that `id` does not exist pub async fn delete_question( State(store): State>>, Path(id): Path, ) -> Response { match store.write().await.remove_question(id).await { Ok(_) => (StatusCode::OK, format!("Question with id {} deleted", id)).into_response(), Err(e) => (StatusCode::NOT_FOUND, e).into_response(), } } /// 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(new_answer): Form, ) -> Response { match store.write().await.add_answer(new_answer).await { Ok(answer) => (StatusCode::CREATED, Json(answer)).into_response(), Err(e) => (StatusCode::CONFLICT, e).into_response(), } } #[derive(Debug, Deserialize)] pub struct Pagination { page: Option, size: Option, }