Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

questions endpoints #33

Merged
merged 6 commits into from Jun 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -15,3 +15,4 @@ futures = "^0.1"
env_logger = "^0.6"
failure = "^0.1.5"
reqwest = "^0.9.14"
actix = { version = "^0.7" }
52 changes: 52 additions & 0 deletions src/lib.rs
Expand Up @@ -98,12 +98,57 @@
//! "created": "2019-11-01T14:30:30"
//! }
//! ```
//!
//! #### ``/questions`
//!
//! **Method:** POST
//!
//! **Headers:**
//!
//! ```txt
//! Content type: application/json
//! Authorization: token <access_token>
//! ```
//!
//! **Body:**
//!
//! ```json
//! {
//! "title": "New Question",
//! "presentation_id": 1,
//! }
//! ```
//!
//! **Response:** 200 OK
//!
//! #### `/questions/{id}`
//!
//! **Method:** GET
//!
//! **Headers:**
//!
//! ```txt
//! Authorization: token <access_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;
extern crate serde_json;
#[macro_use]
extern crate diesel;
extern crate actix;
extern crate actix_web;
extern crate dotenv;
extern crate failure;
Expand Down Expand Up @@ -133,6 +178,7 @@ pub mod helpers;
pub mod middleware;
pub mod models;
pub mod presentations;
pub mod questions;
pub mod schema;
pub mod session;

Expand Down Expand Up @@ -184,4 +230,10 @@ pub fn create_app() -> App<AppState> {
.resource("presentations/{id}", |r| {
r.method(Method::GET).with_async(presentations::get)
})
.resource("/questions", |r| {
r.method(Method::POST).with_async(questions::post)
})
.resource("/questions/{id}", |r| {
r.method(Method::GET).with_async(questions::get)
})
}
37 changes: 36 additions & 1 deletion src/models.rs
Expand Up @@ -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 {
Expand All @@ -17,6 +17,41 @@ 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 {
pub title: String,
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 {
Expand Down
136 changes: 136 additions & 0 deletions src/questions.rs
@@ -0,0 +1,136 @@
use actix::{Handler, Message};
use actix_web::middleware::session::RequestSession;
use actix_web::{AsyncResponder, Path};
use actix_web::{Error, HttpRequest, HttpResponse, Json, State};
use chrono::Utc;
use diesel::prelude::*;
use diesel::result::Error as DieselError;
use futures::Future;
use futures::IntoFuture;
use middleware::GitHubUserId;
use models::{GetQuestion, NewQuestion, NewQuestionJson, Questions};
use GH_USER_SESSION_ID_KEY;
use {AppState, DbExecutor};

impl Message for NewQuestion {
type Result = Result<(), DieselError>;
}

impl Handler<NewQuestion> for DbExecutor {
type Result = Result<(), DieselError>;

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(())
}
}

impl Message for GetQuestion {
type Result = Result<Questions, DieselError>;
}

impl Handler<GetQuestion> for DbExecutor {
type Result = Result<Questions, DieselError>;

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:
///
/// Content type: application/json
/// Authorization: token <access_token>
///
/// Body:
/// ```json
/// {
/// "title": "New Question",
/// "presentation_id": 1,
/// }
/// ```
///
/// Response: 200 OK
pub fn post(
data: Json<NewQuestionJson>,
state: State<AppState>,
req: HttpRequest<AppState>,
) -> Box<Future<Item = HttpResponse, Error = Error>> {
let gh_user_id_session = req
.session()
.get::<GitHubUserId>(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()
}

/// `/questions/{id}` GET
///
/// Headers:
///
/// Authorization: token <access_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<GetQuestion>,
req: HttpRequest<AppState>,
) -> Box<Future<Item = HttpResponse, Error = Error>> {
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()
}