checkpoint for postgres. 70% there
This commit is contained in:
parent
d64580df0e
commit
8678106283
@ -2,4 +2,3 @@
|
||||
POSTGRES_USER = "postgres"
|
||||
POSTGRES_PASSWORD = "DB_PASS_HERE"
|
||||
POSTGRES_HOST = "localhost"
|
||||
POSTGRES_DBNAME = "questionanswer"
|
74
src/api.rs
74
src/api.rs
@ -1,10 +1,14 @@
|
||||
/// All API route handlers of the application
|
||||
use self::{answer::AnswerDTO, question::QuestionDTO, json_store::Store};
|
||||
use self::{
|
||||
answer::AnswerDTO,
|
||||
pg_store::Store,
|
||||
question::{NewQuestion, Question, QuestionDTO},
|
||||
};
|
||||
use crate::*;
|
||||
const DEFAULT_PAGE: usize = 0;
|
||||
const DEFAULT_PAGE_SIZE: usize = 10;
|
||||
|
||||
/// Fetches a single question using a provided id
|
||||
/// 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
|
||||
@ -14,13 +18,24 @@ pub async fn read_question(
|
||||
State(store): State<Arc<RwLock<Store>>>,
|
||||
Path(id): Path<u8>,
|
||||
) -> Response {
|
||||
match store.read().await.fetch_one_question(id) {
|
||||
Ok(question) => question.to_dto(id).into_response(),
|
||||
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
||||
match store.read().await.fetch_one_question_by_id(id).await {
|
||||
Some(question) => {
|
||||
// We could do this in one DB hit with a purpose built store function and different underlying structures, but two hits isn't that bad.
|
||||
let tags = store
|
||||
.read()
|
||||
.await
|
||||
.fetch_tags_by_property(question.id, "question_id")
|
||||
.await
|
||||
.unwrap_or(vec![]);
|
||||
let response_dto: QuestionDTO = QuestionDTO::new(question, tags);
|
||||
response_dto.into_response()
|
||||
}
|
||||
None => (StatusCode::NOT_FOUND).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
|
||||
@ -32,11 +47,25 @@ pub async fn read_questions(
|
||||
let page: usize = pagination.page.unwrap_or(DEFAULT_PAGE);
|
||||
let size: usize = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE);
|
||||
let start: usize = page * size;
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(store.read().await.fetch_many_questions(start, size)),
|
||||
)
|
||||
.into_response()
|
||||
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
|
||||
@ -48,19 +77,21 @@ pub async fn read_questions(
|
||||
/// Http Unprocessable Entity 422 is returned (implicitlly) for a malformed body
|
||||
pub async fn create_question(
|
||||
State(store): State<Arc<RwLock<Store>>>,
|
||||
Json(question_dto): Json<QuestionDTO>,
|
||||
Json(new_question): Json<NewQuestion>,
|
||||
) -> Response {
|
||||
//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_question(id, question) {
|
||||
Ok(question) => (StatusCode::CREATED, Json(&question.to_dto(id))).into_response(),
|
||||
//Step 1: See which of supplied tags already exists
|
||||
//Step 2: Create the non-existing tags
|
||||
//Step 3: Create the question
|
||||
//Step 4: Create the join listing
|
||||
//Step 5: Return result
|
||||
//Step 6: Ponder the use of ORMs
|
||||
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
|
||||
/// At present, questions cannot be 'partially updated' - all fields must be included
|
||||
/// # Parameters
|
||||
/// `QuestionDTO` A JSON representation of a Question including its id
|
||||
/// # Returns
|
||||
@ -69,11 +100,10 @@ pub async fn create_question(
|
||||
/// Http Unprocessable Entity 422 is returned (implicitlly) for a malformed body
|
||||
pub async fn update_question(
|
||||
State(store): State<Arc<RwLock<Store>>>,
|
||||
Json(question_dto): Json<QuestionDTO>,
|
||||
Json(question): Json<Question>,
|
||||
) -> Response {
|
||||
let (id, question) = question_dto.to_entity();
|
||||
match store.write().await.update_question(id, question) {
|
||||
Ok(question) => question.to_dto(id).into_response(),
|
||||
match store.write().await.update_question(question) {
|
||||
Ok(question) => question.into_response(),
|
||||
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
||||
}
|
||||
}
|
||||
@ -89,7 +119,7 @@ pub async fn delete_question(
|
||||
Path(id): Path<u8>,
|
||||
) -> Response {
|
||||
match store.write().await.remove_question(id) {
|
||||
Ok(question) => question.to_dto(id).into_response(),
|
||||
Ok(question) => question.into_response(),
|
||||
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
||||
}
|
||||
}
|
||||
|
@ -1,178 +0,0 @@
|
||||
/// 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::{
|
||||
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 {
|
||||
answers_file: File,
|
||||
questions_file: File,
|
||||
answers: HashMap<u8, Answer>,
|
||||
questions: HashMap<u8, Question>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
// 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 questions_file: File = File::create_new(QUESTIONS_DB_PATH)
|
||||
.or_else(|e| {
|
||||
if e.kind() == ErrorKind::AlreadyExists {
|
||||
File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(QUESTIONS_DB_PATH)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
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<QuestionDTO> =
|
||||
serde_json::from_str(&questions_json).expect("can't read questions.json");
|
||||
let questions: HashMap<u8, Question> = questions_vec
|
||||
.into_iter()
|
||||
.map(|question_dto: QuestionDTO| question_dto.to_entity())
|
||||
.collect();
|
||||
|
||||
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
|
||||
// Not the most efficient approach if we are just adding or deleting a single question, but it does the job at our current scale
|
||||
// TODO - pretty print before writing
|
||||
fn write_questions_file(&mut self) {
|
||||
let questions: &HashMap<u8, Question> = &self.questions;
|
||||
let questions_vec: Vec<QuestionDTO> = 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.questions_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"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return Err(format!("Question with id {} already exists", id));
|
||||
}
|
||||
match self.questions.insert(id, question.clone()) {
|
||||
None => {
|
||||
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_question(&mut self, id: u8) -> Result<Question, String> {
|
||||
match self.questions.remove(&id) {
|
||||
Some(question) => {
|
||||
self.write_questions_file();
|
||||
Ok(question)
|
||||
}
|
||||
None => Err(format!("Question with id {} does not exist", id)),
|
||||
}
|
||||
}
|
||||
pub fn fetch_one_question(&self, id: u8) -> Result<Question, String> {
|
||||
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_questions(&self, start: usize, size: usize) -> Vec<QuestionDTO> {
|
||||
self.questions
|
||||
.iter()
|
||||
.map(|q| q.1.to_dto(*q.0))
|
||||
.skip(start)
|
||||
.take(size)
|
||||
.collect()
|
||||
}
|
||||
pub fn update_question(&mut self, id: u8, question: Question) -> Result<Question, String> {
|
||||
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.write_questions_file();
|
||||
Ok(question)
|
||||
}
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
37
src/main.rs
37
src/main.rs
@ -1,7 +1,9 @@
|
||||
mod answer;
|
||||
mod api;
|
||||
mod pg_store;
|
||||
mod question;
|
||||
mod json_store;
|
||||
mod question_tag;
|
||||
mod tag;
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::{StatusCode, Uri},
|
||||
@ -12,19 +14,10 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
self,
|
||||
postgres::{PgPool, Postgres},
|
||||
Pool,
|
||||
postgres::{PgPool, PgPoolOptions, PgRow, Postgres},
|
||||
Pool, Row,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var,
|
||||
fs::File,
|
||||
io::{ErrorKind, Read, Seek, Write},
|
||||
net::SocketAddr,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use json_store::Store;
|
||||
use std::{env::var, fs::File, io::Read, net::SocketAddr, path::PathBuf, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
// generic handler for any not supported route/method combination
|
||||
@ -54,20 +47,21 @@ async fn serve_file(uri: Uri) -> impl IntoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
async fn db_connection() -> Result<Pool<Postgres>, sqlx::Error> {
|
||||
let url: String = format!(
|
||||
"postgres://{}:{}@{}:5432"/*{}*/,
|
||||
//We could create/select a particular database with the URL path, but this would complicate setup, add another env, and provides little value
|
||||
fn db_string() -> String {
|
||||
format!(
|
||||
"postgres://{}:{}@{}:5432",
|
||||
var("POSTGRES_USER").unwrap(),
|
||||
var("POSTGRES_PASSWORD").unwrap(),
|
||||
var("POSTGRES_HOST").unwrap(),
|
||||
// var("POSTGRES_DBNAME").unwrap()
|
||||
);
|
||||
PgPool::connect(&url).await
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let store: Arc<RwLock<Store>> = Arc::new(RwLock::new(json_store::Store::new()));
|
||||
let db_url: String = db_string();
|
||||
let store: Arc<RwLock<pg_store::Store>> =
|
||||
Arc::new(RwLock::new(pg_store::Store::new(&db_url).await));
|
||||
let ip: SocketAddr = SocketAddr::new([127, 0, 0, 1].into(), 3000);
|
||||
let listener: tokio::net::TcpListener = tokio::net::TcpListener::bind(ip).await.unwrap();
|
||||
let apis = Router::new()
|
||||
@ -83,7 +77,6 @@ async fn main() {
|
||||
.route("/", get(serve_file))
|
||||
.route("/*path", get(serve_file))
|
||||
.fallback(handler_404);
|
||||
let db: Pool<Postgres> = db_connection().await.unwrap();
|
||||
sqlx::migrate!().run(&db).await.unwrap();
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
176
src/pg_store.rs
Normal file
176
src/pg_store.rs
Normal file
@ -0,0 +1,176 @@
|
||||
/// 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::{
|
||||
answer::Answer,
|
||||
question::{NewQuestion, Question},
|
||||
question_tag::QuestionTag,
|
||||
tag::Tag,
|
||||
};
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Store {
|
||||
pub connection: PgPool,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Helper to deal with unwrapping postgres fields as u8 which we want. Not gracefull if you pass in the wrong field name - so do not do that
|
||||
fn id_to_u8(pg_row: &PgRow, id_name: &str) -> u8 {
|
||||
pg_row.try_get::<i16, _>(id_name).unwrap() as u8
|
||||
}
|
||||
|
||||
pub async fn new(db_url: &str) -> Self {
|
||||
let db_pool: Pool<Postgres> = match PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(db_url)
|
||||
.await
|
||||
{
|
||||
Ok(pool) => pool,
|
||||
Err(_) => panic!("Could not estable database connection"),
|
||||
};
|
||||
sqlx::migrate!().run(&db_pool).await.unwrap();
|
||||
Store {
|
||||
connection: db_pool,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn _remove_question_tag(
|
||||
&mut self,
|
||||
question_id: u8,
|
||||
tag_id: u8,
|
||||
) -> Result<bool, String> {
|
||||
let result = sqlx::query("DELETE FROM question_tag WHERE question_id = $1 AND tag_id = $2")
|
||||
.bind(question_id.to_string())
|
||||
.bind(tag_id.to_string())
|
||||
.execute(&self.connection)
|
||||
.await;
|
||||
match result {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
pub async fn _add_question_tag(
|
||||
&mut self,
|
||||
question_id: u8,
|
||||
tag_id: u8,
|
||||
) -> Result<QuestionTag, String> {
|
||||
let result =
|
||||
sqlx::query("INSERT INTO question_tag VALUES ($1,$2) RETURNING question_id, tag_id")
|
||||
.bind(question_id.to_string())
|
||||
.bind(tag_id.to_string())
|
||||
.fetch_one(&self.connection)
|
||||
.await;
|
||||
match result {
|
||||
Ok(pg_row) => Ok(QuestionTag::new(
|
||||
Store::id_to_u8(&pg_row, "question_id"),
|
||||
Store::id_to_u8(&pg_row, "tag_id"),
|
||||
)),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn _add_tags(&mut self, tag_labels: Vec<String>) -> Result<Vec<Tag>, String> {
|
||||
let insert_values: Vec<String> = tag_labels
|
||||
.iter()
|
||||
.map(|label| format!("({}),", label))
|
||||
.collect();
|
||||
|
||||
let result = sqlx::query("INSERT INTO tags (label) VALUES $1 RETURNING id, label")
|
||||
.bind(insert_values)
|
||||
.fetch_all(&self.connection)
|
||||
.await;
|
||||
match result {
|
||||
Ok(pg_rows) => Ok(pg_rows
|
||||
.iter()
|
||||
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
|
||||
.collect()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: property should be a strong type
|
||||
pub async fn fetch_tags_by_property(
|
||||
&self,
|
||||
propert_id: u8,
|
||||
property_type: &str,
|
||||
) -> Option<Vec<Tag>> {
|
||||
let result = sqlx::query("SELECT * FROM tags WHERE $1 = $2")
|
||||
.bind(property_type)
|
||||
.bind(propert_id.to_string())
|
||||
.fetch_all(&self.connection)
|
||||
.await;
|
||||
match result {
|
||||
Ok(tag_rows) => {
|
||||
let tags: Vec<Tag> = tag_rows
|
||||
.iter()
|
||||
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
|
||||
.collect();
|
||||
Some(tags)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_question(&mut self, new_question: NewQuestion) -> Result<Question, String> {
|
||||
let result: Result<PgRow, sqlx::Error> = sqlx::query("INSERT INTO questions (title, content, tags) VALUES ($1, $2, $3) RETURNING id, title, content, tags")
|
||||
.bind(new_question.title).bind(new_question.content).bind(new_question.tags).fetch_one(&self.connection).await;
|
||||
match result {
|
||||
Ok(pg_row) => Ok(Question::new(
|
||||
Store::id_to_u8(&pg_row, "id"),
|
||||
pg_row.get("title"),
|
||||
pg_row.get("content"),
|
||||
)),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_question(&mut self, _id: u8) -> Result<Question, String> {
|
||||
Err("To Do".to_string())
|
||||
}
|
||||
|
||||
pub async fn fetch_one_question_by_id(&self, id: u8) -> Option<Question> {
|
||||
let row_result = sqlx::query("SELECT * FROM questions WHERE id = $1")
|
||||
.bind(id.to_string())
|
||||
.fetch_one(&self.connection)
|
||||
.await;
|
||||
match row_result {
|
||||
Ok(pg_row) => Some(Question::new(
|
||||
Store::id_to_u8(&pg_row, "id"),
|
||||
pg_row.get("title"),
|
||||
pg_row.get("content"),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_many_questions(&self, start: usize, size: usize) -> Option<Vec<Question>> {
|
||||
let rows_result: Result<Vec<PgRow>, sqlx::Error> =
|
||||
sqlx::query("SELECT * FROM questions ORDER BY id LIMIT $1 OFFSET $2")
|
||||
.bind(size.to_string())
|
||||
.bind(start.to_string())
|
||||
.fetch_all(&self.connection)
|
||||
.await;
|
||||
match rows_result {
|
||||
Ok(pg_rows) => {
|
||||
let mut result: Vec<Question> = vec![];
|
||||
for pg_row in pg_rows {
|
||||
result.push(Question::new(
|
||||
Store::id_to_u8(&pg_row, "id"),
|
||||
pg_row.get("title"),
|
||||
pg_row.get("content"),
|
||||
))
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn update_question(&mut self, _question: Question) -> Result<Question, String> {
|
||||
Err("To Do".to_string())
|
||||
}
|
||||
|
||||
pub fn add_answer(&mut self, _id: u8, _answer: Answer) -> Result<Answer, String> {
|
||||
Err("To Do".to_string())
|
||||
}
|
||||
}
|
@ -1,25 +1,30 @@
|
||||
use tag::Tag;
|
||||
|
||||
/// Contains struct definitions regarding questions
|
||||
use crate::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct NewQuestion {
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct QuestionDTO {
|
||||
pub id: u8,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// Question Data Transfer Object, a representation of the expected serialized JSON formated of questions regarding requests, responses, and our question json file
|
||||
impl QuestionDTO {
|
||||
pub fn to_entity(&self) -> (u8, Question) {
|
||||
(
|
||||
self.id,
|
||||
Question {
|
||||
title: self.title.clone(),
|
||||
content: self.content.clone(),
|
||||
tags: self.tags.clone(),
|
||||
},
|
||||
)
|
||||
pub fn new(question: Question, tags: Vec<Tag>) -> Self {
|
||||
QuestionDTO {
|
||||
id: question.id,
|
||||
title: question.title,
|
||||
content: question.content,
|
||||
tags: tags.iter().map(|tag| tag.label.clone()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl IntoResponse for &QuestionDTO {
|
||||
@ -28,29 +33,16 @@ impl IntoResponse for &QuestionDTO {
|
||||
}
|
||||
}
|
||||
|
||||
/// Question 'entity' used for in-memory interactions of questions by the store
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Question {
|
||||
title: String,
|
||||
content: String,
|
||||
tags: Option<Vec<String>>,
|
||||
pub id: u8,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl Question {
|
||||
pub fn _new(_id: u8, title: String, content: String, tags: Option<Vec<String>>) -> Self {
|
||||
Question {
|
||||
title,
|
||||
content,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
pub fn to_dto(&self, id: u8) -> QuestionDTO {
|
||||
QuestionDTO {
|
||||
id,
|
||||
title: self.title.clone(),
|
||||
content: self.content.clone(),
|
||||
tags: self.tags.clone(),
|
||||
}
|
||||
pub fn new(id: u8, title: String, content: String) -> Self {
|
||||
Question { id, title, content }
|
||||
}
|
||||
}
|
||||
impl IntoResponse for &Question {
|
||||
|
16
src/question_tag.rs
Normal file
16
src/question_tag.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct QuestionTag {
|
||||
pub question_id: u8,
|
||||
pub tag_id: u8,
|
||||
}
|
||||
|
||||
impl QuestionTag {
|
||||
pub fn new(question_id: u8, tag_id: u8) -> Self {
|
||||
QuestionTag {
|
||||
question_id,
|
||||
tag_id,
|
||||
}
|
||||
}
|
||||
}
|
13
src/tag.rs
Normal file
13
src/tag.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Tag {
|
||||
pub id: u8,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
pub fn new(id: u8, label: String) -> Self {
|
||||
Tag { id, label }
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user