From 7fa5985536a5077fda967168c72909b3e24867f7 Mon Sep 17 00:00:00 2001 From: Subhojit Paul Date: Wed, 12 Jun 2019 08:54:30 +0530 Subject: [PATCH 1/6] models for the endpoints --- src/models.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/models.rs b/src/models.rs index 27e023c..7875376 100644 --- a/src/models.rs +++ b/src/models.rs @@ -17,6 +17,37 @@ pub struct Questions { pub user_id: i32, } +/// Creates a new question. +#[derive(Insertable, Debug)] +#[table_name = "questions"] +pub struct NewQuestion { + pub title: String, + pub created: NaiveDateTime, + pub presentation_id: i32, + pub user_id: i32, +} + +impl NewQuestion { + pub fn new(title: String, created: NaiveDateTime, presentation_id: i32, user_id: i32) -> Self { + NewQuestion { + title, + created, + presentation_id, + user_id, + } + } +} + +/// The structure of the body of JSON request for creating a new question. +/// +/// This is used for making the API request, and `NewQuestion` is used by the application for +/// creating the question in database. +#[derive(Deserialize, Serialize, Debug)] +pub struct NewQuestionJson { + title: String, + presentation_id: i32, +} + #[derive(Queryable, Serialize, Deserialize, Identifiable)] #[table_name = "answers"] pub struct Answer { From d758f2f7bbfafea2061ba9ea2221ffa18b779804 Mon Sep 17 00:00:00 2001 From: Subhojit Paul Date: Wed, 12 Jun 2019 09:20:27 +0530 Subject: [PATCH 2/6] question POST endpoint init --- Cargo.lock | 1 + Cargo.toml | 1 + src/lib.rs | 2 ++ src/questions.rs | 25 +++++++++++++++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 src/questions.rs diff --git a/Cargo.lock b/Cargo.lock index 85e2b05..0f3708f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,6 +1095,7 @@ dependencies = [ name = "questionnaire-rs" version = "0.1.0" dependencies = [ + "actix 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "diesel 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 12a2e99..6d25f02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ futures = "^0.1" env_logger = "^0.6" failure = "^0.1.5" reqwest = "^0.9.14" +actix = { version = "^0.7" } diff --git a/src/lib.rs b/src/lib.rs index 4d52951..5a091fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,7 @@ extern crate reqwest; extern crate serde_json; #[macro_use] extern crate diesel; +extern crate actix; extern crate actix_web; extern crate dotenv; extern crate failure; @@ -133,6 +134,7 @@ pub mod helpers; pub mod middleware; pub mod models; pub mod presentations; +pub mod questions; pub mod schema; pub mod session; diff --git a/src/questions.rs b/src/questions.rs new file mode 100644 index 0000000..63e1e37 --- /dev/null +++ b/src/questions.rs @@ -0,0 +1,25 @@ +use actix::{Handler, Message}; +use diesel::query_dsl::RunQueryDsl; +use diesel::MysqlConnection; +use models::NewQuestion; +use DbExecutor; + +impl Message for NewQuestion { + type Result = Result<(), super::error::Db>; +} + +impl Handler for DbExecutor { + type Result = Result<(), super::error::Db>; + + fn handle(&mut self, msg: NewQuestion, _ctx: &mut Self::Context) -> Self::Result { + use schema::questions::dsl::questions; + let connection: &MysqlConnection = &self.0.get().unwrap(); + + diesel::insert_into(questions) + .values(&msg) + .execute(connection) + .expect("Error saving the question"); + + Ok(()) + } +} From 527e092feefacc6c292508091b837f09b1fae528 Mon Sep 17 00:00:00 2001 From: Subhojit Paul Date: Thu, 13 Jun 2019 09:36:54 +0530 Subject: [PATCH 3/6] questions POST --- src/lib.rs | 25 +++++++++++++++++++ src/models.rs | 4 +-- src/questions.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5a091fe..4179ca6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,28 @@ //! "created": "2019-11-01T14:30:30" //! } //! ``` +//! +//! #### ``/questions` +//! +//! **Method:** POST +//! +//! **Headers:** +//! +//! ```txt +//! Content type: application/json +//! Authorization: token +//! ``` +//! +//! **Body:** +//! +//! ```json +//! { +//! "title": "New Question", +//! "presentation_id": 1, +//! } +//! ``` +//! +//! **Response:** 200 OK extern crate chrono; extern crate env_logger; extern crate reqwest; @@ -186,4 +208,7 @@ pub fn create_app() -> App { .resource("presentations/{id}", |r| { r.method(Method::GET).with_async(presentations::get) }) + .resource("/questions", |r| { + r.method(Method::POST).with_async(questions::post) + }) } diff --git a/src/models.rs b/src/models.rs index 7875376..71a3ae0 100644 --- a/src/models.rs +++ b/src/models.rs @@ -44,8 +44,8 @@ impl NewQuestion { /// creating the question in database. #[derive(Deserialize, Serialize, Debug)] pub struct NewQuestionJson { - title: String, - presentation_id: i32, + pub title: String, + pub presentation_id: i32, } #[derive(Queryable, Serialize, Deserialize, Identifiable)] diff --git a/src/questions.rs b/src/questions.rs index 63e1e37..3fa1fea 100644 --- a/src/questions.rs +++ b/src/questions.rs @@ -1,8 +1,16 @@ use actix::{Handler, Message}; +use actix_web::middleware::session::RequestSession; +use actix_web::AsyncResponder; +use actix_web::{Error, HttpRequest, HttpResponse, Json, State}; +use chrono::Utc; use diesel::query_dsl::RunQueryDsl; use diesel::MysqlConnection; -use models::NewQuestion; -use DbExecutor; +use futures::Future; +use futures::IntoFuture; +use middleware::GitHubUserId; +use models::{NewQuestion, NewQuestionJson}; +use GH_USER_SESSION_ID_KEY; +use {AppState, DbExecutor}; impl Message for NewQuestion { type Result = Result<(), super::error::Db>; @@ -23,3 +31,54 @@ impl Handler for DbExecutor { Ok(()) } } + +/// `/questions` POST +/// +/// Headers: +/// +/// Content type: application/json +/// Authorization: token +/// +/// Body: +/// ```json +/// { +/// "title": "New Question", +/// "presentation_id": 1, +/// } +/// ``` +/// +/// Response: 200 OK +pub fn post( + data: Json, + state: State, + req: HttpRequest, +) -> Box> { + let gh_user_id_session = req + .session() + .get::(GH_USER_SESSION_ID_KEY) + .into_future(); + + let now = Utc::now(); + + gh_user_id_session + .from_err() + .and_then(move |gh_user_id| { + let input = data.into_inner(); + let new_question = NewQuestion::new( + input.title, + now.naive_utc(), + input.presentation_id, + gh_user_id.unwrap().id, + ); + + state + .db + .send(new_question) + .from_err() + .and_then(|response| match response { + Ok(_) => Ok(HttpResponse::Ok().finish()), + Err(_) => Ok(HttpResponse::InternalServerError().into()), + }) + }) + .responder() +} From b87f597ba3eafc14cbba2ec3e69c5a77e30d6460 Mon Sep 17 00:00:00 2001 From: Subhojit Paul Date: Fri, 14 Jun 2019 09:10:57 +0530 Subject: [PATCH 4/6] better error handling --- src/questions.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/questions.rs b/src/questions.rs index 3fa1fea..da81b12 100644 --- a/src/questions.rs +++ b/src/questions.rs @@ -4,6 +4,7 @@ use actix_web::AsyncResponder; use actix_web::{Error, HttpRequest, HttpResponse, Json, State}; use chrono::Utc; use diesel::query_dsl::RunQueryDsl; +use diesel::result::Error as DieselError; use diesel::MysqlConnection; use futures::Future; use futures::IntoFuture; @@ -13,11 +14,11 @@ use GH_USER_SESSION_ID_KEY; use {AppState, DbExecutor}; impl Message for NewQuestion { - type Result = Result<(), super::error::Db>; + type Result = Result<(), DieselError>; } impl Handler for DbExecutor { - type Result = Result<(), super::error::Db>; + type Result = Result<(), DieselError>; fn handle(&mut self, msg: NewQuestion, _ctx: &mut Self::Context) -> Self::Result { use schema::questions::dsl::questions; From 82461fdbb3bd4a23820687f4f98b7363657a8748 Mon Sep 17 00:00:00 2001 From: Subhojit Paul Date: Fri, 14 Jun 2019 09:34:51 +0530 Subject: [PATCH 5/6] Handler for GET questions --- src/models.rs | 6 +++++- src/questions.rs | 23 ++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/models.rs b/src/models.rs index 71a3ae0..6be77b7 100644 --- a/src/models.rs +++ b/src/models.rs @@ -6,7 +6,7 @@ use schema::presentations; use schema::questions; use serde_derive::*; -#[derive(Queryable, Identifiable, Associations)] +#[derive(Queryable, Serialize, Deserialize, Identifiable, Associations)] #[belongs_to(Presentation, foreign_key = "presentation_id")] #[table_name = "questions"] pub struct Questions { @@ -48,6 +48,10 @@ pub struct NewQuestionJson { pub presentation_id: i32, } +/// This defines an actor for retrieving question from database by id. +#[derive(Queryable, Deserialize)] +pub struct GetQuestion(pub i32); + #[derive(Queryable, Serialize, Deserialize, Identifiable)] #[table_name = "answers"] pub struct Answer { diff --git a/src/questions.rs b/src/questions.rs index da81b12..6b04f4a 100644 --- a/src/questions.rs +++ b/src/questions.rs @@ -3,13 +3,12 @@ use actix_web::middleware::session::RequestSession; use actix_web::AsyncResponder; use actix_web::{Error, HttpRequest, HttpResponse, Json, State}; use chrono::Utc; -use diesel::query_dsl::RunQueryDsl; +use diesel::prelude::*; use diesel::result::Error as DieselError; -use diesel::MysqlConnection; use futures::Future; use futures::IntoFuture; use middleware::GitHubUserId; -use models::{NewQuestion, NewQuestionJson}; +use models::{GetQuestion, NewQuestion, NewQuestionJson, Questions}; use GH_USER_SESSION_ID_KEY; use {AppState, DbExecutor}; @@ -33,6 +32,24 @@ impl Handler for DbExecutor { } } +impl Message for GetQuestion { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: GetQuestion, _ctx: &mut Self::Context) -> Self::Result { + use schema::questions::dsl::{id, questions}; + let connection: &MysqlConnection = + &self.0.get().expect("Unable to get database connection."); + + let result: Questions = questions.filter(id.eq(&msg.0)).first(connection)?; + + Ok(result) + } +} + /// `/questions` POST /// /// Headers: From a63edacb81d9964e9850f04696dc7030346bc51c Mon Sep 17 00:00:00 2001 From: Subhojit Paul Date: Mon, 17 Jun 2019 08:58:51 +0530 Subject: [PATCH 6/6] questions GET endpoint --- src/lib.rs | 25 +++++++++++++++++++++++++ src/questions.rs | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4179ca6..106278b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,6 +120,28 @@ //! ``` //! //! **Response:** 200 OK +//! +//! #### `/questions/{id}` +//! +//! **Method:** GET +//! +//! **Headers:** +//! +//! ```txt +//! Authorization: token +//! ``` +//! +//! **Response:** +//! +//! ```json +//! { +//! "id": 23, +//! "title": "New Question", +//! "created": "2019-11-01T14:30:30", +//! "presentation_id": 3, +//! "user_id": 7, +//! } +//! ``` extern crate chrono; extern crate env_logger; extern crate reqwest; @@ -211,4 +233,7 @@ pub fn create_app() -> App { .resource("/questions", |r| { r.method(Method::POST).with_async(questions::post) }) + .resource("/questions/{id}", |r| { + r.method(Method::GET).with_async(questions::get) + }) } diff --git a/src/questions.rs b/src/questions.rs index 6b04f4a..03bfd28 100644 --- a/src/questions.rs +++ b/src/questions.rs @@ -1,6 +1,6 @@ use actix::{Handler, Message}; use actix_web::middleware::session::RequestSession; -use actix_web::AsyncResponder; +use actix_web::{AsyncResponder, Path}; use actix_web::{Error, HttpRequest, HttpResponse, Json, State}; use chrono::Utc; use diesel::prelude::*; @@ -100,3 +100,37 @@ pub fn post( }) .responder() } + +/// `/questions/{id}` GET +/// +/// Headers: +/// +/// Authorization: token +/// +/// Response: +/// ```json +/// { +/// "id": 23, +/// "title": "New Question", +/// "created": "2019-11-01T14:30:30", +/// "presentation_id": 3, +/// "user_id": 7, +/// } +/// ``` +pub fn get( + data: Path, + req: HttpRequest, +) -> Box> { + let state: &AppState = req.state(); + + state + .db + .send(data.into_inner()) + .from_err() + .and_then(|response| match response { + Ok(result) => Ok(HttpResponse::Ok().json(result)), + Err(DieselError::NotFound) => Ok(HttpResponse::NotFound().into()), + Err(_) => Ok(HttpResponse::InternalServerError().into()), + }) + .responder() +}