diff --git a/.gitignore b/.gitignore index 63d1ee48..c4dfc9b3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ db.sql # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock Cargo.lock + +# Scratch list of items that need to get done. +_TODO diff --git a/Cargo.toml b/Cargo.toml index c4bf6de3..c3f47965 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,8 @@ [workspace] members = [ + "lib/", + "codegen/", + "contrib/", "examples/cookies", "examples/errors", "examples/extended_validation", @@ -18,4 +21,8 @@ members = [ "examples/testing", "examples/from_request", "examples/stream", + "examples/json", ] + +[replace] +"aster:0.26.1" = { git = "https://github.com/jchlapinski/aster" } diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml new file mode 100644 index 00000000..b8a1308b --- /dev/null +++ b/contrib/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rocket_contrib" +version = "0.0.8" +authors = ["Sergio Benitez "] + +[features] +default = ["json"] +json = ["serde", "serde_json"] + +[dependencies] +rocket = { path = "../lib/" } +log = "*" + +# JSON module dependencies +serde = { version = "*", optional = true } +serde_json = { version = "*", optional = true } diff --git a/contrib/src/json/mod.rs b/contrib/src/json/mod.rs new file mode 100644 index 00000000..67cd4f7c --- /dev/null +++ b/contrib/src/json/mod.rs @@ -0,0 +1,131 @@ +extern crate serde; +extern crate serde_json; + +use std::ops::{Deref, DerefMut}; + +use rocket::request::{Request, FromRequest}; +use rocket::response::{header, Responder, Outcome, FreshHyperResponse}; +use rocket::response::mime::{Mime, TopLevel, SubLevel}; + +use self::serde::{Serialize, Deserialize}; +use self::serde_json::Error as JSONError; + +/// The JSON datatype, which implements both `FromRequest` and `Responder`. This +/// type allows you to trivially consume and respond with JSON in your Rocket +/// application. +/// +/// If you're receiving JSON data, simple add a `JSON` type to your function +/// signature where `T` is some type you'd like to parse from JSON. `T` must +/// implement `Deserialize` from [Serde](https://github.com/serde-rs/json). The +/// data is parsed from the HTTP request body. +/// +/// ```rust,ignore +/// #[post("/users/", format = "application/json")] +/// fn new_user(user: JSON) { +/// ... +/// } +/// ``` +/// You don't _need_ to use `format = "application/json"`, but it _may_ be what +/// you want. Using `format = application/json` means that any request that +/// doesn't specify "application/json" as its first `Accept:` header parameter +/// will not be routed to this handler. +/// +/// If you're responding with JSON data, return a `JSON` type, where `T` +/// implements implements `Serialize` from +/// [Serde](https://github.com/serde-rs/json). The content type is set to +/// `application/json` automatically. +/// +/// ```rust,ignore +/// #[get("/users/")] +/// fn user(id: usize) -> JSON { +/// let user_from_id = User::from(id); +/// ... +/// JSON(user_from_id) +/// } +/// ``` +/// +pub struct JSON(pub T); + +impl JSON { + /// Consumes the JSON wrapper and returns the wrapped item. + /// + /// # Example + /// ```rust + /// # use rocket_contrib::JSON; + /// let string = "Hello".to_string(); + /// let my_json = JSON(string); + /// assert_eq!(my_json.unwrap(), "Hello".to_string()); + /// ``` + pub fn unwrap(self) -> T { + self.0 + } +} + +impl<'r, 'c, T: Deserialize> FromRequest<'r, 'c> for JSON { + type Error = JSONError; + fn from_request(request: &'r Request<'c>) -> Result { + Ok(JSON(serde_json::from_slice(request.data.as_slice())?)) + } +} + +impl Responder for JSON { + fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> { + let mime = Mime(TopLevel::Application, SubLevel::Json, vec![]); + res.headers_mut().set(header::ContentType(mime)); + match serde_json::to_string(&self.0) { + Ok(mut json_string) => json_string.respond(res), + Err(e) => { + error_!("JSON failed to serialize: {:?}", e); + Outcome::FailStop + } + } + } +} + +impl Deref for JSON { + type Target = T; + + fn deref<'a>(&'a self) -> &'a T { + &self.0 + } +} + +impl DerefMut for JSON { + fn deref_mut<'a>(&'a mut self) -> &'a mut T { + &mut self.0 + } +} + +/// A nice little macro to create simple HashMaps. Really convenient for +/// returning ad-hoc JSON messages. +/// +/// # Examples +/// +/// ``` +/// # #[macro_use] extern crate rocket_contrib; +/// use std::collections::HashMap; +/// # fn main() { +/// let map: HashMap<&str, usize> = map! { +/// "status" => 0, +/// "count" => 100 +/// }; +/// +/// assert_eq!(map.len(), 2); +/// assert_eq!(map.get("status"), Some(&0)); +/// assert_eq!(map.get("count"), Some(&100)); +/// # } +/// ``` +#[macro_export] +macro_rules! map { + ($($key:expr => $value:expr),+) => ({ + use std::collections::HashMap; + let mut map = HashMap::new(); + $(map.insert($key, $value);)+ + map + }); + + ($($key:expr => $value:expr),+,) => { + map!($($key => $value),+) + }; +} + diff --git a/contrib/src/lib.rs b/contrib/src/lib.rs new file mode 100644 index 00000000..923b261b --- /dev/null +++ b/contrib/src/lib.rs @@ -0,0 +1,40 @@ +#![feature(question_mark)] + +//! This crate contains officially sanctioned contributor libraries that provide +//! functionality commonly used by Rocket applications. +//! +//! These libraries are always kept in-sync with the core Rocket library. They +//! provide common, but not fundamental, abstractions to be used by Rocket +//! applications. In particular, contributor libraries typically export types +//! that implement the `FromRequest` trait, `Responder` trait, or both. +//! +//! Each module in this library is held behind a feature flag, with the most +//! common modules exposed by default. The present feature list is below, with +//! an asterisk next to the features that are enabled by default: +//! +//! * json* +//! +//! The recommend way to include features from this crate via Cargo in your +//! project is by adding a `[dependencies.rocket_contrib]` section to your +//! `Cargo.toml` file, setting `default-features` to false, and specifying +//! features manually. For example, to use the JSON module, you would add: +//! +//! ```rust,ignore +//! [dependencies.rocket_contrib] +//! version = "*" +//! default-features = false +//! features = ["json"] +//! ``` +//! +//! This crate is expected to grow with time, bringing in outside crates to be +//! officially supported by Rocket. + +#[macro_use] extern crate log; +#[macro_use] extern crate rocket; + +#[cfg_attr(feature = "json", macro_use)] +#[cfg(feature = "json")] +mod json; + +#[cfg(feature = "json")] +pub use json::JSON; diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml new file mode 100644 index 00000000..bdfefb7d --- /dev/null +++ b/examples/json/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "json" +version = "0.0.1" +authors = ["Sergio Benitez "] +workspace = "../../" + +[dependencies] +rocket = { path = "../../lib" } +rocket_codegen = { path = "../../codegen" } +serde = "*" +serde_json = "*" +serde_macros = "*" +lazy_static = "*" + +[dependencies.rocket_contrib] +path = "../../contrib" +default-features = false +features = ["json"] diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs new file mode 100644 index 00000000..3b9483d3 --- /dev/null +++ b/examples/json/src/main.rs @@ -0,0 +1,92 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen, serde_macros)] + +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate rocket_contrib; +extern crate rocket; +extern crate serde_json; + +use rocket::{Rocket, Request, Error}; +use rocket_contrib::JSON; +use std::collections::HashMap; +use std::sync::Mutex; + +// The type to represent the ID of a message. +type ID = usize; +type SimpleMap = HashMap<&'static str, &'static str>; + +// We're going to store all of the messages here. No need for a DB. +lazy_static! { + static ref MAP: Mutex> = Mutex::new(HashMap::new()); +} + +#[derive(Serialize, Deserialize)] +struct Message { + id: Option, + contents: String +} + +// TODO: This example can be improved by using `route` with muliple HTTP verbs. +// To be precise, put/post could/should look like: +// #[route(PUT, POST, path = "/", format = "application/json")] +// fn f(method: Method, id: ID, message: JSON) -> Option> { +// let mut hashmap = MAP.lock().unwrap(); +// let exists = hashmap.contains_key(&id); +// if method == Method::Put && exists || method == Method::Post && !exists { +// hashmap.insert(id, message.0.contents); +// return Ok(JSON(map!{ "status" => "ok" })) +// } +// +// None +// } + +#[post("/", format = "application/json")] +fn new(id: ID, message: JSON) -> JSON { + let mut hashmap = MAP.lock().unwrap(); + if hashmap.contains_key(&id) { + JSON(map!{ + "status" => "error", + "reason" => "ID exists. Try put." + }) + } else { + hashmap.insert(id, message.0.contents); + JSON(map!{ "status" => "ok" }) + } +} + +#[put("/", format = "application/json")] +fn update(id: ID, message: JSON) -> Option> { + let mut hashmap = MAP.lock().unwrap(); + if hashmap.contains_key(&id) { + hashmap.insert(id, message.0.contents); + Some(JSON(map!{ "status" => "ok" })) + } else { + None + } +} + +#[get("/", format = "application/json")] +fn get(id: ID) -> Option> { + let hashmap = MAP.lock().unwrap(); + hashmap.get(&id).map(|contents| { + JSON(Message { + id: Some(id), + contents: contents.clone() + }) + }) +} + +#[error(404)] +fn not_found<'r>(_: Error, _: &'r Request<'r>) -> JSON { + JSON(map! { + "status" => "error", + "reason" => "Resource was not found." + }) +} + +fn main() { + let mut rocket = Rocket::new("localhost", 8000); + rocket.mount("/message", routes![new, update, get]); + rocket.catch(errors![not_found]); + rocket.launch(); +} diff --git a/lib/src/response/stream.rs b/lib/src/response/stream.rs index 20635510..9838bb2c 100644 --- a/lib/src/response/stream.rs +++ b/lib/src/response/stream.rs @@ -13,7 +13,7 @@ impl Stream { } impl Responder for Stream { - fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> { + fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { let mut stream = res.start().unwrap(); let mut buffer = [0; CHUNK_SIZE]; let mut complete = false; diff --git a/scripts/test.sh b/scripts/test.sh index 3e7f453c..3b4d4e0a 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -4,6 +4,7 @@ set -e EXAMPLES_DIR="examples" LIB_DIR="lib" CODEGEN_DIR="codegen" +CONTRIB_DIR="contrib" # Add Cargo to PATH. export PATH=${HOME}/.cargo/bin:${PATH} @@ -26,6 +27,7 @@ function build_and_test() { build_and_test $LIB_DIR build_and_test $CODEGEN_DIR +build_and_test $CONTRIB_DIR for file in ${EXAMPLES_DIR}/*; do if [ -d "${file}" ]; then