This repository has been archived on 2025-04-28. You can view files and clone it, but cannot push or open issues or pull requests.
rust-web/src/api.rs
2024-06-01 19:50:34 -07:00

185 lines
7.3 KiB
Rust

/// 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<Arc<RwLock<Store>>>,
Path(id): Path<u8>,
) -> 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<Arc<RwLock<Store>>>,
Query(pagination): Query<Pagination>,
) -> 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<QuestionDTO> = 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<Arc<RwLock<Store>>>,
Json(new_question): Json<NewQuestion>,
) -> 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<Arc<RwLock<Store>>>,
Json(question): Json<QuestionDTO>,
) -> 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<Arc<RwLock<Store>>>,
Path(id): Path<u8>,
) -> 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<Arc<RwLock<Store>>>,
Form(new_answer): Form<NewAnswer>,
) -> 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<usize>,
size: Option<usize>,
}