#[cfg_attr(not(test), macro_use)]
extern crate nickel;
extern crate hyper;
extern crate serde;
extern crate serde_json;
extern crate serde_derive;
use nickel::{Nickel, ListeningServer, HttpRouter, JsonBody, Request, Response, MiddlewareResult};
use nickel::status::StatusCode;
use serde_derive::{Serialize, Deserialize};
use std::error::Error as StdError;
use std::{env, str};
use std::sync::atomic::{self, AtomicUsize};
trait Database : Send + Sync + 'static {
fn get_users(&self) -> Vec<String>;
}
impl Database for Vec<String> {
fn get_users(&self) -> Vec<String> {
self.clone()
}
}
struct ServerData {
hits: AtomicUsize,
database: Box<Database>
}
impl ServerData {
fn hitcount(&self) -> usize {
self.hits.load(atomic::Ordering::Relaxed)
}
fn log_hit(&self) -> usize {
self.hits.fetch_add(1, atomic::Ordering::Relaxed)
}
fn get_users(&self) -> Vec<String> {
self.database.get_users()
}
}
fn main() {
let port = env::var("PORT").map(|s| s.parse().unwrap()).unwrap_or(3000);
let address = &format!("0.0.0.0:{}", port);
let database = vec![];
start_server(address, database).unwrap();
}
fn log_hit<'mw, 'conn>(_req: &mut Request<'mw, 'conn, ServerData>, res: Response<'mw, ServerData>) -> MiddlewareResult<'mw, ServerData> {
res.data().log_hit();
return res.next_middleware()
}
fn start_server<D>(address: &str, database: D) -> Result<ListeningServer, Box<StdError>>
where D: Database {
let server_data = ServerData {
hits: AtomicUsize::new(0),
database: Box::new(database),
};
let mut server = Nickel::with_data(server_data);
server.utilize(log_hit);
server.get("/", middleware!( "Hello World" ));
server.get("/users", middleware! { |_, res| <ServerData>
let users = res.data().get_users();
serde_json::from_str::<serde_json::Value>(&format!(r#"{{ "users": {:?} }}"#, users)).unwrap()
});
server.post("/", middleware! { |req, res|
#[derive(Serialize, Deserialize)]
struct Data { name: String, age: Option<u32> }
let client = try_with!(res, req.json_as::<Data>().map_err(|e| (StatusCode::BadRequest, e)));
match client.age {
Some(age) => {
serde_json::from_str::<serde_json::Value>(&format!(
r#"{{ "message": "Hello {}, your age is {}" }}"#,
client.name,
age
)).unwrap()
}
None => {
serde_json::from_str::<serde_json::Value>(&format!(
r#"{{ "message": "Hello {}, I don't know your age" }}"#,
client.name
)).unwrap()
}
}
});
server.get("/hits", middleware!{ |_req, res| <ServerData>
res.data().hitcount().to_string()
});
server.listen(address)
}
#[cfg(test)]
mod tests {
use self::support::{Body, Server, STATIC_SERVER, get, post};
use hyper::header;
use nickel::status::StatusCode;
use serde_json::Value;
use std::{thread, time};
fn get_hits_after_delay(server: &Server) -> u32 {
thread::sleep(time::Duration::from_secs(1));
let mut response = server.get("/hits");
response.body().parse().unwrap()
}
#[test]
fn root_responds_with_hello_world() {
let mut response = get("/");
assert_eq!(response.body(), "Hello World");
assert_eq!(response.status, StatusCode::Ok);
}
#[test]
fn server_is_shared_with_other_tests() {
assert!(get_hits_after_delay(&STATIC_SERVER) > 1);
}
#[test]
fn root_responds_with_modified_json() {
let mut response = post("/", r#"{ "name": "Rust", "age": 1 }"#);
let json: Value = serde_json::from_str(&response.body()).unwrap();
assert_eq!(json["message"].as_str(), Some("Hello Rust, your age is 1"));
assert_eq!(response.status, StatusCode::Ok);
assert_eq!(
response.headers.get::<header::ContentType>(),
Some(&header::ContentType::json())
);
}
#[test]
fn accepts_json_with_missing_fields() {
let mut response = post("/", r#"{ "name": "Rust" }"#);
let json: Value = serde_json::from_str(&response.body()).unwrap();
assert_eq!(json["message"].as_str(), Some("Hello Rust, I don't know your age"));
assert_eq!(response.status, StatusCode::Ok);
assert_eq!(
response.headers.get::<header::ContentType>(),
Some(&header::ContentType::json())
);
}
#[test]
fn doesnt_accept_bad_inputs() {
let response = post("/", r#"{ }"#);
assert_eq!(response.status, StatusCode::BadRequest);
}
#[test]
fn non_shared_server() {
let test_local_server = {
let server = super::start_server("127.0.0.1:0", vec![]).unwrap();
Server::new(server)
};
assert_eq!(get_hits_after_delay(&test_local_server), 1);
}
#[test]
fn has_no_users_by_default() {
let mut response = get("/users");
let json: Value = serde_json::from_str(&response.body()).unwrap();
assert_eq!(json["users"].as_array().unwrap().len(), 0);
assert_eq!(response.status, StatusCode::Ok);
assert_eq!(
response.headers.get::<header::ContentType>(),
Some(&header::ContentType::json())
);
}
#[test]
fn non_shared_server_with_different_database() {
let server = {
let bots = vec!["bors".into(), "homu".into(), "highfive".into()];
let server = super::start_server("127.0.0.1:0", bots).unwrap();
Server::new(server)
};
let mut response = server.get("/users");
let json: Value = serde_json::from_str(&response.body()).unwrap();
assert_eq!(json["users"].as_array().unwrap().len(), 3);
assert_eq!(response.status, StatusCode::Ok);
assert_eq!(
response.headers.get::<header::ContentType>(),
Some(&header::ContentType::json())
);
}
mod support {
use hyper::client::{Client, Response as HyperResponse};
use nickel::ListeningServer;
use std::net::SocketAddr;
pub trait Body {
fn body(self) -> String;
}
impl<'a> Body for &'a mut HyperResponse {
fn body(self) -> String {
use std::io::Read;
let mut body = String::new();
self.read_to_string(&mut body).expect("Failed to read body of Response");
println!("Read body: {}", body);
body
}
}
pub struct Server(SocketAddr);
impl Server {
pub fn new(server: ListeningServer) -> Server {
let wrapped = Server(server.socket());
server.detach();
wrapped
}
pub fn get(&self, path: &str) -> HyperResponse {
let url = self.url_for(path);
Client::new().get(&url).send().unwrap()
}
pub fn post(&self, path: &str, body: &str) -> HyperResponse {
let url = self.url_for(path);
Client::new().post(&url).body(body).send().unwrap()
}
pub fn url_for(&self, path: &str) -> String {
format!("http://{}{}", self.0, path)
}
}
lazy_static! {
pub static ref STATIC_SERVER: Server = {
let server = super::super::start_server("127.0.0.1:0", vec![]).unwrap();
Server::new(server)
};
}
pub fn get(path: &str) -> HyperResponse {
STATIC_SERVER.get(path)
}
pub fn post(path: &str, body: &str) -> HyperResponse {
STATIC_SERVER.post(path, body)
}
}
}