Skip to content

Commit 9e6f9b2

Browse files
authored
feat: initial prototype from the wip branch
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
1 parent b283255 commit 9e6f9b2

File tree

13 files changed

+520
-21
lines changed

13 files changed

+520
-21
lines changed

.travis.yml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
language: rust
2+
sudo: false
3+
cache: cargo
4+
5+
rust:
6+
- nightly
7+
env:
8+
global:
9+
# XXX: begin_test_transaction doesn't play nice over threaded tests
10+
- RUST_TEST_THREADS=1
11+
- ROCKET_DATABASE_URL="mysql://[email protected]/megaphone"
12+
13+
services:
14+
- mysql
15+
16+
# XXX: kill the diesel_cli requirement:
17+
# https://docs.rs/diesel/0.16.0/diesel/macro.embed_migrations.html
18+
before_script:
19+
- mysql -e 'CREATE DATABASE IF NOT EXISTS megaphone;'
20+
- |
21+
cargo install diesel_cli --no-default-features --features mysql || \
22+
echo "diesel_cli already installed"
23+
- diesel setup --database-url $ROCKET_DATABASE_URL

Cargo.toml

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ version = "0.1.0"
44
authors = ["jrconlin <[email protected]>"]
55

66
[dependencies]
7-
diesel = { version = "1.0", features = ["mysql"] }
8-
dotenv = "0.9"
7+
diesel = { version = "1.0", features = ["mysql", "r2d2"] }
8+
failure = "0.1"
99
rocket = "0.3"
1010
rocket_codegen = "0.3"
11+
rocket_contrib = "0.3"
12+
serde = "1.0"
13+
serde_derive = "1.0"
1114
serde_json = "1.0"
12-
websocket = "0.20"

README.md

+12-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ See [API doc](https://docs.google.com/document/d/1Wxqf1a4HDkKgHDIswPmhmdvk8KPoME
55

66

77
***NOTE***: This will require:
8-
* rust nightly. See [rocket.rs Getting Started](https://rocket.rs/guide/getting-started/) for
9-
additional steps.
10-
* libmysql-dev installed
8+
9+
* rust nightly. See [rocket.rs Getting
10+
Started](https://rocket.rs/guide/getting-started/) for additional steps.
11+
* mysql
12+
* libmysqlclient installed (brew install mysql on macOS, apt-get install
13+
libmysqlclient-dev on Ubuntu)
14+
* diesel cli: (cargo install diesel_cli --no-default-features
15+
--features mysql)
16+
17+
Run:
18+
* export ROCKET_DATABASE_URL=mysql://scott:tiger@mydatabase/megaphone
19+
* $ diesel setup --database-url $ROCKET_DATABASE_URL

Rocket.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[development]
2+
#database_url = "mysql://"
3+
json_logging = false
4+
5+
[staging]
6+
#database_url = "mysql://"
7+
json_logging = true
8+
9+
[production]
10+
#database_url = "mysql://"
11+
json_logging = true

migrations/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE broadcastsv1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE broadcastsv1 (
2+
broadcaster_id VARCHAR(64) NOT NULL,
3+
bchannel_id VARCHAR(128) NOT NULL,
4+
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
5+
version VARCHAR(200) NOT NULL,
6+
PRIMARY KEY(broadcaster_id, bchannel_id)
7+
);

src/db/mod.rs

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
pub mod schema;
2+
pub mod models;
3+
4+
use std::ops::Deref;
5+
6+
use diesel::mysql::MysqlConnection;
7+
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
8+
use failure::err_msg;
9+
10+
use rocket::http::Status;
11+
use rocket::request::{self, FromRequest};
12+
use rocket::{Config, Outcome, Request, State};
13+
14+
use error::Result;
15+
16+
pub type MysqlPool = Pool<ConnectionManager<MysqlConnection>>;
17+
18+
pub fn pool_from_config(config: &Config) -> Result<MysqlPool> {
19+
let database_url = config
20+
.get_str("database_url")
21+
.map_err(|_| err_msg("ROCKET_DATABASE_URL undefined"))?
22+
.to_string();
23+
let max_size = config.get_int("database_pool_max_size").unwrap_or(10) as u32;
24+
let manager = ConnectionManager::<MysqlConnection>::new(database_url);
25+
Ok(Pool::builder().max_size(max_size).build(manager)?)
26+
}
27+
28+
pub struct Conn(pub PooledConnection<ConnectionManager<MysqlConnection>>);
29+
30+
impl Deref for Conn {
31+
type Target = MysqlConnection;
32+
33+
#[inline(always)]
34+
fn deref(&self) -> &Self::Target {
35+
&self.0
36+
}
37+
}
38+
39+
impl<'a, 'r> FromRequest<'a, 'r> for Conn {
40+
type Error = ();
41+
42+
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
43+
let pool = request.guard::<State<MysqlPool>>()?;
44+
match pool.get() {
45+
Ok(conn) => Outcome::Success(Conn(conn)),
46+
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
47+
}
48+
}
49+
}

src/db/models.rs

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use failure::ResultExt;
2+
use diesel::{replace_into, RunQueryDsl};
3+
use diesel::mysql::MysqlConnection;
4+
5+
use super::schema::broadcastsv1;
6+
use error::{HandlerErrorKind, HandlerResult};
7+
8+
#[derive(Debug, Queryable, Insertable)]
9+
#[table_name = "broadcastsv1"]
10+
pub struct Broadcast {
11+
pub broadcaster_id: String,
12+
pub bchannel_id: String,
13+
pub version: String,
14+
}
15+
16+
impl Broadcast {
17+
pub fn id(&self) -> String {
18+
format!("{}/{}", self.broadcaster_id, self.bchannel_id)
19+
}
20+
}
21+
22+
/// An authorized broadcaster
23+
pub struct Broadcaster {
24+
pub id: String,
25+
}
26+
27+
impl Broadcaster {
28+
pub fn new_broadcast(
29+
self,
30+
conn: &MysqlConnection,
31+
bchannel_id: String,
32+
version: String,
33+
) -> HandlerResult<usize> {
34+
let broadcast = Broadcast {
35+
broadcaster_id: self.id,
36+
bchannel_id: bchannel_id,
37+
version: version,
38+
};
39+
Ok(replace_into(broadcastsv1::table)
40+
.values(&broadcast)
41+
.execute(conn)
42+
.context(HandlerErrorKind::DBError)?)
43+
}
44+
}
45+
46+
// An authorized reader of current broadcasts
47+
//struct BroadcastAdmin;

src/db/schema.rs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
table! {
2+
broadcastsv1 (broadcaster_id, bchannel_id) {
3+
broadcaster_id -> Varchar,
4+
bchannel_id -> Varchar,
5+
last_updated -> Timestamp,
6+
version -> Varchar,
7+
}
8+
}

src/error.rs

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/// Error handling based on the failure crate
2+
///
3+
/// Only rocket's Handlers can render error responses w/ a contextual JSON
4+
/// payload. So request guards should generally return VALIDATION_FAILED,
5+
/// leaving error handling to the Handler (which in turn must take a Result of
6+
/// request guards' fields).
7+
///
8+
/// HandlerErrors are rocket Responders (render their own error responses).
9+
use std::fmt;
10+
use std::result;
11+
12+
use failure::{Backtrace, Context, Error, Fail};
13+
use rocket::{self, response, Request};
14+
use rocket::http::Status;
15+
use rocket::response::{Responder, Response};
16+
use rocket_contrib::Json;
17+
18+
pub type Result<T> = result::Result<T, Error>;
19+
20+
pub type HandlerResult<T> = result::Result<T, HandlerError>;
21+
22+
/// Signal a request guard failure, propagated up to the Handler to render an
23+
/// error response
24+
pub const VALIDATION_FAILED: Status = Status::InternalServerError;
25+
26+
#[derive(Debug)]
27+
pub struct HandlerError {
28+
inner: Context<HandlerErrorKind>,
29+
}
30+
31+
#[derive(Clone, Eq, PartialEq, Debug, Fail)]
32+
pub enum HandlerErrorKind {
33+
/// 401 Unauthorized
34+
#[fail(display = "Unauthorized: {}", _0)]
35+
Unauthorized(String),
36+
/// 404 Not Found
37+
#[fail(display = "Not Found")]
38+
NotFound,
39+
#[fail(display = "A database error occurred")]
40+
DBError,
41+
#[fail(display = "Version information not included in body of update")]
42+
MissingVersionDataError,
43+
#[fail(display = "Invalid Version info (must be URL safe Base 64)")]
44+
InvalidVersionDataError,
45+
#[fail(display = "Unexpected rocket error: {:?}", _0)]
46+
RocketError(rocket::Error), // rocket::Error isn't a std Error (so no #[cause])
47+
}
48+
49+
impl HandlerErrorKind {
50+
/// Return a rocket response Status to be rendered for an error
51+
pub fn http_status(&self) -> Status {
52+
match *self {
53+
HandlerErrorKind::DBError => Status::ServiceUnavailable,
54+
HandlerErrorKind::NotFound => Status::NotFound,
55+
HandlerErrorKind::Unauthorized(..) => Status::Unauthorized,
56+
_ => Status::BadRequest,
57+
}
58+
}
59+
}
60+
61+
impl Fail for HandlerError {
62+
fn cause(&self) -> Option<&Fail> {
63+
self.inner.cause()
64+
}
65+
66+
fn backtrace(&self) -> Option<&Backtrace> {
67+
self.inner.backtrace()
68+
}
69+
}
70+
71+
impl fmt::Display for HandlerError {
72+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73+
fmt::Display::fmt(&self.inner, f)
74+
}
75+
}
76+
77+
impl HandlerError {
78+
pub fn kind(&self) -> &HandlerErrorKind {
79+
self.inner.get_context()
80+
}
81+
}
82+
83+
impl From<HandlerErrorKind> for HandlerError {
84+
fn from(kind: HandlerErrorKind) -> HandlerError {
85+
HandlerError {
86+
inner: Context::new(kind),
87+
}
88+
}
89+
}
90+
91+
impl From<Context<HandlerErrorKind>> for HandlerError {
92+
fn from(inner: Context<HandlerErrorKind>) -> HandlerError {
93+
HandlerError { inner: inner }
94+
}
95+
}
96+
97+
/// Generate HTTP error responses for HandlerErrors
98+
impl<'r> Responder<'r> for HandlerError {
99+
fn respond_to(self, request: &Request) -> response::Result<'r> {
100+
let status = self.kind().http_status();
101+
let json = Json(json!({
102+
"status": status.code,
103+
"error": format!("{}", self)
104+
}));
105+
// XXX: logging
106+
Response::build_from(json.respond_to(request)?)
107+
.status(status)
108+
.ok()
109+
}
110+
}

0 commit comments

Comments
 (0)