From 9f312ba630bfe9879439f2ca7792e740424eee4f Mon Sep 17 00:00:00 2001 From: David Westgate Date: Fri, 26 Apr 2024 21:54:39 -0700 Subject: [PATCH] overhaul; break up store/questions; fix up apis; entity/dto approach --- src/api.rs | 53 ++++++++++++++------------ src/main.rs | 29 ++++++++++----- src/question.rs | 93 +++++++++++++++++----------------------------- src/questions.json | 15 ++++---- src/store.rs | 58 +++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 100 deletions(-) create mode 100644 src/store.rs diff --git a/src/api.rs b/src/api.rs index cb1104d..8a79dea 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,11 +1,7 @@ -use axum::{ - extract::{Path, State}, - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; +use crate::*; + +use self::{question::QuestionDTO, store::Store}; -use crate::question::{Question, Store}; /** GET /questions (empty body; return JSON) POST /questions (JSON body; return HTTP status code) @@ -15,34 +11,43 @@ POST /answers (www-url-encoded body; return HTTP status code) * */ pub async fn read_question(State(store): State, Path(id): Path) -> Response { - //TODO - (StatusCode::OK, " Get Questions").into_response() + match store.fetch(id) { + Ok(question) => question.to_dto(id).into_response(), + Err(e) => (StatusCode::NOT_FOUND, e).into_response(), + } } -pub async fn read_all_questions(State(store): State) -> Response { - //TODO - (StatusCode::OK, " Get Questions").into_response() +pub async fn read_questions(State(store): State) -> Response { + (StatusCode::OK, Json(store.fetch_all())).into_response() } pub async fn create_question( State(store): State, - Json(question): Json, + Json(question_dto): Json, ) -> Response { - //TODO - (StatusCode::CREATED, "Post Questions").into_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.add(id, question) { + Ok(question) => (StatusCode::CREATED, Json(&question.to_dto(id))).into_response(), + Err(e) => (StatusCode::CONFLICT, e).into_response(), + } } pub async fn update_question( State(store): State, - Path(id): Path, - Json(question): Json, + Json(question_dto): Json, ) -> Response { - //TODO - (StatusCode::CREATED, "Put Questions..").into_response() + let (id, question) = question_dto.to_entity(); + match store.update(id, question) { + Ok(question) => question.to_dto(id).into_response(), + Err(e) => (StatusCode::NOT_FOUND, e).into_response(), + } } pub async fn delete_question(State(store): State, Path(id): Path) -> Response { - //TODO - (StatusCode::OK, "Delete Questions..").into_response() + match store.remove(id) { + Ok(question) => question.to_dto(id).into_response(), + Err(e) => (StatusCode::NOT_FOUND, e).into_response(), + } } -pub async fn create_answer(State(store): State /*TODO */) -> Response { - //TODO - (StatusCode::CREATED, "Post Answers..").into_response() +pub async fn create_answer(State(_store): State /*TODO */) -> Response { + todo!() } diff --git a/src/main.rs b/src/main.rs index ac8fdb5..e7fc853 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,45 @@ mod api; -mod err; mod question; +mod store; use axum::{ + extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, routing::{delete, get, post, put}, - Router, + Json, Router, }; - +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::net::SocketAddr; +//use std::sync::Arc; +//use tokio::sync::{self,RwLock}; async fn handle() -> Response { (StatusCode::OK, "Visiting the root").into_response() } +async fn handler_404() -> Response { + (StatusCode::NOT_FOUND, "404 Not Found").into_response() +} + #[tokio::main] async fn main() { - let store = question::Store::new(); - let ip = SocketAddr::new([127, 0, 0, 1].into(), 3000); - let listener = tokio::net::TcpListener::bind(ip).await.unwrap(); - let apis = Router::new() + let store = store::Store::new(); + + 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 = Router::new() .route("/question/:id", get(api::read_question)) - .route("/questions", get(api::read_all_questions)) + .route("/questions", get(api::read_questions)) .route("/question", post(api::create_question)) .route("/question/:id", put(api::update_question)) .route("/question/:id", delete(api::delete_question)) .route("/answers", post(api::create_answer)) //.nest(path, router) .with_state(store) - .fallback(err::handler_404); - let app = Router::new().route("/", get(handle)).merge(apis); + .fallback(handler_404); + let app: Router = Router::new().route("/", get(handle)).merge(apis); axum::serve(listener, app).await.unwrap(); } diff --git a/src/question.rs b/src/question.rs index 7902539..8c387a6 100644 --- a/src/question.rs +++ b/src/question.rs @@ -1,79 +1,54 @@ -use std::collections::HashMap; +use crate::*; -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone)] -pub(crate) struct Store { - questions: HashMap, +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct QuestionDTO { + pub id: u8, + pub title: String, + pub content: String, + pub tags: Option>, } -impl Store { - pub fn new() -> Self { - Store { - questions: Self::init(), - } - } - fn init() -> HashMap { - let file = include_str!("./questions.json"); - serde_json::from_str(file).expect("can't read questions.json") - } - - pub fn add(mut self, question: Question) -> Result { - match self.questions.get(&question.id) { - Some(_) => Err(format!("Question with id {} already exists", question.id)), - None => Ok(self - .questions - .insert(question.id.clone(), question) - .unwrap()), - } - } - pub fn remove(mut self, id: u8) -> Result { - match self.questions.remove(&id) { - Some(question) => Ok(question), - None => Err(format!("Question with id {} does not exist", id)), - } - } - pub fn fetch_one(self, id: u8) -> Result { - match self.questions.get(&id) { - Some(question) => Ok(question.clone()), - None => Err(format!("Question with id {} does not exist", id)), - } - } - pub fn fetch_all(self) -> Vec { - self.questions.values().cloned().collect() - } - pub fn update(mut self, question: Question) -> Result { - match self.questions.get(&question.id) { - Some(_) => Ok(self - .questions - .insert(question.id.clone(), question) - .unwrap()), - None => Err(format!("Question with id {} does not exists", question.id)), - } +impl QuestionDTO { + pub fn to_entity(&self) -> (u8, Question) { + ( + self.id, + Question { + title: self.title.clone(), + content: self.content.clone(), + tags: self.tags.clone(), + }, + ) } } -#[derive(Deserialize, Serialize, Clone)] -pub(crate) struct Question { - id: u8, +impl IntoResponse for &QuestionDTO { + fn into_response(self) -> Response { + (StatusCode::OK, Json(&self)).into_response() + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Question { title: String, content: String, tags: Option>, } impl Question { - fn new(id: u8, title: String, content: String, tags: Option>) -> Self { + pub fn _new(_id: u8, title: String, content: String, tags: Option>) -> Self { Question { - id, 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(), + } + } } impl IntoResponse for &Question { fn into_response(self) -> Response { diff --git a/src/questions.json b/src/questions.json index 3b1e3c0..8b2b25f 100644 --- a/src/questions.json +++ b/src/questions.json @@ -1,8 +1,9 @@ -{ - "1" : { - "id": 1, - "title": "How?", - "content": "Please help!", - "tags": ["general"] +[ + { + "id": 1, + "title": "How?", + "content": "Please help!", + "tags": ["general"] + } -} \ No newline at end of file +] \ No newline at end of file diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..64e0b4c --- /dev/null +++ b/src/store.rs @@ -0,0 +1,58 @@ +use crate::*; + +use self::question::{Question, QuestionDTO}; + +#[derive(Clone, Debug)] +pub(crate) struct Store { + questions: HashMap, +} + +impl Store { + pub fn new() -> Self { + Store { + questions: Self::init(), + } + } + fn init() -> HashMap { + let file = include_str!("./questions.json"); + let a: Vec = serde_json::from_str(file).expect("can't read questions.json"); + println!("init"); + a.into_iter() + .map(|question_dto: QuestionDTO| question_dto.to_entity()) + .collect() + } + + pub fn add(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 => Ok(question), //none since key cannot already exist + _ => Err("Server Error".to_string()), + } + } + pub fn remove(mut self, id: u8) -> Result { + match self.questions.remove(&id) { + Some(question) => Ok(question), + None => Err(format!("Question with id {} does not exist", id)), + } + } + pub fn fetch(self, id: u8) -> Result { + match self.questions.get(&id) { + Some(question) => Ok(question.clone()), + None => Err(format!("Question with id {} does not exists", id)), + } + } + pub fn fetch_all(self) -> Vec { + self.questions.values().cloned().collect() + } + pub fn update(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) => Ok(question), + None => Err("Server Error".to_string()), + } + } +}