overhaul; break up store/questions; fix up apis; entity/dto approach

This commit is contained in:
David Westgate 2024-04-26 21:54:39 -07:00
parent 0c685d755c
commit 9f312ba630
5 changed files with 148 additions and 100 deletions

View File

@ -1,11 +1,7 @@
use axum::{ use crate::*;
extract::{Path, State},
http::StatusCode, use self::{question::QuestionDTO, store::Store};
response::{IntoResponse, Response},
Json,
};
use crate::question::{Question, Store};
/** /**
GET /questions (empty body; return JSON) GET /questions (empty body; return JSON)
POST /questions (JSON body; return HTTP status code) 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<Store>, Path(id): Path<u8>) -> Response { pub async fn read_question(State(store): State<Store>, Path(id): Path<u8>) -> Response {
//TODO match store.fetch(id) {
(StatusCode::OK, " Get Questions").into_response() 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<Store>) -> Response { pub async fn read_questions(State(store): State<Store>) -> Response {
//TODO (StatusCode::OK, Json(store.fetch_all())).into_response()
(StatusCode::OK, " Get Questions").into_response()
} }
pub async fn create_question( pub async fn create_question(
State(store): State<Store>, State(store): State<Store>,
Json(question): Json<Question>, Json(question_dto): Json<QuestionDTO>,
) -> Response { ) -> Response {
//TODO //Normally, the server should generate the id, user provided id's (and the whole request) should be rejected.
(StatusCode::CREATED, "Post Questions").into_response() //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( pub async fn update_question(
State(store): State<Store>, State(store): State<Store>,
Path(id): Path<u8>, Json(question_dto): Json<QuestionDTO>,
Json(question): Json<Question>,
) -> Response { ) -> Response {
//TODO let (id, question) = question_dto.to_entity();
(StatusCode::CREATED, "Put Questions..").into_response() 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<Store>, Path(id): Path<u8>) -> Response { pub async fn delete_question(State(store): State<Store>, Path(id): Path<u8>) -> Response {
//TODO match store.remove(id) {
(StatusCode::OK, "Delete Questions..").into_response() Ok(question) => question.to_dto(id).into_response(),
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
}
} }
pub async fn create_answer(State(store): State<Store> /*TODO */) -> Response { pub async fn create_answer(State(_store): State<Store> /*TODO */) -> Response {
//TODO todo!()
(StatusCode::CREATED, "Post Answers..").into_response()
} }

View File

@ -1,36 +1,45 @@
mod api; mod api;
mod err;
mod question; mod question;
mod store;
use axum::{ use axum::{
extract::{Path, State},
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing::{delete, get, post, put}, routing::{delete, get, post, put},
Router, Json, Router,
}; };
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
//use std::sync::Arc;
//use tokio::sync::{self,RwLock};
async fn handle() -> Response { async fn handle() -> Response {
(StatusCode::OK, "Visiting the root").into_response() (StatusCode::OK, "Visiting the root").into_response()
} }
async fn handler_404() -> Response {
(StatusCode::NOT_FOUND, "404 Not Found").into_response()
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let store = question::Store::new(); let store = store::Store::new();
let ip = SocketAddr::new([127, 0, 0, 1].into(), 3000);
let listener = tokio::net::TcpListener::bind(ip).await.unwrap(); let ip: SocketAddr = SocketAddr::new([127, 0, 0, 1].into(), 3000);
let apis = Router::new() 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("/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", post(api::create_question))
.route("/question/:id", put(api::update_question)) .route("/question/:id", 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("/answers", post(api::create_answer))
//.nest(path, router) //.nest(path, router)
.with_state(store) .with_state(store)
.fallback(err::handler_404); .fallback(handler_404);
let app = Router::new().route("/", get(handle)).merge(apis); let app: Router = Router::new().route("/", get(handle)).merge(apis);
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }

View File

@ -1,79 +1,54 @@
use std::collections::HashMap; use crate::*;
use axum::{ #[derive(Deserialize, Serialize, Clone, Debug)]
http::StatusCode, pub struct QuestionDTO {
response::{IntoResponse, Response}, pub id: u8,
Json, pub title: String,
}; pub content: String,
use serde::{Deserialize, Serialize}; pub tags: Option<Vec<String>>,
#[derive(Clone)]
pub(crate) struct Store {
questions: HashMap<u8, Question>,
} }
impl Store { impl QuestionDTO {
pub fn new() -> Self { pub fn to_entity(&self) -> (u8, Question) {
Store { (
questions: Self::init(), self.id,
} Question {
} title: self.title.clone(),
fn init() -> HashMap<u8, Question> { content: self.content.clone(),
let file = include_str!("./questions.json"); tags: self.tags.clone(),
serde_json::from_str(file).expect("can't read questions.json") },
} )
pub fn add(mut self, question: Question) -> Result<Question, String> {
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<Question, String> {
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<Question, String> {
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<Question> {
self.questions.values().cloned().collect()
}
pub fn update(mut self, question: Question) -> Result<Question, String> {
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)),
}
} }
} }
#[derive(Deserialize, Serialize, Clone)] impl IntoResponse for &QuestionDTO {
pub(crate) struct Question { fn into_response(self) -> Response {
id: u8, (StatusCode::OK, Json(&self)).into_response()
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Question {
title: String, title: String,
content: String, content: String,
tags: Option<Vec<String>>, tags: Option<Vec<String>>,
} }
impl Question { impl Question {
fn new(id: u8, title: String, content: String, tags: Option<Vec<String>>) -> Self { pub fn _new(_id: u8, title: String, content: String, tags: Option<Vec<String>>) -> Self {
Question { Question {
id,
title, title,
content, content,
tags, 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 { impl IntoResponse for &Question {
fn into_response(self) -> Response { fn into_response(self) -> Response {

View File

@ -1,8 +1,9 @@
{ [
"1" : { {
"id": 1, "id": 1,
"title": "How?", "title": "How?",
"content": "Please help!", "content": "Please help!",
"tags": ["general"] "tags": ["general"]
} }
} ]

58
src/store.rs Normal file
View File

@ -0,0 +1,58 @@
use crate::*;
use self::question::{Question, QuestionDTO};
#[derive(Clone, Debug)]
pub(crate) struct Store {
questions: HashMap<u8, Question>,
}
impl Store {
pub fn new() -> Self {
Store {
questions: Self::init(),
}
}
fn init() -> HashMap<u8, Question> {
let file = include_str!("./questions.json");
let a: Vec<QuestionDTO> = 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<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 => Ok(question), //none since key cannot already exist
_ => Err("Server Error".to_string()),
}
}
pub fn remove(mut self, id: u8) -> Result<Question, String> {
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<Question, String> {
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<Question> {
self.questions.values().cloned().collect()
}
pub fn update(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) => Ok(question),
None => Err("Server Error".to_string()),
}
}
}