Add contrib crate. Add JSON to contrib. Add JSON example.

This commit is contained in:
Sergio Benitez 2016-09-19 16:24:01 -07:00
parent 1c00793d0d
commit a3218192dd
9 changed files with 310 additions and 1 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -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" }

16
contrib/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "rocket_contrib"
version = "0.0.8"
authors = ["Sergio Benitez <sb@sergio.bz>"]
[features]
default = ["json"]
json = ["serde", "serde_json"]
[dependencies]
rocket = { path = "../lib/" }
log = "*"
# JSON module dependencies
serde = { version = "*", optional = true }
serde_json = { version = "*", optional = true }

131
contrib/src/json/mod.rs Normal file
View File

@ -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<T>` 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<User>) {
/// ...
/// }
/// ```
/// 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<T>` 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/<id>")]
/// fn user(id: usize) -> JSON<User> {
/// let user_from_id = User::from(id);
/// ...
/// JSON(user_from_id)
/// }
/// ```
///
pub struct JSON<T>(pub T);
impl<T> JSON<T> {
/// 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<T> {
type Error = JSONError;
fn from_request(request: &'r Request<'c>) -> Result<Self, Self::Error> {
Ok(JSON(serde_json::from_slice(request.data.as_slice())?))
}
}
impl<T: Serialize> Responder for JSON<T> {
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<T> Deref for JSON<T> {
type Target = T;
fn deref<'a>(&'a self) -> &'a T {
&self.0
}
}
impl<T> DerefMut for JSON<T> {
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),+)
};
}

40
contrib/src/lib.rs Normal file
View File

@ -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;

18
examples/json/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "json"
version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
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"]

92
examples/json/src/main.rs Normal file
View File

@ -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<HashMap<ID, String>> = Mutex::new(HashMap::new());
}
#[derive(Serialize, Deserialize)]
struct Message {
id: Option<ID>,
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 = "/<id>", format = "application/json")]
// fn f(method: Method, id: ID, message: JSON<Message>) -> Option<JSON<SimpleMap>> {
// 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("/<id>", format = "application/json")]
fn new(id: ID, message: JSON<Message>) -> JSON<SimpleMap> {
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("/<id>", format = "application/json")]
fn update(id: ID, message: JSON<Message>) -> Option<JSON<SimpleMap>> {
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("/<id>", format = "application/json")]
fn get(id: ID) -> Option<JSON<Message>> {
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<SimpleMap> {
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();
}

View File

@ -13,7 +13,7 @@ impl<T: Read> Stream<T> {
}
impl<T: Read> Responder for Stream<T> {
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;

View File

@ -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