Skip to content

Commit

Permalink
feat: initial prototype from the wip branch
Browse files Browse the repository at this point in the history
o basic rocket setup w/ error handling via failure
o initial db setup via diesel+mysql
o skeleton of needed auth calls
o tests + travisci

Issue #4
  • Loading branch information
pjenvey authored Mar 14, 2018
1 parent b283255 commit 9e6f9b2
Show file tree
Hide file tree
Showing 13 changed files with 520 additions and 21 deletions.
23 changes: 23 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
language: rust
sudo: false
cache: cargo

rust:
- nightly
env:
global:
# XXX: begin_test_transaction doesn't play nice over threaded tests
- RUST_TEST_THREADS=1
- ROCKET_DATABASE_URL="mysql://[email protected]/megaphone"

services:
- mysql

# XXX: kill the diesel_cli requirement:
# https://docs.rs/diesel/0.16.0/diesel/macro.embed_migrations.html
before_script:
- mysql -e 'CREATE DATABASE IF NOT EXISTS megaphone;'
- |
cargo install diesel_cli --no-default-features --features mysql || \
echo "diesel_cli already installed"
- diesel setup --database-url $ROCKET_DATABASE_URL
8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ version = "0.1.0"
authors = ["jrconlin <[email protected]>"]

[dependencies]
diesel = { version = "1.0", features = ["mysql"] }
dotenv = "0.9"
diesel = { version = "1.0", features = ["mysql", "r2d2"] }
failure = "0.1"
rocket = "0.3"
rocket_codegen = "0.3"
rocket_contrib = "0.3"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
websocket = "0.20"
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ See [API doc](https://docs.google.com/document/d/1Wxqf1a4HDkKgHDIswPmhmdvk8KPoME


***NOTE***: This will require:
* rust nightly. See [rocket.rs Getting Started](https://rocket.rs/guide/getting-started/) for
additional steps.
* libmysql-dev installed

* rust nightly. See [rocket.rs Getting
Started](https://rocket.rs/guide/getting-started/) for additional steps.
* mysql
* libmysqlclient installed (brew install mysql on macOS, apt-get install
libmysqlclient-dev on Ubuntu)
* diesel cli: (cargo install diesel_cli --no-default-features
--features mysql)

Run:
* export ROCKET_DATABASE_URL=mysql://scott:tiger@mydatabase/megaphone
* $ diesel setup --database-url $ROCKET_DATABASE_URL
11 changes: 11 additions & 0 deletions Rocket.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[development]
#database_url = "mysql://"
json_logging = false

[staging]
#database_url = "mysql://"
json_logging = true

[production]
#database_url = "mysql://"
json_logging = true
Empty file added migrations/.gitkeep
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE broadcastsv1;
7 changes: 7 additions & 0 deletions migrations/2018-02-20-220249_create_broadcastsv1_table/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE broadcastsv1 (
broadcaster_id VARCHAR(64) NOT NULL,
bchannel_id VARCHAR(128) NOT NULL,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
version VARCHAR(200) NOT NULL,
PRIMARY KEY(broadcaster_id, bchannel_id)
);
49 changes: 49 additions & 0 deletions src/db/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
pub mod schema;
pub mod models;

use std::ops::Deref;

use diesel::mysql::MysqlConnection;
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use failure::err_msg;

use rocket::http::Status;
use rocket::request::{self, FromRequest};
use rocket::{Config, Outcome, Request, State};

use error::Result;

pub type MysqlPool = Pool<ConnectionManager<MysqlConnection>>;

pub fn pool_from_config(config: &Config) -> Result<MysqlPool> {
let database_url = config
.get_str("database_url")
.map_err(|_| err_msg("ROCKET_DATABASE_URL undefined"))?
.to_string();
let max_size = config.get_int("database_pool_max_size").unwrap_or(10) as u32;
let manager = ConnectionManager::<MysqlConnection>::new(database_url);
Ok(Pool::builder().max_size(max_size).build(manager)?)
}

pub struct Conn(pub PooledConnection<ConnectionManager<MysqlConnection>>);

impl Deref for Conn {
type Target = MysqlConnection;

#[inline(always)]
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<'a, 'r> FromRequest<'a, 'r> for Conn {
type Error = ();

fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
let pool = request.guard::<State<MysqlPool>>()?;
match pool.get() {
Ok(conn) => Outcome::Success(Conn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
}
}
}
47 changes: 47 additions & 0 deletions src/db/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use failure::ResultExt;
use diesel::{replace_into, RunQueryDsl};
use diesel::mysql::MysqlConnection;

use super::schema::broadcastsv1;
use error::{HandlerErrorKind, HandlerResult};

#[derive(Debug, Queryable, Insertable)]
#[table_name = "broadcastsv1"]
pub struct Broadcast {
pub broadcaster_id: String,
pub bchannel_id: String,
pub version: String,
}

impl Broadcast {
pub fn id(&self) -> String {
format!("{}/{}", self.broadcaster_id, self.bchannel_id)
}
}

/// An authorized broadcaster
pub struct Broadcaster {
pub id: String,
}

impl Broadcaster {
pub fn new_broadcast(
self,
conn: &MysqlConnection,
bchannel_id: String,
version: String,
) -> HandlerResult<usize> {
let broadcast = Broadcast {
broadcaster_id: self.id,
bchannel_id: bchannel_id,
version: version,
};
Ok(replace_into(broadcastsv1::table)
.values(&broadcast)
.execute(conn)
.context(HandlerErrorKind::DBError)?)
}
}

// An authorized reader of current broadcasts
//struct BroadcastAdmin;
8 changes: 8 additions & 0 deletions src/db/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
table! {
broadcastsv1 (broadcaster_id, bchannel_id) {
broadcaster_id -> Varchar,
bchannel_id -> Varchar,
last_updated -> Timestamp,
version -> Varchar,
}
}
110 changes: 110 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/// Error handling based on the failure crate
///
/// Only rocket's Handlers can render error responses w/ a contextual JSON
/// payload. So request guards should generally return VALIDATION_FAILED,
/// leaving error handling to the Handler (which in turn must take a Result of
/// request guards' fields).
///
/// HandlerErrors are rocket Responders (render their own error responses).
use std::fmt;
use std::result;

use failure::{Backtrace, Context, Error, Fail};
use rocket::{self, response, Request};
use rocket::http::Status;
use rocket::response::{Responder, Response};
use rocket_contrib::Json;

pub type Result<T> = result::Result<T, Error>;

pub type HandlerResult<T> = result::Result<T, HandlerError>;

/// Signal a request guard failure, propagated up to the Handler to render an
/// error response
pub const VALIDATION_FAILED: Status = Status::InternalServerError;

#[derive(Debug)]
pub struct HandlerError {
inner: Context<HandlerErrorKind>,
}

#[derive(Clone, Eq, PartialEq, Debug, Fail)]
pub enum HandlerErrorKind {
/// 401 Unauthorized
#[fail(display = "Unauthorized: {}", _0)]
Unauthorized(String),
/// 404 Not Found
#[fail(display = "Not Found")]
NotFound,
#[fail(display = "A database error occurred")]
DBError,
#[fail(display = "Version information not included in body of update")]
MissingVersionDataError,
#[fail(display = "Invalid Version info (must be URL safe Base 64)")]
InvalidVersionDataError,
#[fail(display = "Unexpected rocket error: {:?}", _0)]
RocketError(rocket::Error), // rocket::Error isn't a std Error (so no #[cause])
}

impl HandlerErrorKind {
/// Return a rocket response Status to be rendered for an error
pub fn http_status(&self) -> Status {
match *self {
HandlerErrorKind::DBError => Status::ServiceUnavailable,
HandlerErrorKind::NotFound => Status::NotFound,
HandlerErrorKind::Unauthorized(..) => Status::Unauthorized,
_ => Status::BadRequest,
}
}
}

impl Fail for HandlerError {
fn cause(&self) -> Option<&Fail> {
self.inner.cause()
}

fn backtrace(&self) -> Option<&Backtrace> {
self.inner.backtrace()
}
}

impl fmt::Display for HandlerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.inner, f)
}
}

impl HandlerError {
pub fn kind(&self) -> &HandlerErrorKind {
self.inner.get_context()
}
}

impl From<HandlerErrorKind> for HandlerError {
fn from(kind: HandlerErrorKind) -> HandlerError {
HandlerError {
inner: Context::new(kind),
}
}
}

impl From<Context<HandlerErrorKind>> for HandlerError {
fn from(inner: Context<HandlerErrorKind>) -> HandlerError {
HandlerError { inner: inner }
}
}

/// Generate HTTP error responses for HandlerErrors
impl<'r> Responder<'r> for HandlerError {
fn respond_to(self, request: &Request) -> response::Result<'r> {
let status = self.kind().http_status();
let json = Json(json!({
"status": status.code,
"error": format!("{}", self)
}));
// XXX: logging
Response::build_from(json.respond_to(request)?)
.status(status)
.ok()
}
}
Loading

0 comments on commit 9e6f9b2

Please sign in to comment.