Merge pull request #1 from SergioBenitez/master

update
This commit is contained in:
zoumi 2017-09-03 22:30:36 +08:00 committed by GitHub
commit d36687c136
290 changed files with 18260 additions and 5363 deletions

55
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,55 @@
Hello, and thanks for opening a new issue about Rocket!
Before opening your issue, we ask that you search through existing issues and
pull requests to see if your bug report, concern, request, or comment has
already been addressed. Ensure to search through both open and closed issues and
pull requests. If this is a question, feature request, or general comment,
please ensure that you have read the relevant sections of the documentation
before posting your issue. Finally, consider asking questions on IRC or Matrix
before opening an issue.
If you feel confident that your issue is unique, please include the following
information, selecting the category that best describes your issue:
## Bug Reports
Bug reports _must_ include:
1. The version of Rocket you're using. Ensure it's the latest, if possible.
2. The operating system (distribution and version) where the issue occurs.
3. A brief description of the bug that includes:
* The nature of the bug.
* When the bug occurs.
* What you expected vs. what actually happened.
4. How you uncovered the bug. Short, reproducible tests are especially useful.
5. Ideas, if any, about what Rocket is doing incorrectly.
## Questions
Any questions _must_ include:
1. The version of Rocket this question is based on, if any.
2. What steps you've taken to answer the question yourself.
3. What documentation you believe should include an answer to this question.
## Feature Requests
Feature requests _must_ include:
1. Why you believe this feature is necessary.
2. A convincing use-case for this feature.
3. Why this feature can't or shouldn't exist outside of Rocket.
## General Comments
Feel free to comment at will. We simply ask that your comments are well
constructed and actionable. Consider whether IRC or Matrix would be a better
venue for discussion.

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ scripts/upload-docs.sh
# Backup files.
*.bak
# Uploads in pastebin example.
examples/pastebin/upload/*

View File

@ -1,4 +1,6 @@
language: rust
sudo: required # so we get a VM with higher specs
dist: trusty # so we get a VM with higher specs
cache: cargo
rust:
- nightly

View File

@ -1,3 +1,391 @@
# Version 0.3.2 (Aug 15, 2017)
## Core
* Added conversion methods from and to `Box<UncasedStr>`.
## Codegen
* Lints were removed due to compiler instability. Lints will likely return as
a separate `rocket_lints` crate.
# Version 0.3.1 (Aug 11, 2017)
## Core
* Added support for ASCII colors on modern Windows consoles.
* Form field renames can now include _any_ valid characters, not just idents.
## Codegen
* Ignored named route parameters are now allowed (`_ident`).
* Fixed issue where certain paths would cause a lint `assert!` to fail
([#367](https://github.com/SergioBenitez/Rocket/issues/367)).
* Lints were updated for `2017-08-10` nightly.
* Minimum required `rustc` is `1.21.0-nightly (2017-08-10)`.
## Contrib
* Tera errors that were previously skipped internally are now emitted.
## Documentation
* Typos were fixed across the board.
# Version 0.3.0 (Jul 14, 2017)
## New Features
This release includes the following new features:
* [Fairings], Rocket's structure middleware, were introduced.
* [Native TLS support] was introduced.
* [Private cookies] were introduced.
* A [`MsgPack`] type has been added to [`contrib`] for simple consumption and
returning of MessagePack data.
* Launch failures ([`LaunchError`]) from [`Rocket::launch()`] are now returned
for inspection without panicking.
* Routes without query parameters now match requests with or without query
parameters.
* [Default rankings] range from -4 to -1, preferring static paths and routes
with query string matches.
* A native [`Accept`] header structure was added.
* The [`Accept`] request header can be retrieved via [`Request::accept()`].
* Incoming form fields [can be renamed] via a new `#[form(field = "name")]`
structure field attribute.
* All active routes can be retrieved via [`Rocket::routes()`].
* [`Response::body_string()`] was added to retrieve the response body as a
`String`.
* [`Response::body_bytes()`] was added to retrieve the response body as a
`Vec<u8>`.
* [`Response::content_type()`] was added to easily retrieve the Content-Type
header of a response.
* Size limits on incoming data are [now
configurable](https://rocket.rs/guide/overview/#configuration).
* [`Request::limits()`] was added to retrieve incoming data limits.
* Responders may dynamically adjust their response based on the incoming
request.
* [`Request::guard()`] was added for simple retrieval of request guards.
* [`Request::route()`] was added to retrieve the active route, if any.
* `&Route` is now a request guard.
* The base mount path of a [`Route`] can be retrieved via `Route::base` or
`Route::base()`.
* [`Cookies`] supports _private_ (authenticated encryption) cookies, encryped
with the `secret_key` config key.
* `Config::{development, staging, production}` constructors were added for
[`Config`].
* [`Config::get_datetime()`] was added to retrieve an extra as a `Datetime`.
* Forms can be now parsed _leniently_ via the new [`LenientForm`] data guard.
* The `?` operator can now be used with `Outcome`.
* Quoted string, array, and table based [configuration parameters] can be set
via environment variables.
* Log coloring is disabled when `stdout` is not a TTY.
* [`FromForm`] is implemented for `Option<T: FromForm>`, `Result<T: FromForm,
T::Error>`.
* The [`NotFound`] responder was added for simple **404** response
construction.
[Fairings]: https://rocket.rs/guide/fairings/
[Native TLS support]: https://rocket.rs/guide/configuration/#configuring-tls
[Private cookies]: https://rocket.rs/guide/requests/#private-cookies
[can be renamed]: https://rocket.rs/guide/requests/#field-renaming
[`MsgPack`]: https://api.rocket.rs/rocket_contrib/struct.MsgPack.html
[`Rocket::launch()`]: https://api.rocket.rs/rocket/struct.Rocket.html#method.launch
[`LaunchError`]: https://api.rocket.rs/rocket/error/struct.LaunchError.html
[Default rankings]: https://api.rocket.rs/rocket/struct.Route.html
[`Route`]: https://api.rocket.rs/rocket/struct.Route.html
[`Accept`]: https://api.rocket.rs/rocket/http/struct.Accept.html
[`Request::accept()`]: https://api.rocket.rs/rocket/struct.Request.html#method.accept
[`contrib`]: https://api.rocket.rs/rocket_contrib/
[`Rocket::routes()`]: https://api.rocket.rs/rocket/struct.Rocket.html#method.routes
[`Response::body_string()`]: https://api.rocket.rs/rocket/struct.Response.html#method.body_string
[`Response::body_bytes()`]: https://api.rocket.rs/rocket/struct.Response.html#method.body_bytes
[`Response::content_type()`]: https://api.rocket.rs/rocket/struct.Response.html#method.content_type
[`Request::guard()`]: https://api.rocket.rs/rocket/struct.Request.html#method.guard
[`Request::limits()`]: https://api.rocket.rs/rocket/struct.Request.html#method.limits
[`Request::route()`]: https://api.rocket.rs/rocket/struct.Request.html#method.route
[`Config`]: https://api.rocket.rs/rocket/struct.Config.html
[`Cookies`]: https://api.rocket.rs/rocket/http/enum.Cookies.html
[`Config::get_datetime()`]: https://api.rocket.rs/rocket/struct.Config.html#method.get_datetime
[`LenientForm`]: https://api.rocket.rs/rocket/request/struct.LenientForm.html
[configuration parameters]: https://api.rocket.rs/rocket/config/index.html#environment-variables
[`NotFound`]: https://api.rocket.rs/rocket/response/status/struct.NotFound.html
## Breaking Changes
This release includes many breaking changes. These changes are listed below
along with a short note about how to handle the breaking change in existing
applications.
* **`session_key` was renamed to `secret_key`, requires a 256-bit base64 key**
It's unlikely that `session_key` was previously used. If it was, rename
`session_key` to `secret_key`. Generate a random 256-bit base64 key using a
tool like openssl: `openssl rand -base64 32`.
* **The `&Cookies` request guard has been removed in favor of `Cookies`**
Change `&Cookies` in a request guard position to `Cookies`.
* **`Rocket::launch()` now returns a `LaunchError`, doesn't panic.**
For the old behavior, suffix a call to `.launch()` with a semicolon:
`.launch();`.
* **Routes without query parameters match requests with or without query
parameters.**
There is no workaround, but this change may allow manual ranks from routes
to be removed.
* **The `format` route attribute on non-payload requests matches against the
Accept header.**
Excepting a custom request guard, there is no workaround. Previously,
`format` always matched against the Content-Type header, regardless of
whether the request method indicated a payload or not.
* **A type of `&str` can no longer be used in form structures or parameters.**
Use the new [`&RawStr`] type instead.
* **`ContentType` is no longer a request guard.**
Use `&ContentType` instead.
* **`Request::content_type()` returns `&ContentType` instead of
`ContentType`.**
Use `.clone()` on `&ContentType` if a type of `ContentType` is required.
* **`Response::header_values()` was removed. `Response::headers()` now returns
an `&HeaderMap`.**
A call to `Response::headers()` can be replaced with
`Response::headers().iter()`. A call to `Response::header_values(name)` can
be replaced with `Response::headers().get(name)`.
* **Route collisions result in a hard error and panic.**
There is no workaround. Previously, route collisions were a warning.
* **The [`IntoOutcome`] trait has been expanded and made more flexible.**
There is no workaround. `IntoOutcome::into_outcome()` now takes a `Failure`
value to use. `IntoOutcome::or_forward()` was added to return a `Forward`
outcome if `self` indicates an error.
* **The 'testing' feature was removed.**
Remove `features = ["testing"]` from `Cargo.toml`. Use the new [`local`]
module for testing.
* **`serde` was updated to 1.0.**
There is no workaround. Ensure all dependencies rely on `serde` `1.0`.
* **`config::active()` was removed.**
Use [`Rocket::config()`] to retrieve the configuration before launch. If
needed, use [managed state] to store config information for later use.
* **The [`Responder`] trait has changed.**
`Responder::respond(self)` was removed in favor of
`Responder::respond_to(self, &Request)`. Responders may dynamically adjust
their response based on the incoming request.
* **`Outcome::of(Responder)` was removed while `Outcome::from(&Request,
Responder)` was added.**
Use `Outcome::from(..)` instead of `Outcome::of(..)`.
* **Usage of templates requires `Template::fairing()` to be attached.**
Call `.attach(Template::fairing())` on the application's Rocket instance
before launching.
* **The `Display` implementation of `Template` was removed.**
Use [`Template::show()`] to render a template directly.
* **`Request::new()` is no longer exported.**
There is no workaround.
* **The [`FromForm`] trait has changed.**
`Responder::from_form_items(&mut FormItems)` was removed in favor of
`Responder::from_form(&mut FormItems, bool)`. The second parameter indicates
whether parsing should be strict (if `true`) or lenient (if `false`).
* **`LoggingLevel` was removed as a root reexport.**
It can now be imported from `rocket::config::LoggingLevel`.
* **An `Io` variant was added to [`ConfigError`].**
Ensure `match`es on `ConfigError` include an `Io` variant.
* **[`ContentType::from_extension()`] returns an `Option<ContentType>`.**
For the old behvavior, use `.unwrap_or(ContentType::Any)`.
* **The `IntoValue` config trait was removed in favor of `Into<Value>`.**
There is no workaround. Use `Into<Value>` as necessary.
* **The `rocket_contrib::JSON` type has been renamed to
[`rocket_contrib::Json`].**
Use `Json` instead of `JSON`.
* **All structs in the [`content`] module use TitleCase names.**
Use `Json`, `Xml`, `Html`, and `Css` instead of `JSON`, `XML`, `HTML`, and
`CSS`, respectively.
[`&RawStr`]: https://api.rocket.rs/rocket/http/struct.RawStr.html
[`IntoOutcome`]: https://api.rocket.rs/rocket/outcome/trait.IntoOutcome.html
[`local`]: https://api.rocket.rs/rocket/local/index.html
[`Rocket::config()`]: https://api.rocket.rs/rocket/struct.Rocket.html#method.config
[managed state]: https://rocket.rs/guide/state/
[`Responder`]: https://api.rocket.rs/rocket/response/trait.Responder.html
[`Template::show()`]: https://api.rocket.rs/rocket_contrib/struct.Template.html#method.show
[`FromForm`]: https://api.rocket.rs/rocket/request/trait.FromForm.html
[`ConfigError`]: https://api.rocket.rs/rocket/config/enum.ConfigError.html
[`ContentType::from_extension()`]: https://api.rocket.rs/rocket/http/struct.ContentType.html#method.from_extension
[`rocket_contrib::Json`]: https://api.rocket.rs/rocket_contrib/struct.Json.html
[`content`]: https://api.rocket.rs/rocket/response/content/index.html
## General Improvements
In addition to new features, Rocket saw the following improvements:
* "Rocket" is now capatilized in the `Server` HTTP header.
* The generic parameter of `rocket_contrib::Json` defaults to `json::Value`.
* The trailing '...' in the launch message was removed.
* The launch message prints regardless of the config environment.
* For debugging, `FromData` is implemented for `Vec<u8>` and `String`.
* The port displayed on launch is the port resolved, not the one configured.
* The `uuid` dependency was updated to `0.5`.
* The `base64` dependency was updated to `0.6`.
* The `toml` dependency was updated to `0.4`.
* The `handlebars` dependency was updated to `0.27`.
* The `tera` dependency was updated to `0.10`.
* [`yansi`] is now used for all terminal coloring.
* The `dev` `rustc` release channel is supported during builds.
* [`Config`] is now exported from the root.
* [`Request`] implements `Clone` and `Debug`.
* The `workers` config parameter now defaults to `num_cpus * 2`.
* Console logging for table-based config values is improved.
* `PartialOrd`, `Ord`, and `Hash` are now implemented for [`State`].
* The format of a request is always logged when available.
[`yansi`]: https://crates.io/crates/yansi
[`Request`]: https://api.rocket.rs/rocket/struct.Request.html
[`State`]: https://api.rocket.rs/rocket/struct.State.html
## Infrastructure
* All examples include a test suite.
* The `master` branch now uses a `-dev` version number.
# Version 0.2.8 (Jun 01, 2017)
## Codegen
* Lints were updated for `2017-06-01` nightly.
* Minimum required `rustc` is `1.19.0-nightly (2017-06-01)`.
# Version 0.2.7 (May 26, 2017)
## Codegen
* Codegen was updated for `2017-05-26` nightly.
# Version 0.2.6 (Apr 17, 2017)
## Codegen
* Allow `k` and `v` to be used as fields in `FromForm` structures by avoiding
identifier collisions ([#265]).
[#265]: https://github.com/SergioBenitez/Rocket/issues/265
# Version 0.2.5 (Apr 16, 2017)
## Codegen
* Lints were updated for `2017-04-15` nightly.
* Minimum required `rustc` is `1.18.0-nightly (2017-04-15)`.
# Version 0.2.4 (Mar 30, 2017)
## Codegen
* Codegen was updated for `2017-03-30` nightly.
* Minimum required `rustc` is `1.18.0-nightly (2017-03-30)`.
# Version 0.2.3 (Mar 22, 2017)
## Fixes
* Multiple header values for the same header name are now properly preserved
(#223).
## Core
* The `get_slice` and `get_table` methods were added to `Config`.
* The `pub_restricted` feature has been stabilized!
## Codegen
* Lints were updated for `2017-03-20` nightly.
* Minimum required `rustc` is `1.17.0-nightly (2017-03-22)`.
## Infrastructure
* The test script now denies trailing whitespace.
# Version 0.2.2 (Feb 26, 2017)
## Codegen
* Lints were updated for `2017-02-25` and `2017-02-26` nightlies.
* Minimum required `rustc` is `1.17.0-nightly (2017-02-26)`.
# Version 0.2.1 (Feb 24, 2017)
## Core Fixes
* `Flash` cookie deletion functions as expected regardless of the path.
* `config` properly accepts IPv6 addresses.
* Multiple `Set-Cookie` headers are properly set.
## Core Improvements
* `Display` and `Error` were implemented for `ConfigError`.
* `webp`, `ttf`, `otf`, `woff`, and `woff2` were added as known content types.
* Routes are presorted for faster routing.
* `into_bytes` and `into_inner` methods were added to `Body`.
## Codegen
* Fixed `unmanaged_state` lint so that it works with prefilled type aliases.
## Contrib
* Better errors are emitted on Tera template parse errors.
## Documentation
* Fixed typos in `manage` and `JSON` docs.
## Infrastructure
* Updated doctests for latest Cargo nightly.
# Version 0.2.0 (Feb 06, 2017)
Detailed release notes for v0.2 can also be found on
@ -134,7 +522,7 @@ In addition to new features, Rocket saw the following smaller improvements:
* Clippy issues injected by codegen are resolved.
* Handlebars was updated to `0.25`.
* The `PartialEq` implementation of `Config` doesn't consider the path or
session key.
secret key.
* Hyper dependency updated to `0.10`.
* The `Error` type for `JSON as FromData` has been exposed as `SerdeError`.
* SVG was added as a known Content-Type.

View File

@ -1,3 +1,6 @@
[profile.dev]
codegen-units = 4
[workspace]
members = [
"lib/",
@ -5,30 +8,32 @@ members = [
"contrib/",
"examples/cookies",
"examples/errors",
"examples/extended_validation",
"examples/forms",
"examples/form_validation",
"examples/hello_person",
"examples/query_params",
"examples/hello_world",
"examples/manual_routes",
"examples/optional_redirect",
"examples/optional_result",
"examples/redirect",
"examples/static_files",
"examples/todo",
"examples/content_types",
"examples/hello_ranks",
"examples/ranking",
"examples/testing",
"examples/from_request",
"examples/request_guard",
"examples/stream",
"examples/json",
"examples/msgpack",
"examples/handlebars_templates",
"examples/form_kitchen_sink",
"examples/config",
"examples/hello_alt_methods",
"examples/raw_upload",
"examples/pastebin",
"examples/state",
"examples/managed_queue",
"examples/uuid",
# "examples/raw_sqlite",
"examples/session",
"examples/raw_sqlite",
"examples/tls",
"examples/fairings",
]

View File

@ -16,7 +16,7 @@ expressibility, and speed. Here's an example of a complete Rocket application:
extern crate rocket;
#[get("/<name>/<age>")]
fn hello(name: &str, age: u8) -> String {
fn hello(name: String, age: u8) -> String {
format!("Hello, {} year old named {}!", age, name)
}
@ -105,7 +105,7 @@ example, the tests for routing can be found at the bottom of the
Code generation tests can be found in `codegen/tests`. We use the
[compiletest](https://crates.io/crates/compiletest_rs) library, which was
extracted from `rustc`, for testing. See the [compiler test
documentation](https://github.com/rust-lang/rust/blob/master/COMPILER_TESTS.md)
documentation](https://github.com/rust-lang/rust/blob/master/src/test/COMPILER_TESTS.md)
for information on how to write compiler tests.
## Documentation
@ -148,8 +148,8 @@ Rocket is designed to be performant. At this time, its performance is
[bottlenecked by the Hyper HTTP
library](https://github.com/SergioBenitez/Rocket/issues/17). Even so, Rocket
currently performs _significantly better_ than the latest version of
asynchronous Hyper on a simple "Hello, world!" benchmark. Rocket also performs
_significantly better_ than the Iron web framework:
multithreaded asynchronous Hyper on a simple "Hello, world!" benchmark. Rocket
also performs _significantly better_ than the Iron web framework:
**Machine Specs:**
@ -169,7 +169,7 @@ _significantly better_ than the Iron web framework:
Requests/sec: 75051.28
Transfer/sec: 10.45MB
**Hyper v0.10.0-a.0 (1/12/2016)** (46 LOC) results (best of 3, +/- 5000 req/s, +/- 30us latency):
**Hyper v0.10-rotor (1/12/2016)** (46 LOC) results (best of 3, +/- 5000 req/s, +/- 30us latency):
Running 10s test @ http://localhost:80
1 threads and 18 connections
@ -213,3 +213,5 @@ Rocket is licensed under either of the following, at your option:
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
The Rocket website source is licensed under [separate terms](site/README.md#license).

View File

@ -1,6 +1,6 @@
[package]
name = "rocket_codegen"
version = "0.2.0"
version = "0.4.0-dev"
authors = ["Sergio Benitez <sb@sergio.bz>"]
description = "Code generation for the Rocket web framework."
documentation = "https://api.rocket.rs/rocket_codegen/"
@ -15,12 +15,12 @@ build = "build.rs"
plugin = true
[dependencies]
rocket = { version = "0.2.0", path = "../lib/" }
log = "^0.3"
rocket = { version = "0.4.0-dev", path = "../lib/" }
log = "0.3"
[dev-dependencies]
compiletest_rs = "^0.2"
compiletest_rs = "0.2.9"
[build-dependencies]
ansi_term = "^0.9"
version_check = "^0.1"
yansi = "0.3"
version_check = "0.1.3"

View File

@ -1,46 +1,37 @@
//! This tiny build script ensures that rocket_codegen is not compiled with an
//! incompatible version of rust.
extern crate ansi_term;
extern crate yansi;
extern crate version_check;
use ansi_term::Colour::{Red, Yellow, Blue, White};
use version_check::{is_nightly, is_min_version, is_min_date};
use yansi::Color::{Red, Yellow, Blue, White};
use version_check::{supports_features, is_min_version, is_min_date};
// Specifies the minimum nightly version needed to compile Rocket's codegen.
const MIN_DATE: &'static str = "2017-01-31";
const MIN_VERSION: &'static str = "1.16.0-nightly";
// Convenience macro for writing to stderr.
macro_rules! printerr {
($($arg:tt)*) => ({
use std::io::prelude::*;
write!(&mut ::std::io::stderr(), "{}\n", format_args!($($arg)*))
.expect("Failed to write to stderr.")
})
}
const MIN_DATE: &'static str = "2017-08-10";
const MIN_VERSION: &'static str = "1.21.0-nightly";
fn main() {
let ok_nightly = is_nightly();
let ok_channel = supports_features();
let ok_version = is_min_version(MIN_VERSION);
let ok_date = is_min_date(MIN_DATE);
let print_version_err = |version: &str, date: &str| {
printerr!("{} {}. {} {}.",
eprintln!("{} {}. {} {}.",
White.paint("Installed version is:"),
Yellow.paint(format!("{} ({})", version, date)),
White.paint("Minimum required:"),
Yellow.paint(format!("{} ({})", MIN_VERSION, MIN_DATE)));
};
match (ok_nightly, ok_version, ok_date) {
(Some(is_nightly), Some((ok_version, version)), Some((ok_date, date))) => {
if !is_nightly {
printerr!("{} {}",
Red.bold().paint("Error:"),
White.paint("Rocket requires a nightly version of Rust."));
match (ok_channel, ok_version, ok_date) {
(Some(ok_channel), Some((ok_version, version)), Some((ok_date, date))) => {
if !ok_channel {
eprintln!("{} {}",
Red.paint("Error:").bold(),
White.paint("Rocket requires a nightly or dev version of Rust."));
print_version_err(&*version, &*date);
printerr!("{}{}{}",
eprintln!("{}{}{}",
Blue.paint("See the getting started guide ("),
White.paint("https://rocket.rs/guide/getting-started/"),
Blue.paint(") for more information."));
@ -48,10 +39,10 @@ fn main() {
}
if !ok_version || !ok_date {
printerr!("{} {}",
Red.bold().paint("Error:"),
eprintln!("{} {}",
Red.paint("Error:").bold(),
White.paint("Rocket codegen requires a more recent version of rustc."));
printerr!("{}{}{}",
eprintln!("{}{}{}",
Blue.paint("Use `"),
White.paint("rustup update"),
Blue.paint("` or your preferred method to update Rust."));

View File

@ -1,10 +1,12 @@
#![allow(unused_imports)] // FIXME: Why is this coming from quote_tokens?
use std::mem::transmute;
use std::collections::HashMap;
use syntax::ext::base::{Annotatable, ExtCtxt};
use syntax::print::pprust::{stmt_to_string};
use syntax::ast::{ItemKind, Expr, MetaItem, Mutability, VariantData, Ident};
use syntax::ast::StructField;
use syntax::codemap::Span;
use syntax::ext::build::AstBuilder;
use syntax::ptr::P;
@ -13,14 +15,14 @@ use syntax_ext::deriving::generic::MethodDef;
use syntax_ext::deriving::generic::{StaticStruct, Substructure, TraitDef, ty};
use syntax_ext::deriving::generic::combine_substructure as c_s;
use utils::strip_ty_lifetimes;
use utils::{strip_ty_lifetimes, is_valid_ident, SpanExt};
static ONLY_STRUCTS_ERR: &'static str = "`FromForm` can only be derived for \
structures with named fields.";
static PRIVATE_LIFETIME: &'static str = "'rocket";
fn get_struct_lifetime(ecx: &mut ExtCtxt, item: &Annotatable, span: Span)
-> Option<&'static str> {
-> Option<String> {
match *item {
Annotatable::Item(ref item) => match item.node {
ItemKind::Struct(_, ref generics) => {
@ -28,13 +30,7 @@ fn get_struct_lifetime(ecx: &mut ExtCtxt, item: &Annotatable, span: Span)
0 => None,
1 => {
let lifetime = generics.lifetimes[0].lifetime;
// According to the documentation, this is safe:
// Because the interner lives for the life of the
// thread, this can be safely treated as an immortal
// string, as long as it never crosses between threads.
let lifetime_name: &'static str =
unsafe { transmute(&*lifetime.name.as_str()) };
Some(lifetime_name)
Some(lifetime.ident.to_string())
}
_ => {
ecx.span_err(item.span, "cannot have more than one \
@ -54,7 +50,7 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
annotated: &Annotatable, push: &mut FnMut(Annotatable)) {
let struct_lifetime = get_struct_lifetime(ecx, annotated, span);
let (lifetime_var, trait_generics) = match struct_lifetime {
lifetime@Some(_) => (lifetime, ty::LifetimeBounds::empty()),
Some(ref lifetime) => (Some(lifetime.as_str()), ty::LifetimeBounds::empty()),
None => (Some(PRIVATE_LIFETIME), ty::LifetimeBounds {
lifetimes: vec![(PRIVATE_LIFETIME, vec![])],
bounds: vec![]
@ -68,7 +64,11 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
is_unsafe: false,
supports_unions: false,
span: span,
attributes: Vec::new(),
// We add these attribute because some `FromFormValue` implementations
// can't fail. This is indicated via the `!` type. Rust checks if a
// match is made with something of that type, and since we always emit
// an `Err` match, we'll get this lint warning.
attributes: vec![quote_attr!(ecx, #[allow(unreachable_code, unreachable_patterns)])],
path: ty::Path {
path: vec!["rocket", "request", "FromForm"],
lifetime: lifetime_var,
@ -79,7 +79,7 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
generics: trait_generics,
methods: vec![
MethodDef {
name: "from_form_items",
name: "from_form",
generics: ty::LifetimeBounds::empty(),
explicit_self: None,
args: vec![
@ -91,10 +91,15 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
global: true
})),
ty::Borrowed(None, Mutability::Mutable)
)
),
ty::Literal(ty::Path {
path: vec!["bool"],
lifetime: None,
params: vec![],
global: false,
})
],
ret_ty: ty::Ty::Literal(
ty::Path {
ret_ty: ty::Literal(ty::Path {
path: vec!["std", "result", "Result"],
lifetime: None,
params: vec![
@ -102,8 +107,7 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
Box::new(error_type.clone())
],
global: true,
}
),
}),
attributes: vec![],
is_unsafe: false,
combine_substructure: c_s(Box::new(from_form_substructure)),
@ -118,18 +122,85 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
trait_def.expand(ecx, meta_item, annotated, push);
}
fn is_valid_field_name(name: &str) -> bool {
// The HTML5 spec (4.10.18.1) says 'isindex' is not allowed.
if name == "isindex" || name.is_empty() {
return false
}
// We allow all visible ASCII characters except '&', '=', and '?' since we
// use those as control characters for parsing.
name.chars()
.all(|c| (c >= ' ' && c <= '~') && c != '&' && c != '=' && c != '?')
}
pub fn extract_field_ident_name(ecx: &ExtCtxt, struct_field: &StructField)
-> (Ident, String, Span) {
let ident = match struct_field.ident {
Some(ident) => ident,
None => ecx.span_fatal(struct_field.span, ONLY_STRUCTS_ERR)
};
let field_attrs: Vec<_> = struct_field.attrs.iter()
.filter(|attr| attr.check_name("form"))
.collect();
let default = |ident: Ident| (ident, ident.to_string(), struct_field.span);
if field_attrs.len() == 0 {
return default(ident);
} else if field_attrs.len() > 1 {
ecx.span_err(struct_field.span, "only a single #[form(..)] \
attribute can be applied to a given struct field at a time");
return default(ident);
}
let field_attr = field_attrs[0];
::syntax::attr::mark_known(&field_attr);
if !field_attr.meta_item_list().map_or(false, |l| l.len() == 1) {
ecx.struct_span_err(field_attr.span, "incorrect use of attribute")
.help(r#"the `form` attribute must have the form: #[form(field = "..")]"#)
.emit();
return default(ident);
}
let inner_item = &field_attr.meta_item_list().unwrap()[0];
if !inner_item.check_name("field") {
ecx.struct_span_err(inner_item.span, "invalid `form` attribute contents")
.help(r#"only the 'field' key is supported: #[form(field = "..")]"#)
.emit();
return default(ident);
}
if !inner_item.is_value_str() {
ecx.struct_span_err(inner_item.span, "invalid `field` in attribute")
.help(r#"the `form` attribute must have the form: #[form(field = "..")]"#)
.emit();
return default(ident);
}
let name = inner_item.value_str().unwrap().as_str().to_string();
let sp = inner_item.span.shorten_upto(name.len() + 2);
if !is_valid_field_name(&name) {
ecx.struct_span_err(sp, "invalid form field name")
.help("field names must be visible ASCII without '&', '=', or '?'")
.emit();
}
(ident, name, sp)
}
fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substructure) -> P<Expr> {
// Check that we specified the methods to the argument correctly.
const EXPECTED_ARGS: usize = 1;
let arg = if substr.nonself_args.len() == EXPECTED_ARGS {
&substr.nonself_args[0]
const EXPECTED_ARGS: usize = 2;
let (items_arg, strict_arg) = if substr.nonself_args.len() == EXPECTED_ARGS {
(&substr.nonself_args[0], &substr.nonself_args[1])
} else {
let msg = format!("incorrect number of arguments in `from_form_string`: \
expected {}, found {}", EXPECTED_ARGS, substr.nonself_args.len());
cx.span_bug(trait_span, msg.as_str());
};
debug!("argument is: {:?}", arg);
debug!("arguments are: {:?}, {:?}", items_arg, strict_arg);
// Ensure the the fields are from a 'StaticStruct' and extract them.
let fields = match *substr.fields {
@ -140,19 +211,25 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
_ => cx.span_bug(trait_span, "impossible substructure in `from_form`")
};
// Create a vector of (ident, type) pairs, one for each field in struct.
let mut fields_and_types = vec![];
// Vec of (ident: Ident, type: Ty, name: String), one for each field.
let mut names = HashMap::new();
let mut fields_info = vec![];
for field in fields {
let ident = match field.ident {
Some(ident) => ident,
None => cx.span_fatal(trait_span, ONLY_STRUCTS_ERR)
};
let (ident, name, span) = extract_field_ident_name(cx, field);
let stripped_ty = strip_ty_lifetimes(field.ty.clone());
fields_and_types.push((ident, stripped_ty));
if let Some(sp) = names.get(&name).map(|sp| *sp) {
cx.struct_span_err(span, "field with duplicate name")
.span_note(sp, "original was declared here")
.emit();
} else {
names.insert(name.clone(), span);
}
debug!("Fields and types: {:?}", fields_and_types);
fields_info.push((ident, stripped_ty, name));
}
debug!("Fields, types, attrs: {:?}", fields_info);
let mut stmts = Vec::new();
// The thing to do when we wish to exit with an error.
@ -164,7 +241,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// placed into the final struct. They start out as `None` and are changed
// to Some when a parse completes, or some default value if the parse was
// unsuccessful and default() returns Some.
for &(ref ident, ref ty) in &fields_and_types {
for &(ref ident, ref ty, _) in &fields_info {
stmts.push(quote_stmt!(cx,
let mut $ident: ::std::option::Option<$ty> = None;
).unwrap());
@ -173,16 +250,15 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// Generating an arm for each struct field. This matches against the key and
// tries to parse the value according to the type.
let mut arms = vec![];
for &(ref ident, _) in &fields_and_types {
let ident_string = ident.to_string();
let id_str = ident_string.as_str();
for &(ref ident, _, ref name) in &fields_info {
arms.push(quote_tokens!(cx,
$id_str => {
$ident = match ::rocket::request::FromFormValue::from_form_value(v) {
Ok(v) => Some(v),
Err(e) => {
$name => {
let __r = ::rocket::http::RawStr::from_str(__v);
$ident = match ::rocket::request::FromFormValue::from_form_value(__r) {
Ok(__v) => Some(__v),
Err(__e) => {
println!(" => Error parsing form val '{}': {:?}",
$id_str, e);
$name, __e);
$return_err_stmt
}
};
@ -193,19 +269,18 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// The actual match statement. Iterate through all of the fields in the form
// and use the $arms generated above.
stmts.push(quote_stmt!(cx,
for (k, v) in $arg {
match k {
for (__k, __v) in $items_arg {
match __k.as_str() {
$arms
field if field == "_method" => {
/* This is a Rocket-specific field. If the user hasn't asked
* for it, just let it go by without error. This should stay
* in sync with Rocket::preprocess. */
}
_ => {
// If we're parsing strictly, emit an error for everything
// the user hasn't asked for. Keep synced with 'preprocess'.
if $strict_arg && __k != "_method" {
println!(" => {}={} has no matching field in struct.",
k, v);
__k, __v);
$return_err_stmt
}
}
};
}
).unwrap());
@ -214,26 +289,20 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// that each parameter actually is Some() or has a default value.
let mut failure_conditions = vec![];
// Start with `false` in case there are no fields.
failure_conditions.push(quote_tokens!(cx, false));
for &(ref ident, ref ty) in (&fields_and_types).iter() {
// Pushing an "||" (or) between every condition.
failure_conditions.push(quote_tokens!(cx, ||));
for &(ref ident, ref ty, _) in (&fields_info).iter() {
failure_conditions.push(quote_tokens!(cx,
if $ident.is_none() &&
<$ty as ::rocket::request::FromFormValue>::default().is_none() {
println!(" => '{}' did not parse.", stringify!($ident));
true
} else { false }
$return_err_stmt;
}
));
}
// The fields of the struct, which are just the let bindings declared above
// or the default value.
let mut result_fields = vec![];
for &(ref ident, ref ty) in &fields_and_types {
for &(ref ident, ref ty, _) in &fields_info {
result_fields.push(quote_tokens!(cx,
$ident: $ident.unwrap_or_else(||
<$ty as ::rocket::request::FromFormValue>::default().unwrap()
@ -245,9 +314,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// the structure.
let self_ident = substr.type_ident;
let final_block = quote_block!(cx, {
if $failure_conditions {
$return_err_stmt;
}
$failure_conditions
Ok($self_ident { $result_fields })
});

View File

@ -8,8 +8,8 @@ use syntax::ext::base::{Annotatable, ExtCtxt};
use syntax::tokenstream::TokenTree;
use syntax::parse::token;
const ERR_PARAM: &'static str = "_error";
const REQ_PARAM: &'static str = "_request";
const ERR_PARAM: &'static str = "__err";
const REQ_PARAM: &'static str = "__req";
trait ErrorGenerateExt {
fn generate_fn_arguments(&self, &ExtCtxt, Ident, Ident) -> Vec<TokenTree>;
@ -71,7 +71,8 @@ pub fn error_decorator(ecx: &mut ExtCtxt,
$req_ident: &'_b ::rocket::Request)
-> ::rocket::response::Result<'_b> {
let user_response = $user_fn_name($fn_arguments);
let response = ::rocket::response::Responder::respond(user_response)?;
let response = ::rocket::response::Responder::respond_to(user_response,
$req_ident)?;
let status = ::rocket::http::Status::raw($code);
::rocket::response::Response::build().status(status).merge(response).ok()
}

View File

@ -12,9 +12,10 @@ use syntax::ast::{Arg, Ident, Stmt, Expr, MetaItem, Path};
use syntax::ext::base::{Annotatable, ExtCtxt};
use syntax::ext::build::AstBuilder;
use syntax::parse::token;
use syntax::symbol::InternedString;
use syntax::ptr::P;
use rocket::http::{Method, ContentType};
use rocket::http::{Method, MediaType};
fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path {
quote_enum!(ecx, method => ::rocket::http::Method {
@ -22,38 +23,28 @@ fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path {
})
}
fn content_type_to_expr(ecx: &ExtCtxt, ct: Option<ContentType>) -> Option<P<Expr>> {
fn media_type_to_expr(ecx: &ExtCtxt, ct: Option<MediaType>) -> Option<P<Expr>> {
ct.map(|ct| {
let (top, sub) = (ct.ttype.as_str(), ct.subtype.as_str());
quote_expr!(ecx, ::rocket::http::ContentType {
ttype: ::rocket::http::ascii::UncasedAscii {
string: ::std::borrow::Cow::Borrowed($top)
},
subtype: ::rocket::http::ascii::UncasedAscii {
string: ::std::borrow::Cow::Borrowed($sub)
},
params: None
let (top, sub) = (ct.top().as_str(), ct.sub().as_str());
quote_expr!(ecx, ::rocket::http::MediaType {
source: ::rocket::http::Source::None,
top: ::rocket::http::IndexedStr::Concrete(
::std::borrow::Cow::Borrowed($top)
),
sub: ::rocket::http::IndexedStr::Concrete(
::std::borrow::Cow::Borrowed($sub)
),
params: ::rocket::http::MediaParams::Static(&[])
})
})
}
trait RouteGenerateExt {
fn gen_form(&self, &ExtCtxt, Option<&Spanned<Ident>>, P<Expr>) -> Option<Stmt>;
fn missing_declared_err<T: Display>(&self, ecx: &ExtCtxt, arg: &Spanned<T>);
fn generate_data_statement(&self, ecx: &ExtCtxt) -> Option<Stmt>;
fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option<Stmt>;
fn generate_param_statements(&self, ecx: &ExtCtxt) -> Vec<Stmt>;
fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec<TokenTree>;
fn explode(&self, ecx: &ExtCtxt) -> (&str, Path, P<Expr>, P<Expr>);
}
impl RouteGenerateExt for RouteParams {
impl RouteParams {
fn missing_declared_err<T: Display>(&self, ecx: &ExtCtxt, arg: &Spanned<T>) {
let fn_span = self.annotated_fn.span();
let msg = format!("'{}' is declared as an argument...", arg.node);
ecx.span_err(arg.span, &msg);
ecx.span_err(fn_span, "...but isn't in the function signature.");
let (fn_span, fn_name) = (self.annotated_fn.span(), self.annotated_fn.ident());
ecx.struct_span_err(arg.span, &format!("unused dynamic parameter: `{}`", arg.node))
.span_note(fn_span, &format!("expected argument named `{}` in `{}`", arg.node, fn_name))
.emit();
}
fn gen_form(&self,
@ -73,14 +64,17 @@ impl RouteGenerateExt for RouteParams {
let name = arg.ident().expect("form param identifier").prepend(PARAM_PREFIX);
let ty = strip_ty_lifetimes(arg.ty.clone());
Some(quote_stmt!(ecx,
#[allow(non_snake_case)]
let $name: $ty = {
let mut items = ::rocket::request::FormItems::from($form_string);
let obj = match ::rocket::request::FromForm::from_form_items(items.by_ref()) {
let form = ::rocket::request::FromForm::from_form(items.by_ref(), true);
#[allow(unreachable_patterns)]
let obj = match form {
Ok(v) => v,
Err(_) => return ::rocket::Outcome::Forward(_data)
Err(_) => return ::rocket::Outcome::Forward(__data)
};
if !items.exhausted() {
if !items.exhaust() {
println!(" => The query string {:?} is malformed.", $form_string);
return ::rocket::Outcome::Failure(::rocket::http::Status::BadRequest);
}
@ -104,8 +98,9 @@ impl RouteGenerateExt for RouteParams {
let name = arg.ident().expect("form param identifier").prepend(PARAM_PREFIX);
let ty = strip_ty_lifetimes(arg.ty.clone());
Some(quote_stmt!(ecx,
#[allow(non_snake_case, unreachable_patterns)]
let $name: $ty =
match ::rocket::data::FromData::from_data(_req, _data) {
match ::rocket::data::FromData::from_data(__req, __data) {
::rocket::Outcome::Success(d) => d,
::rocket::Outcome::Forward(d) =>
return ::rocket::Outcome::Forward(d),
@ -119,9 +114,9 @@ impl RouteGenerateExt for RouteParams {
fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option<Stmt> {
let param = self.query_param.as_ref();
let expr = quote_expr!(ecx,
match _req.uri().query() {
match __req.uri().query() {
Some(query) => query,
None => return ::rocket::Outcome::Forward(_data)
None => return ::rocket::Outcome::Forward(__data)
}
);
@ -148,24 +143,25 @@ impl RouteGenerateExt for RouteParams {
// Note: the `None` case shouldn't happen if a route is matched.
let ident = param.ident().prepend(PARAM_PREFIX);
let expr = match param {
Param::Single(_) => quote_expr!(ecx, match _req.get_param_str($i) {
Param::Single(_) => quote_expr!(ecx, match __req.get_param_str($i) {
Some(s) => <$ty as ::rocket::request::FromParam>::from_param(s),
None => return ::rocket::Outcome::Forward(_data)
None => return ::rocket::Outcome::Forward(__data)
}),
Param::Many(_) => quote_expr!(ecx, match _req.get_raw_segments($i) {
Param::Many(_) => quote_expr!(ecx, match __req.get_raw_segments($i) {
Some(s) => <$ty as ::rocket::request::FromSegments>::from_segments(s),
None => return ::rocket::Outcome::Forward(_data)
None => return ::rocket::Outcome::Forward(__data)
}),
};
let original_ident = param.ident();
fn_param_statements.push(quote_stmt!(ecx,
#[allow(non_snake_case, unreachable_patterns)]
let $ident: $ty = match $expr {
Ok(v) => v,
Err(e) => {
println!(" => Failed to parse '{}': {:?}",
stringify!($original_ident), e);
return ::rocket::Outcome::Forward(_data)
return ::rocket::Outcome::Forward(__data)
}
};
).expect("declared param parsing statement"));
@ -192,13 +188,13 @@ impl RouteGenerateExt for RouteParams {
let ident = arg.ident().unwrap().prepend(PARAM_PREFIX);
let ty = strip_ty_lifetimes(arg.ty.clone());
fn_param_statements.push(quote_stmt!(ecx,
#[allow(non_snake_case)]
#[allow(non_snake_case, unreachable_patterns)]
let $ident: $ty = match
::rocket::request::FromRequest::from_request(_req) {
::rocket::outcome::Outcome::Success(v) => v,
::rocket::outcome::Outcome::Forward(_) =>
return ::rocket::Outcome::forward(_data),
::rocket::outcome::Outcome::Failure((code, _)) => {
::rocket::request::FromRequest::from_request(__req) {
::rocket::Outcome::Success(v) => v,
::rocket::Outcome::Forward(_) =>
return ::rocket::Outcome::Forward(__data),
::rocket::Outcome::Failure((code, _)) => {
return ::rocket::Outcome::Failure(code)
},
};
@ -217,14 +213,15 @@ impl RouteGenerateExt for RouteParams {
sep_by_tok(ecx, &args, token::Comma)
}
fn explode(&self, ecx: &ExtCtxt) -> (&str, Path, P<Expr>, P<Expr>) {
fn explode(&self, ecx: &ExtCtxt) -> (InternedString, &str, Path, P<Expr>, P<Expr>) {
let name = self.annotated_fn.ident().name.as_str();
let path = &self.uri.node.as_str();
let method = method_to_path(ecx, self.method.node);
let format = self.format.as_ref().map(|kv| kv.value().clone());
let content_type = option_as_expr(ecx, &content_type_to_expr(ecx, format));
let media_type = option_as_expr(ecx, &media_type_to_expr(ecx, format));
let rank = option_as_expr(ecx, &self.rank);
(path, method, content_type, rank)
(name, path, method, media_type, rank)
}
}
@ -233,9 +230,8 @@ fn generic_route_decorator(known_method: Option<Spanned<Method>>,
ecx: &mut ExtCtxt,
sp: Span,
meta_item: &MetaItem,
annotated: Annotatable)
-> Vec<Annotatable>
{
annotated: Annotatable
) -> Vec<Annotatable> {
let mut output = Vec::new();
// Parse the route and generate the code to create the form and param vars.
@ -251,28 +247,33 @@ fn generic_route_decorator(known_method: Option<Spanned<Method>>,
let user_fn_name = route.annotated_fn.ident();
let route_fn_name = user_fn_name.prepend(ROUTE_FN_PREFIX);
emit_item(&mut output, quote_item!(ecx,
fn $route_fn_name<'_b>(_req: &'_b ::rocket::Request, _data: ::rocket::Data)
// Allow the `unreachable_code` lint for those FromParam impls that have
// an `Error` associated type of !.
#[allow(unreachable_code)]
fn $route_fn_name<'_b>(__req: &'_b ::rocket::Request, __data: ::rocket::Data)
-> ::rocket::handler::Outcome<'_b> {
$param_statements
$query_statement
$data_statement
let responder = $user_fn_name($fn_arguments);
::rocket::handler::Outcome::of(responder)
::rocket::handler::Outcome::from(__req, responder)
}
).unwrap());
// Generate and emit the static route info that uses the just generated
// function as its handler. A proper Rocket route will be created from this.
let struct_name = user_fn_name.prepend(ROUTE_STRUCT_PREFIX);
let (path, method, content_type, rank) = route.explode(ecx);
let (name, path, method, media_type, rank) = route.explode(ecx);
let static_route_info_item = quote_item!(ecx,
/// Rocket code generated static route information structure.
#[allow(non_upper_case_globals)]
pub static $struct_name: ::rocket::StaticRouteInfo =
::rocket::StaticRouteInfo {
name: $name,
method: $method,
path: $path,
handler: $route_fn_name,
format: $content_type,
format: $media_type,
rank: $rank,
};
).expect("static route info");

View File

@ -1,10 +1,9 @@
//! # Rocket - Code Generation
//!
//! This crate implements the code generation portions of Rocket. This includes
//! custom derives, custom attributes, procedural macros, and lints. The
//! documentation here is purely technical. The code generation facilities are
//! documented thoroughly in the [Rocket programming
//! guide](https://rocket.rs/guide).
//! custom derives, custom attributes, and procedural macros. The documentation
//! here is purely technical. The code generation facilities are documented
//! thoroughly in the [Rocket programming guide](https://rocket.rs/guide).
//!
//! ## Custom Attributes
//!
@ -39,9 +38,9 @@
//!
//! INTEGER := isize, as defined by Rust
//! STRING := UTF-8 string literal, as defined by Rust
//! IDENT := Valid identifier, as defined by Rust
//! IDENT := valid identifier, as defined by Rust
//!
//! URI_SEG := Valid HTTP URI Segment
//! URI_SEG := valid HTTP URI Segment
//! DYNAMIC_PARAM := '<' IDENT '..'? '>' (string literal)
//! </pre>
//!
@ -70,6 +69,47 @@
//!
//! * **FromForm**
//!
//! ### `FromForm`
//!
//! The [`FromForm`] derive can be applied to structures with named fields:
//!
//! #[derive(FromForm)]
//! struct MyStruct {
//! field: usize,
//! other: String
//! }
//!
//! Each field's type is required to implement [`FromFormValue`]. The derive
//! accepts one field attribute: `form`, with the following syntax:
//!
//! <pre>
//! form := 'field' '=' '"' IDENT '"'
//!
//! IDENT := valid identifier, as defined by Rust
//! </pre>
//!
//! When applied, the attribute looks as follows:
//!
//! #[derive(FromForm)]
//! struct MyStruct {
//! field: usize,
//! #[form(field = "renamed_field")]
//! other: String
//! }
//!
//! The derive generates an implementation for the [`FromForm`] trait. The
//! implementation parses a form whose field names match the field names of the
//! structure on which the derive was applied. Each field's value is parsed with
//! the [`FromFormValue`] implementation of the field's type. The `FromForm`
//! implementation succeeds only when all of the field parses succeed.
//!
//! The `form` field attribute can be used to direct that a different incoming
//! field name is expected. In this case, the attribute's field name is used
//! instead of the structure's field name when parsing a form.
//!
//! [`FromForm`]: /rocket/request/trait.FromForm.html
//! [`FromFormValue`]: /rocket/request/trait.FromFormValue.html
//!
//! ## Procedural Macros
//!
//! This crate implements the following procedural macros:
@ -85,19 +125,6 @@
//! PATH := a path, as defined by Rust
//! </pre>
//!
//! ## Lints
//!
//! This crate implements the following lints:
//!
//! * **unmounted_route**: defaults to _warn_
//!
//! emits a warning when a declared route is not mounted
//!
//! * **unmanaged_state**: defaults to _warn_
//!
//! emits a warning when a `State<T>` request guest is used in a mounted
//! route without managing a value for `T`
//!
//! # Debugging Codegen
//!
//! When the `ROCKET_CODEGEN_DEBUG` environment variable is set, this crate logs
@ -110,17 +137,15 @@
//! ```
#![crate_type = "dylib"]
#![feature(quote, concat_idents, plugin_registrar, rustc_private, unicode)]
#![feature(quote, concat_idents, plugin_registrar, rustc_private)]
#![feature(custom_attribute)]
#![feature(i128_type)]
#![allow(unused_attributes)]
#![allow(deprecated)]
#[macro_use] extern crate log;
#[macro_use] extern crate rustc;
extern crate syntax;
extern crate syntax_ext;
extern crate syntax_pos;
extern crate rustc_plugin;
extern crate rocket;
@ -128,7 +153,6 @@ extern crate rocket;
mod parser;
mod macros;
mod decorators;
mod lints;
use std::env;
use rustc_plugin::Registry;
@ -164,18 +188,12 @@ macro_rules! register_derives {
)
}
macro_rules! register_lints {
($registry:expr, $($item:ident),+) => ($(
$registry.register_late_lint_pass(Box::new(lints::$item::default()));
)+)
}
/// Compiler hook for Rust to register plugins.
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
// Enable logging early if the DEBUG_ENV_VAR is set.
if env::var(DEBUG_ENV_VAR).is_ok() {
::rocket::logger::init(::rocket::LoggingLevel::Debug);
::rocket::logger::init(::rocket::config::LoggingLevel::Debug);
}
reg.register_macro("routes", macros::routes);
@ -196,6 +214,4 @@ pub fn plugin_registrar(reg: &mut Registry) {
"patch" => patch_decorator,
"options" => options_decorator
);
register_lints!(reg, RocketLint);
}

View File

@ -1,332 +0,0 @@
mod utils;
use self::utils::*;
use ::{ROUTE_ATTR, ROUTE_INFO_ATTR};
use std::mem::transmute;
use std::collections::HashMap;
use rustc::lint::{Level, LateContext, LintContext, LintPass, LateLintPass, LintArray};
use rustc::hir::{Item, Expr, Crate, Decl, FnDecl, Body, QPath, PatKind};
use rustc::hir::def_id::DefId;
use rustc::ty::Ty;
use rustc::hir::intravisit::{FnKind};
use rustc::hir::Decl_::*;
use rustc::hir::Expr_::*;
use syntax_pos::Span;
use syntax::symbol::Symbol as Name;
use syntax::ast::NodeId;
const STATE_TYPE: &'static [&'static str] = &["rocket", "request", "state", "State"];
// Information about a specific Rocket instance.
#[derive(Debug, Default)]
struct InstanceInfo {
// Mapping from mounted struct info to the span of the mounted call.
mounted: HashMap<DefId, Span>,
// Mapping from managed types to the span of the manage call.
managed: HashMap<Ty<'static>, Span>,
}
/// A `Receiver` captures the "receiver" of a Rocket instance method call. A
/// Receiver can be an existing instance of Rocket or a call to an Rocket
/// initialization function.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
enum Receiver {
Instance(DefId, Span),
Call(NodeId, Span),
}
impl Receiver {
/// Returns the span associated with the receiver.
pub fn span(&self) -> Span {
match *self {
Receiver::Instance(_, sp) | Receiver::Call(_, sp) => sp
}
}
}
#[derive(Debug, Default)]
pub struct RocketLint {
// All of the types that were requested as managed state.
// (fn_name, fn_span, info_struct_def_id, req_type, req_param_span)
requested: Vec<(Name, Span, DefId, Ty<'static>, Span)>,
// Mapping from a `Rocket` instance initialization call span (an ignite or
// custom call) to the collected info about that instance.
instances: HashMap<Option<Receiver>, InstanceInfo>,
// Map of all route info structure names found in the program to its defid.
// This is used to map a declared route to its info structure defid.
info_structs: HashMap<Name, DefId>,
// The name, span, and info DefId for all route functions found. The DefId
// is obtained by indexing into info_structs with the name found in the
// attribute that Rocket generates.
declared: Vec<(Name, Span, DefId)>,
// Mapping from known named Rocket instances to initial receiver. This is
// used to do a sort-of flow-based analysis. We track variable declarations
// and link calls to Rocket methods to the (as best as we can tell) initial
// call to generate that Rocket instance. We use this to group calls to
// `manage` and `mount` to give more accurate warnings.
instance_vars: HashMap<DefId, Receiver>,
}
declare_lint!(UNMOUNTED_ROUTE, Warn, "Warn on routes that are unmounted.");
declare_lint!(UNMANAGED_STATE, Warn, "Warn on declared use of unmanaged state.");
impl<'tcx> LintPass for RocketLint {
fn get_lints(&self) -> LintArray {
lint_array!(UNMANAGED_STATE, UNMOUNTED_ROUTE)
}
}
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for RocketLint {
// This fills out the `instance_vars` table by tracking calls to
// function/methods that create Rocket instances. If the call is a method
// call with a receiver that we know is a Rocket instance, then we know it's
// been moved, and we track that move by linking all definition to the same
// receiver.
fn check_decl(&mut self, cx: &LateContext<'a, 'tcx>, decl: &'tcx Decl) {
// We only care about local declarations...everything else seems very
// unlikely. This is imperfect, after all.
if let DeclLocal(ref local) = decl.node {
// Retrieve the def_id for the new binding.
let new_def_id = match local.pat.node {
PatKind::Binding(_, def_id, ..) => def_id ,
_ => return
};
// `init` is the RHS of the declaration.
if let Some(ref init) = local.init {
// We only care about declarations that result in Rocket insts.
if !returns_rocket_instance(cx, init) {
return;
}
let (expr, span) = match find_initial_receiver(cx, init) {
Some(expr) => (expr, expr.span),
None => return
};
// If the receiver is a path, check if this path was declared
// before by another binding and use that binding's receiver as
// this binding's receiver, essentially taking us back in time.
// If we don't know about it, just insert a new receiver.
if let ExprPath(QPath::Resolved(_, ref path)) = expr.node {
if let Some(old_def_id) = path.def.def_id_opt() {
if let Some(&prev) = self.instance_vars.get(&old_def_id) {
self.instance_vars.insert(new_def_id, prev);
} else {
let recvr = Receiver::Instance(old_def_id, span);
self.instance_vars.insert(new_def_id, recvr);
}
}
}
// We use a call as a base case. Maybe it's a brand new Rocket
// instance, maybe it's a function returning a Rocket instance.
// Who knows. This is where imperfection comes in. We're just
// going to assume that calls to `mount` and `manage` are
// grouped with their originating call.
if let ExprCall(ref expr, ..) = expr.node {
let recvr = Receiver::Call(expr.id, span);
self.instance_vars.insert(new_def_id, recvr);
}
}
}
}
// Here, we collect all of the calls to `manage` and `mount` by instance,
// where the instance is determined by the receiver of the call. We look up
// the receiver in the type table we've constructed. If it's there, we use
// it, if not, we use the call as the receiver.
fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr) {
/// Fetches the top-level `Receiver` instance given that a method call
/// was made to the receiver `rexpr`. Top-level here means "the
/// original". We search the `instance_vars` table to retrieve it.
let instance_for = |lint: &mut RocketLint, rexpr: &Expr| -> Option<Receiver> {
match rexpr.node {
ExprPath(QPath::Resolved(_, ref p)) => {
p.def.def_id_opt()
.and_then(|id| lint.instance_vars.get(&id))
.map(|recvr| recvr.clone())
}
ExprCall(ref c, ..) => Some(Receiver::Call(c.id, rexpr.span)),
_ => unreachable!()
}
};
if let Some((recvr, args)) = rocket_method_call("manage", cx, expr) {
let managed_val = &args[0];
let instance = recvr.and_then(|r| instance_for(self, r));
if let Some(managed_ty) = cx.tables.expr_ty_opt(managed_val) {
self.instances.entry(instance)
.or_insert_with(|| InstanceInfo::default())
.managed
.insert(unsafe { transmute(managed_ty) }, managed_val.span);
}
}
if let Some((recvr, args)) = rocket_method_call("mount", cx, expr) {
let instance = recvr.and_then(|r| instance_for(self, r));
for def_id in extract_mount_fn_def_ids(cx, &args[1]) {
self.instances.entry(instance)
.or_insert_with(|| InstanceInfo::default())
.mounted
.insert(def_id, expr.span);
}
}
// This captures a corner case where neither `manage` nor `mount` is
// called on an instance.
if let Some((Some(recvr_expr), _)) = rocket_method_call("launch", cx, expr) {
if let Some(instance) = instance_for(self, recvr_expr) {
self.instances.entry(Some(instance))
.or_insert_with(|| InstanceInfo::default());
}
}
}
// We collect all of the names and defids for the info structures that
// Rocket has generated. We do this by simply looking at the attribute,
// which Rocket's codegen was kind enough to generate.
fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx Item) {
// Return early if this is not a route info structure.
if !item.attrs.iter().any(|attr| attr.check_name(ROUTE_INFO_ATTR)) {
return;
}
if let Some(def_id) = cx.tcx.hir.opt_local_def_id(item.id) {
self.info_structs.insert(item.name, def_id);
}
}
/// We do two things here: 1) we find all of the `State` request guards a
/// user wants, and 2) we find all of the routes declared by the user. We
/// determine that a function is a route by looking for the attribute that
/// Rocket declared. We tie the route to the info structure, obtained from
/// the `check_item` call, so that we can determine if the route was mounted
/// or not. The tie is done by looking at the name of the info structure in
/// the attribute that Rocket generated and then looking up the structure in
/// the `info_structs` map. The structure _must_ be there since Rocket
/// always generates the structure before the route.
fn check_fn(&mut self,
cx: &LateContext<'a, 'tcx>,
kind: FnKind<'tcx>,
decl: &'tcx FnDecl,
_: &'tcx Body,
fn_sp: Span,
fn_id: NodeId) {
// Get the name of the function, if any.
let fn_name = match kind {
FnKind::ItemFn(name, ..) => name,
_ => return
};
// Figure out if this is a route function by trying to find the
// `ROUTE_ATTR` attribute and extracing the info struct's name from it.
let attr_value = kind.attrs().iter().filter_map(|attr| {
match attr.check_name(ROUTE_ATTR) {
false => None,
true => attr.value.meta_item_list().and_then(|list| list[0].name())
}
}).next();
// Try to get the DEF_ID using the info struct's name from the
// `info_structs` map. Return early if anything goes awry.
let def_id = match attr_value {
Some(val) if self.info_structs.contains_key(&val) => {
self.info_structs.get(&val).unwrap()
}
_ => return
};
// Add this to the list of declared routes to check for mounting later
// unless unmounted routes were explicitly allowed for this function.
if cx.current_level(UNMOUNTED_ROUTE) != Level::Allow {
self.declared.push((fn_name, fn_sp, def_id.clone()));
}
// If unmanaged state was explicitly allowed for this function, don't
// record any additional information. Just return now.
if cx.current_level(UNMANAGED_STATE) == Level::Allow {
return;
}
// Collect all of the `State` types and spans into `tys` and `spans`.
let mut ty_and_spans: Vec<(Ty<'static>, Span)> = vec![];
if let Some(sig) = cx.tables.liberated_fn_sigs.get(&fn_id) {
for (i, input_ty) in sig.inputs().iter().enumerate() {
let def_id = match input_ty.ty_to_def_id() {
Some(id) => id,
None => continue
};
if match_def_path(cx.tcx, def_id, STATE_TYPE) {
if let Some(inner_type) = input_ty.walk_shallow().next() {
if decl.inputs.len() <= i {
println!("internal lint error: \
signature and declaration length mismatch: \
{:?}, {:?}", sig.inputs(), decl.inputs);
println!("this is likely a bug. please report this.");
continue;
}
let ty = unsafe { transmute(inner_type) };
let span = decl.inputs[i].span;
ty_and_spans.push((ty, span));
}
}
}
}
// Insert the information we've collected.
for (ty, span) in ty_and_spans {
self.requested.push((fn_name, fn_sp, def_id.clone(), ty, span));
}
}
fn check_crate_post(&mut self, cx: &LateContext<'a, 'tcx>, _: &'tcx Crate) {
// Iterate through all the instances, emitting warnings.
for (instance, info) in self.instances.iter() {
self.unmounted_warnings(cx, *instance, info);
self.unmanaged_warnings(cx, *instance, info);
}
}
}
impl RocketLint {
fn unmounted_warnings(&self, cx: &LateContext,
rcvr: Option<Receiver>,
info: &InstanceInfo) {
// Emit a warning for all unmounted, declared routes.
for &(route_name, fn_sp, info_def_id) in self.declared.iter() {
if !info.mounted.contains_key(&info_def_id) {
let help_span = rcvr.map(|r| r.span());
msg_and_help(cx, UNMOUNTED_ROUTE, fn_sp,
&format!("the '{}' route is not mounted", route_name),
"Rocket will not dispatch requests to unmounted routes.",
help_span, "maybe add a call to `mount` here?");
}
}
}
fn unmanaged_warnings(&self,
cx: &LateContext,
rcvr: Option<Receiver>,
info: &InstanceInfo) {
for &(_, _, info_def_id, ty, sp) in self.requested.iter() {
// Don't warn on unmounted routes.
if !info.mounted.contains_key(&info_def_id) { continue }
if !info.managed.contains_key(&ty) {
let help_span = rcvr.map(|r| r.span());
msg_and_help(cx, UNMANAGED_STATE, sp,
&format!("'{}' is not currently being managed by Rocket", ty),
"this 'State' request guard will always fail",
help_span, "maybe add a call to `manage` here?");
}
}
}
}

View File

@ -1,183 +0,0 @@
use rustc::ty;
use rustc::hir::def_id::DefId;
use rustc::lint::{LintContext, Lint, LateContext};
use rustc::hir::Expr_::*;
use rustc::hir::Expr;
use rustc::hir::def::Def;
use syntax::symbol;
use syntax_pos::Span;
const ROCKET_TYPE: &'static [&'static str] = &["rocket", "rocket", "Rocket"];
const ROCKET_IGNITE_FN: &'static [&'static str] = &["rocket", "ignite"];
const ROCKET_IGNITE_STATIC: &'static [&'static str] = &["rocket", "rocket",
"Rocket", "ignite"];
const ROCKET_CUSTOM_FN: &'static [&'static str] = &["rocket", "custom"];
const ROCKET_CUSTOM_STATIC: &'static [&'static str] = &["rocket", "rocket",
"Rocket", "custom"];
const ABSOLUTE: &'static ty::item_path::RootMode =
&ty::item_path::RootMode::Absolute;
/// Check if a `DefId`'s path matches the given absolute type path usage.
pub fn match_def_path(tcx: ty::TyCtxt, def_id: DefId, path: &[&str]) -> bool {
struct AbsolutePathBuffer {
names: Vec<symbol::InternedString>,
}
impl ty::item_path::ItemPathBuffer for AbsolutePathBuffer {
fn root_mode(&self) -> &ty::item_path::RootMode {
ABSOLUTE
}
fn push(&mut self, text: &str) {
self.names.push(symbol::Symbol::intern(text).as_str());
}
}
let mut apb = AbsolutePathBuffer { names: vec![] };
tcx.push_item_path(&mut apb, def_id);
apb.names.len() == path.len() &&
apb.names.iter().zip(path.iter()).all(|(a, &b)| &**a == b)
}
/// Check if the method call given in `expr` belongs to given type.
pub fn is_impl_method(cx: &LateContext, expr: &Expr, path: &[&str]) -> bool {
let method_call = ty::MethodCall::expr(expr.id);
let trt_id = cx.tables
.method_map
.get(&method_call)
.and_then(|callee| cx.tcx.impl_of_method(callee.def_id));
if let Some(trt_id) = trt_id {
match_def_path(cx.tcx, trt_id, path)
} else {
false
}
}
pub fn find_initial_receiver<'e>(cx: &LateContext,
expr: &'e Expr)
-> Option<&'e Expr> {
match expr.node {
ExprMethodCall(_, _, ref args) => find_initial_receiver(cx, &args[0]),
ExprCall(..) if is_rocket_start_call(cx, expr) => Some(expr),
ExprCall(ref call, _) => find_initial_receiver(cx, call),
ExprPath(_) => Some(expr),
_ => None,
}
}
pub fn rocket_method_call<'e>(method: &str,
cx: &LateContext,
expr: &'e Expr)
-> (Option<(Option<&'e Expr>, &'e [Expr])>) {
if let ExprMethodCall(ref name, _, ref exprs) = expr.node {
if &*name.node.as_str() == method && is_impl_method(cx, expr, ROCKET_TYPE) {
let receiver = find_initial_receiver(cx, &exprs[0]);
return Some((receiver, &exprs[1..]));
}
}
None
}
pub fn is_rocket_start_call(cx: &LateContext, expr: &Expr) -> bool {
if let ExprCall(ref expr, ..) = expr.node {
if let ExprPath(ref qpath) = expr.node {
let def_id = cx.tables.qpath_def(qpath, expr.id).def_id();
if match_def_path(cx.tcx, def_id, ROCKET_IGNITE_FN) {
return true;
} else if match_def_path(cx.tcx, def_id, ROCKET_IGNITE_STATIC) {
return true;
} else if match_def_path(cx.tcx, def_id, ROCKET_CUSTOM_FN) {
return true;
} else if is_impl_method(cx, expr, ROCKET_CUSTOM_STATIC) {
return true;
}
}
}
false
}
pub fn extract_mount_fn_def_ids(cx: &LateContext, expr: &Expr) -> Vec<DefId> {
let mut output = Vec::new();
// Call to into_vec
if let ExprCall(_, ref args) = expr.node {
if let Some(&ExprBox(ref expr)) = args.iter().next().map(|e| &e.node) {
// Array of routes.
if let ExprArray(ref members) = expr.node {
for expr in members.iter() {
// Route::From call
if let ExprCall(_, ref args) = expr.node {
if args.len() < 1 {
continue;
}
// address of info struct
if let ExprAddrOf(_, ref expr) = args[0].node {
// path to info_struct
if let ExprPath(ref qpath) = expr.node {
let def = cx.tables.qpath_def(qpath, expr.id);
output.push(def.def_id());
}
}
}
}
}
}
}
output
}
pub fn returns_rocket_instance(cx: &LateContext, expr: &Expr) -> bool {
if let Some(ref ty) = cx.tables.expr_ty_opt(expr) {
if let Some(def_id) = ty.ty_to_def_id() {
if match_def_path(cx.tcx, def_id, ROCKET_TYPE) {
return true;
}
}
}
false
}
pub trait DefExt {
fn def_id_opt(&self) -> Option<DefId>;
}
impl DefExt for Def {
fn def_id_opt(&self) -> Option<DefId> {
match *self {
Def::Fn(id) | Def::Mod(id) | Def::Static(id, _) | Def::Variant(id)
| Def::VariantCtor(id, ..) | Def::Enum(id) | Def::TyAlias(id)
| Def::AssociatedTy(id) | Def::TyParam(id) | Def::Struct(id)
| Def::StructCtor(id, ..) | Def::Union(id) | Def::Trait(id)
| Def::Method(id) | Def::Const(id) | Def::AssociatedConst(id)
| Def::Local(id) | Def::Upvar(id, ..) | Def::Macro(id) => Some(id),
Def::Label(..) | Def::PrimTy(..) | Def::SelfTy(..) | Def::Err => None,
}
}
}
pub fn msg_and_help<'a, T: LintContext<'a>>(cx: &T,
lint: &'static Lint,
msg_sp: Span,
msg: &str,
note: &str,
help_sp: Option<Span>,
help: &str) {
let mut b = cx.struct_span_lint(lint, msg_sp, msg);
b.note(note);
if let Some(span) = help_sp {
b.span_help(span, help);
}
b.emit();
}

View File

@ -9,7 +9,7 @@ use utils::{span, MetaItemExt, SpanExt, is_valid_ident};
use super::{Function, ParamIter};
use super::keyvalue::KVSpanned;
use super::uri::validate_uri;
use rocket::http::{Method, ContentType};
use rocket::http::{Method, MediaType};
use rocket::http::uri::URI;
/// This structure represents the parsed `route` attribute.
@ -25,7 +25,7 @@ pub struct RouteParams {
pub uri: Spanned<URI<'static>>,
pub data_param: Option<KVSpanned<Ident>>,
pub query_param: Option<Spanned<Ident>>,
pub format: Option<KVSpanned<ContentType>>,
pub format: Option<KVSpanned<MediaType>>,
pub rank: Option<KVSpanned<isize>>,
}
@ -258,25 +258,25 @@ fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> isize {
-1
}
fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> ContentType {
fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> MediaType {
if let LitKind::Str(ref s, _) = *kv.value() {
if let Ok(ct) = ContentType::from_str(&s.as_str()) {
if let Ok(ct) = MediaType::from_str(&s.as_str()) {
if !ct.is_known() {
let msg = format!("'{}' is not a known content-type", s);
let msg = format!("'{}' is not a known media type", s);
ecx.span_warn(kv.value.span, &msg);
}
return ct;
} else {
ecx.span_err(kv.value.span, "malformed content-type");
ecx.span_err(kv.value.span, "malformed media type");
}
}
ecx.struct_span_err(kv.span, r#"`format` must be a "content/type""#)
ecx.struct_span_err(kv.span, r#"`format` must be a "media/type""#)
.help(r#"format, if specified, must be a key-value pair where
the key is `format` and the value is a string representing the
content-type accepted. e.g: format = "application/json""#)
media type accepted. e.g: format = "application/json""#)
.emit();
ContentType::Any
MediaType::Any
}

View File

@ -60,9 +60,9 @@ fn valid_segments(ecx: &ExtCtxt, uri: &URI, sp: Span) -> bool {
ecx.struct_span_err(span, "parameter names must be valid identifiers")
.note(&format!("{:?} is not a valid identifier", param))
.emit();
} else if param.starts_with('_') {
ecx.struct_span_err(span, "parameters cannot be ignored")
.note(&format!("{:?} is being ignored", param))
} else if param == "_" {
ecx.struct_span_err(span, "parameters must be named")
.help("use a name such as `_guard` or `_param`")
.emit();
} else {
continue

View File

@ -17,7 +17,7 @@ use syntax::parse::token::Token;
use syntax::tokenstream::TokenTree;
use syntax::ast::{Item, Expr};
use syntax::ext::base::{Annotatable, ExtCtxt};
use syntax::codemap::{spanned, Span, Spanned, DUMMY_SP};
use syntax::codemap::{Span, Spanned, DUMMY_SP};
use syntax::ext::quote::rt::ToTokens;
use syntax::print::pprust::item_to_string;
use syntax::ptr::P;
@ -26,7 +26,7 @@ use syntax::ast::{Attribute, Lifetime, LifetimeDef, Ty};
use syntax::attr::HasAttrs;
pub fn span<T>(t: T, span: Span) -> Spanned<T> {
spanned(span.lo, span.hi, t)
Spanned { node: t, span: span }
}
pub fn sep_by_tok<T>(ecx: &ExtCtxt, things: &[T], token: Token) -> Vec<TokenTree>

View File

@ -1,8 +1,6 @@
use syntax::codemap::{Span, BytePos};
pub trait SpanExt {
fn shorten_to(self, to_length: usize) -> Span;
/// Trim the span on the left and right by `length`.
fn trim(self, length: u32) -> Span;
@ -11,6 +9,12 @@ pub trait SpanExt {
/// Trim the span on the right by `length`.
fn trim_right(self, length: usize) -> Span;
// Trim from the right so that the span is `length` in size.
fn shorten_to(self, to_length: usize) -> Span;
// Trim from the left so that the span is `length` in size.
fn shorten_upto(self, length: usize) -> Span;
}
impl SpanExt for Span {
@ -29,6 +33,11 @@ impl SpanExt for Span {
self
}
fn shorten_upto(mut self, length: usize) -> Span {
self.lo = self.hi - BytePos(length as u32);
self
}
fn trim(mut self, length: u32) -> Span {
self.lo = self.lo + BytePos(length);
self.hi = self.hi - BytePos(length);

View File

@ -5,9 +5,9 @@
fn get() -> &'static str { "hi" }
#[get("")] //~ ERROR absolute
fn get1(name: &str) -> &'static str { "hi" }
fn get1(id: usize) -> &'static str { "hi" }
#[get("a/b/c")] //~ ERROR absolute
fn get2(name: &str) -> &'static str { "hi" }
fn get2(id: usize) -> &'static str { "hi" }
fn main() { }

View File

@ -4,6 +4,6 @@
extern crate rocket;
#[get("/")]
fn get(_: &str) -> &'static str { "hi" } //~ ERROR argument
fn get(_: usize) -> &'static str { "hi" } //~ ERROR argument
fn main() { }

View File

@ -12,7 +12,7 @@ fn get1() -> &'static str { "hi" }
#[get(path = "/", rank = "2")] //~ ERROR must be an int
fn get2() -> &'static str { "hi" }
#[get(path = "/", format = 100)] //~ ERROR must be a "content/type"
#[get(path = "/", format = 100)] //~ ERROR must be a "media/type"
fn get3() -> &'static str { "hi" }
fn main() {

View File

@ -0,0 +1,111 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]
#[derive(FromForm)]
struct MyForm {
#[form(field = "blah", field = "bloo")]
//~^ ERROR: incorrect use of attribute
my_field: String,
}
#[derive(FromForm)]
struct MyForm1 {
#[form]
//~^ ERROR: incorrect use of attribute
my_field: String,
}
#[derive(FromForm)]
struct MyForm2 {
#[form("blah")]
//~^ ERROR: invalid `form` attribute
my_field: String,
}
#[derive(FromForm)]
struct MyForm3 {
#[form(123)]
//~^ ERROR: invalid `form` attribute
my_field: String,
}
#[derive(FromForm)]
struct MyForm4 {
#[form(beep = "bop")]
//~^ ERROR: invalid `form` attribute
my_field: String,
}
#[derive(FromForm)]
struct MyForm5 {
#[form(field = "blah")]
#[form(field = "blah")]
my_field: String,
//~^ ERROR: only a single
}
#[derive(FromForm)]
struct MyForm6 {
#[form(field = true)]
//~^ ERROR: invalid `field` in attribute
my_field: String,
}
#[derive(FromForm)]
struct MyForm7 {
#[form(field)]
//~^ ERROR: invalid `field` in attribute
my_field: String,
}
#[derive(FromForm)]
struct MyForm8 {
#[form(field = 123)]
//~^ ERROR: invalid `field` in attribute
my_field: String,
}
#[derive(FromForm)]
struct MyForm9 {
#[form(field = "hello")]
first: String,
#[form(field = "hello")]
//~^ ERROR: field with duplicate name
other: String,
}
#[derive(FromForm)]
struct MyForm10 {
first: String,
#[form(field = "first")]
//~^ ERROR: field with duplicate name
other: String,
}
#[derive(FromForm)]
struct MyForm11 {
#[form(field = "hello&world")]
//~^ ERROR: invalid form field
first: String,
}
#[derive(FromForm)]
struct MyForm12 {
#[form(field = "!@#$%^&*()_")]
//~^ ERROR: invalid form field
first: String,
}
#[derive(FromForm)]
struct MyForm13 {
#[form(field = "?")]
//~^ ERROR: invalid form field
first: String,
}
#[derive(FromForm)]
struct MyForm14 {
#[form(field = "")]
//~^ ERROR: invalid form field
first: String,
}

View File

@ -1,13 +1,16 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
#[get("/<name>")] //~ ERROR 'name' is declared
fn get(other: &str) -> &'static str { "hi" } //~ ERROR isn't in the function
#[get("/<name>")] //~ ERROR unused dynamic parameter: `name`
fn get(other: usize) -> &'static str { "hi" } //~ NOTE expected
#[get("/a?<r>")] //~ ERROR 'r' is declared
fn get1() -> &'static str { "hi" } //~ ERROR isn't in the function
#[get("/a?<r>")] //~ ERROR unused dynamic parameter: `r`
fn get1() -> &'static str { "hi" } //~ NOTE expected
#[post("/a", data = "<test>")] //~ ERROR 'test' is declared
fn post() -> &'static str { "hi" } //~ ERROR isn't in the function
#[post("/a", data = "<test>")] //~ ERROR unused dynamic parameter: `test`
fn post() -> &'static str { "hi" } //~ NOTE expected
#[get("/<_r>")] //~ ERROR unused dynamic parameter: `_r`
fn get2(r: usize) -> &'static str { "hi" } //~ NOTE expected
fn main() { }

View File

@ -1,70 +0,0 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
#![allow(dead_code)]
#![deny(unmanaged_state)]
extern crate rocket;
use rocket::State;
struct MyType;
struct MySecondType;
mod external {
#[get("/state/extern")]
fn unmanaged(_c: ::State<i32>) { }
//~^ ERROR not currently being managed
#[get("/state/extern")]
fn managed(_c: ::State<u32>) { }
//~^ WARN is not mounted
#[get("/state/extern")]
fn unmanaged_unmounted(_c: ::State<u8>) { }
//~^ WARN is not mounted
//~^^ WARN is not mounted
}
#[get("/state/bad")]
fn unmanaged(_b: State<MySecondType>) { }
//~^ ERROR not currently being managed
//~^^ ERROR not currently being managed
#[get("/state/ok")]
fn managed(_a: State<u32>) { }
#[get("/state/bad")]
fn managed_two(_b: State<MyType>) { }
#[get("/state/ok")]
fn unmounted_doesnt_error(_a: State<i8>) { }
//~^ WARN is not mounted
//~^^ WARN is not mounted
#[get("/ignored")]
#[allow(unmanaged_state)]
fn ignored(_b: State<u16>) { }
//~^ WARN is not mounted
#[get("/unmounted/ignored")]
#[allow(unmounted_route)]
fn unmounted_ignored() { }
#[get("/mounted/nonce")]
fn mounted_only_once() { }
//~^ WARN is not mounted
fn main() {
rocket::ignite()
.mount("/", routes![managed, unmanaged, external::unmanaged])
.mount("/", routes![managed_two, ignored, mounted_only_once])
.manage(MyType)
.manage(100u32);
rocket::ignite()
.mount("/", routes![managed, unmanaged, external::unmanaged])
.mount("/", routes![external::managed, managed_two])
.manage(MyType)
.manage(100i32)
.manage(100u32);
}

View File

@ -4,35 +4,35 @@
extern crate rocket;
#[get("/", format = "applicationx-custom")] //~ ERROR malformed
//~^ ERROR `format` must be a "content/type"
//~^ ERROR `format` must be a "media/type"
fn one() -> &'static str { "hi" }
#[get("/", format = "")] //~ ERROR malformed
//~^ ERROR `format` must be a "content/type"
//~^ ERROR `format` must be a "media/type"
fn two() -> &'static str { "hi" }
#[get("/", format = "//")] //~ ERROR malformed
//~^ ERROR `format` must be a "content/type"
//~^ ERROR `format` must be a "media/type"
fn three() -> &'static str { "hi" }
#[get("/", format = "/")] //~ ERROR malformed
//~^ ERROR `format` must be a "content/type"
//~^ ERROR `format` must be a "media/type"
fn four() -> &'static str { "hi" }
#[get("/", format = "a/")] //~ ERROR malformed
//~^ ERROR `format` must be a "content/type"
//~^ ERROR `format` must be a "media/type"
fn five() -> &'static str { "hi" }
#[get("/", format = "/a")] //~ ERROR malformed
//~^ ERROR `format` must be a "content/type"
//~^ ERROR `format` must be a "media/type"
fn six() -> &'static str { "hi" }
#[get("/", format = "/a/")] //~ ERROR malformed
//~^ ERROR `format` must be a "content/type"
//~^ ERROR `format` must be a "media/type"
fn seven() -> &'static str { "hi" }
#[get("/", format = "a/b/")] //~ ERROR malformed
//~^ ERROR `format` must be a "content/type"
//~^ ERROR `format` must be a "media/type"
fn eight() -> &'static str { "hi" }
fn main() { }

View File

@ -4,16 +4,16 @@
#[get("/><")] //~ ERROR malformed
fn get() -> &'static str { "hi" }
#[get("/<name><")] //~ ERROR malformed
fn get1(name: &str) -> &'static str { "hi" }
#[get("/<id><")] //~ ERROR malformed
fn get1(id: usize) -> &'static str { "hi" }
#[get("/<<<<name><")] //~ ERROR malformed
fn get2(name: &str) -> &'static str { "hi" }
#[get("/<<<<id><")] //~ ERROR malformed
fn get2(id: usize) -> &'static str { "hi" }
#[get("/<!>")] //~ ERROR identifiers
fn get3() -> &'static str { "hi" }
#[get("/<_>")] //~ ERROR ignored
#[get("/<_>")] //~ ERROR named
fn get4() -> &'static str { "hi" }
#[get("/<1>")] //~ ERROR identifiers

View File

@ -1,17 +1,17 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
#[get("/<param>")] //~ ERROR declared
fn get() { } //~ ERROR isn't in the function signature
#[get("/<param>")] //~ ERROR unused dynamic parameter: `param`
fn get() { } //~ NOTE expected
#[get("/<a>")] //~ ERROR declared
fn get2() { } //~ ERROR isn't in the function signature
#[get("/<a>")] //~ ERROR unused dynamic parameter: `a`
fn get2() { } //~ NOTE expected
#[get("/a/b/c/<a>/<b>")]
//~^ ERROR 'a' is declared
//~^^ ERROR 'b' is declared
//~^ ERROR unused dynamic parameter: `a`
//~^^ ERROR unused dynamic parameter: `b`
fn get32() { }
//~^ ERROR isn't in the function signature
//~^^ ERROR isn't in the function signature
//~^ NOTE expected
//~^^ NOTE expected
fn main() { }

View File

@ -1,14 +0,0 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
#![allow(dead_code)]
#![deny(unmounted_route)]
extern crate rocket;
#[get("/")]
fn index() { }
//~^ ERROR is not mounted
fn main() {
rocket::ignite().launch();
}

View File

@ -3,13 +3,13 @@
extern crate rocket;
#[get("/", format = "application/x-custom")] //~ WARNING not a known content-type
#[get("/", format = "application/x-custom")] //~ WARNING not a known media type
fn one() -> &'static str { "hi" }
#[get("/", format = "x-custom/plain")] //~ WARNING not a known content-type
#[get("/", format = "x-custom/plain")] //~ WARNING not a known media type
fn two() -> &'static str { "hi" }
#[get("/", format = "x-custom/x-custom")] //~ WARNING not a known content-type
#[get("/", format = "x-custom/x-custom")] //~ WARNING not a known media type
fn three() -> &'static str { "hi" }
// Make the test fail here so we can actually check for the warnings above.

View File

@ -3,20 +3,20 @@
extern crate rocket;
use rocket::http::Cookies;
use rocket::http::{Cookies, RawStr};
use rocket::request::Form;
#[derive(FromForm)]
struct User<'a> {
name: &'a str,
name: &'a RawStr,
nickname: String,
}
#[post("/<name>?<query>", format = "application/json", data = "<user>", rank = 2)]
fn get<'r>(name: &str,
query: User<'r>,
#[post("/<name>?<_query>", format = "application/json", data = "<user>", rank = 2)]
fn get<'r>(name: &RawStr,
_query: User<'r>,
user: Form<'r, User<'r>>,
cookies: &Cookies)
cookies: Cookies)
-> &'static str {
"hi"
}

View File

@ -4,6 +4,7 @@
extern crate rocket;
use rocket::request::{FromForm, FromFormValue, FormItems};
use rocket::http::RawStr;
#[derive(Debug, PartialEq, FromForm)]
struct TodoTask {
@ -20,8 +21,8 @@ enum FormOption {
impl<'v> FromFormValue<'v> for FormOption {
type Error = &'v str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
let variant = match v {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
let variant = match v.as_str() {
"a" => FormOption::A,
"b" => FormOption::B,
"c" => FormOption::C,
@ -37,19 +38,19 @@ struct FormInput<'r> {
checkbox: bool,
number: usize,
radio: FormOption,
password: &'r str,
password: &'r RawStr,
textarea: String,
select: FormOption,
}
#[derive(Debug, PartialEq, FromForm)]
struct DefaultInput<'r> {
arg: Option<&'r str>,
arg: Option<&'r RawStr>,
}
#[derive(Debug, PartialEq, FromForm)]
struct ManualMethod<'r> {
_method: Option<&'r str>,
_method: Option<&'r RawStr>,
done: bool
}
@ -61,33 +62,46 @@ struct UnpresentCheckbox {
#[derive(Debug, PartialEq, FromForm)]
struct UnpresentCheckboxTwo<'r> {
checkbox: bool,
something: &'r str
something: &'r RawStr
}
fn parse<'f, T: FromForm<'f>>(string: &'f str) -> Option<T> {
#[derive(Debug, PartialEq, FromForm)]
struct FieldNamedV<'r> {
v: &'r RawStr,
}
fn parse<'f, T: FromForm<'f>>(string: &'f str, strict: bool) -> Option<T> {
let mut items = FormItems::from(string);
let result = T::from_form_items(items.by_ref());
if !items.exhausted() {
let result = T::from_form(items.by_ref(), strict);
if !items.exhaust() {
panic!("Invalid form input.");
}
result.ok()
}
fn strict<'f, T: FromForm<'f>>(string: &'f str) -> Option<T> {
parse(string, true)
}
fn lenient<'f, T: FromForm<'f>>(string: &'f str) -> Option<T> {
parse(string, false)
}
fn main() {
// Same number of arguments: simple case.
let task: Option<TodoTask> = parse("description=Hello&completed=on");
let task: Option<TodoTask> = strict("description=Hello&completed=on");
assert_eq!(task, Some(TodoTask {
description: "Hello".to_string(),
completed: true
}));
// Argument in string but not in form.
let task: Option<TodoTask> = parse("other=a&description=Hello&completed=on");
let task: Option<TodoTask> = strict("other=a&description=Hello&completed=on");
assert!(task.is_none());
// Ensure _method isn't required.
let task: Option<TodoTask> = parse("_method=patch&description=Hello&completed=off");
let task: Option<TodoTask> = strict("_method=patch&description=Hello&completed=off");
assert_eq!(task, Some(TodoTask {
description: "Hello".to_string(),
completed: false
@ -98,46 +112,88 @@ fn main() {
"checkbox=off", "textarea=", "select=a", "radio=c",
].join("&");
let input: Option<FormInput> = parse(&form_string);
let input: Option<FormInput> = strict(&form_string);
assert_eq!(input, Some(FormInput {
checkbox: false,
number: 10,
radio: FormOption::C,
password: "testing",
password: "testing".into(),
textarea: "".to_string(),
select: FormOption::A,
}));
// Argument not in string with default in form.
let default: Option<DefaultInput> = parse("");
let default: Option<DefaultInput> = strict("");
assert_eq!(default, Some(DefaultInput {
arg: None
}));
// Ensure _method can be captured if desired.
let manual: Option<ManualMethod> = parse("_method=put&done=true");
let manual: Option<ManualMethod> = strict("_method=put&done=true");
assert_eq!(manual, Some(ManualMethod {
_method: Some("put"),
_method: Some("put".into()),
done: true
}));
let manual: Option<ManualMethod> = lenient("_method=put&done=true");
assert_eq!(manual, Some(ManualMethod {
_method: Some("put".into()),
done: true
}));
// And ignored when not present.
let manual: Option<ManualMethod> = parse("done=true");
let manual: Option<ManualMethod> = strict("done=true");
assert_eq!(manual, Some(ManualMethod {
_method: None,
done: true
}));
// Check that a `bool` value that isn't in the form is marked as `false`.
let manual: Option<UnpresentCheckbox> = parse("");
let manual: Option<UnpresentCheckbox> = strict("");
assert_eq!(manual, Some(UnpresentCheckbox {
checkbox: false
}));
// Check that a `bool` value that isn't in the form is marked as `false`.
let manual: Option<UnpresentCheckboxTwo> = parse("something=hello");
let manual: Option<UnpresentCheckboxTwo> = strict("something=hello");
assert_eq!(manual, Some(UnpresentCheckboxTwo {
checkbox: false,
something: "hello"
something: "hello".into()
}));
// Check that a structure with one field `v` parses correctly.
let manual: Option<FieldNamedV> = strict("v=abc");
assert_eq!(manual, Some(FieldNamedV {
v: "abc".into()
}));
// Check that a structure with one field `v` parses correctly (lenient).
let manual: Option<FieldNamedV> = lenient("v=abc");
assert_eq!(manual, Some(FieldNamedV { v: "abc".into() }));
let manual: Option<FieldNamedV> = lenient("v=abc&a=123");
assert_eq!(manual, Some(FieldNamedV { v: "abc".into() }));
let manual: Option<FieldNamedV> = lenient("c=abcddef&v=abc&a=123");
assert_eq!(manual, Some(FieldNamedV { v: "abc".into() }));
// Check default values (bool) with lenient parsing.
let manual: Option<UnpresentCheckboxTwo> = lenient("something=hello");
assert_eq!(manual, Some(UnpresentCheckboxTwo {
checkbox: false,
something: "hello".into()
}));
let manual: Option<UnpresentCheckboxTwo> = lenient("hi=hi&something=hello");
assert_eq!(manual, Some(UnpresentCheckboxTwo {
checkbox: false,
something: "hello".into()
}));
// Check that a missing field doesn't parse, even leniently.
let manual: Option<FieldNamedV> = lenient("a=abc");
assert!(manual.is_none());
let manual: Option<FieldNamedV> = lenient("_method=abc");
assert!(manual.is_none());
}

View File

@ -4,8 +4,11 @@
extern crate rocket;
#[get("/test/<one>/<two>/<three>")]
fn get(one: &str, two: usize, three: isize) -> &'static str { "hi" }
fn get(one: String, two: usize, three: isize) -> &'static str { "hi" }
#[get("/test/<_one>/<_two>/<__three>")]
fn ignored(_one: String, _two: usize, __three: isize) -> &'static str { "hi" }
fn main() {
let _ = routes![get];
let _ = routes![get, ignored];
}

View File

@ -10,6 +10,9 @@ struct Form { }
fn main() {
// Same number of arguments: simple case.
let task = Form::from_form_items(&mut FormItems::from(""));
let task = Form::from_form(&mut FormItems::from(""), true);
assert_eq!(task, Ok(Form { }));
let task = Form::from_form(&mut FormItems::from(""), false);
assert_eq!(task, Ok(Form { }));
}

View File

@ -0,0 +1,61 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]
extern crate rocket;
use rocket::request::{FromForm, FromFormValue, FormItems};
use rocket::http::RawStr;
#[derive(Debug, PartialEq, FromForm)]
struct Form {
single: usize,
#[form(field = "camelCase")]
camel_case: String,
#[form(field = "TitleCase")]
title_case: String,
#[form(field = "type")]
field_type: isize,
#[form(field = "DOUBLE")]
double: String,
#[form(field = "a.b")]
dot: isize,
}
fn parse<'f, T: FromForm<'f>>(string: &'f str, strict: bool) -> Option<T> {
let mut items = FormItems::from(string);
let result = T::from_form(items.by_ref(), strict);
if !items.exhaust() {
panic!("Invalid form input.");
}
result.ok()
}
fn parse_strict<'f, T: FromForm<'f>>(string: &'f str) -> Option<T> {
parse(string, true)
}
fn main() {
let form_string = &[
"single=100", "camelCase=helloThere", "TitleCase=HiHi", "type=-2",
"DOUBLE=bing_bong", "a.b=123",
].join("&");
let form: Option<Form> = parse_strict(&form_string);
assert_eq!(form, Some(Form {
single: 100,
camel_case: "helloThere".into(),
title_case: "HiHi".into(),
field_type: -2,
double: "bing_bong".into(),
dot: 123,
}));
let form_string = &[
"single=100", "camel_case=helloThere", "TitleCase=HiHi", "type=-2",
"DOUBLE=bing_bong", "dot=123",
].join("&");
let form: Option<Form> = parse_strict(&form_string);
assert!(form.is_none());
}

View File

@ -1,7 +1,6 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
#![allow(dead_code)]
#![deny(unmounted_route)]
extern crate rocket;

View File

@ -4,7 +4,7 @@
extern crate rocket;
#[get("/<todo>")]
fn todo(todo: &str) -> &str {
fn todo(todo: String) -> String {
todo
}

View File

@ -1,7 +1,6 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
#![allow(dead_code, unused_variables)]
#![deny(unmounted_routes, unmanaged_state)]
extern crate rocket;

View File

@ -1,7 +1,6 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
#![allow(dead_code, unused_variables)]
#![deny(unmounted_route, unmanaged_state)]
extern crate rocket;

View File

@ -3,7 +3,7 @@ extern crate compiletest_rs as compiletest;
use std::path::PathBuf;
fn run_mode(mode: &'static str) {
let mut config = compiletest::default_config();
let mut config = compiletest::Config::default();
let cfg_mode = mode.parse().expect("Invalid mode");
config.mode = cfg_mode;

View File

@ -1,6 +1,6 @@
[package]
name = "rocket_contrib"
version = "0.2.0"
version = "0.4.0-dev"
authors = ["Sergio Benitez <sb@sergio.bz>"]
description = "Community contributed libraries for the Rocket web framework."
documentation = "https://api.rocket.rs/rocket_contrib/"
@ -13,26 +13,29 @@ license = "MIT/Apache-2.0"
[features]
default = ["json"]
json = ["serde", "serde_json"]
msgpack = ["serde", "rmp-serde"]
tera_templates = ["tera", "templates"]
handlebars_templates = ["handlebars", "templates"]
# Internal use only.
templates = ["serde", "serde_json", "lazy_static_macro", "glob"]
lazy_static_macro = ["lazy_static"]
templates = ["serde", "serde_json", "glob"]
[dependencies]
rocket = { version = "0.2.0", path = "../lib/" }
rocket = { version = "0.4.0-dev", path = "../lib/" }
log = "^0.3"
# UUID dependencies.
uuid = { version = "^0.4", optional = true }
uuid = { version = "^0.5", optional = true }
# JSON and templating dependencies.
serde = { version = "^0.9", optional = true }
serde_json = { version = "^0.9.3", optional = true }
# Serialization and templating dependencies.
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
rmp-serde = { version = "^0.13", optional = true }
# Templating dependencies only.
handlebars = { version = "^0.25", optional = true, features = ["serde_type"] }
handlebars = { version = "^0.29", optional = true }
glob = { version = "^0.2", optional = true }
lazy_static = { version = "^0.2", optional = true }
tera = { version = "^0.7", optional = true }
tera = { version = "^0.10", optional = true }
[package.metadata.docs.rs]
all-features = true

323
contrib/src/json.rs Normal file
View File

@ -0,0 +1,323 @@
use std::ops::{Deref, DerefMut};
use std::io::Read;
use rocket::outcome::{Outcome, IntoOutcome};
use rocket::request::Request;
use rocket::data::{self, Data, FromData};
use rocket::response::{self, Responder, content};
use rocket::http::Status;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json;
pub use serde_json::error::Error as SerdeError;
/// The JSON type: implements `FromData` and `Responder`, allowing you to easily
/// consume and respond with JSON.
///
/// ## Receiving JSON
///
/// If you're receiving JSON data, simply add a `data` parameter to your route
/// arguments and ensure the type of the parameter is a `Json<T>`, where `T` is
/// some type you'd like to parse from JSON. `T` must implement `Deserialize` or
/// `DeserializeOwned` from [Serde](https://github.com/serde-rs/json). The data
/// is parsed from the HTTP request body.
///
/// ```rust,ignore
/// #[post("/users/", format = "application/json", data = "<user>")]
/// 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 `Content-Type` header value will
/// not be routed to the handler.
///
/// ## Sending JSON
///
/// If you're responding with JSON data, return a `Json<T>` type, where `T`
/// implements `Serialize` from [Serde](https://github.com/serde-rs/json). The
/// content type of the response 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)
/// }
/// ```
///
/// ## Incoming Data Limits
///
/// The default size limit for incoming JSON data is 1MiB. Setting a limit
/// protects your application from denial of service (DOS) attacks and from
/// resource exhaustion through high memory consumption. The limit can be
/// increased by setting the `limits.json` configuration parameter. For
/// instance, to increase the JSON limit to 5MiB for all environments, you may
/// add the following to your `Rocket.toml`:
///
/// ```toml
/// [global.limits]
/// json = 5242880
/// ```
#[derive(Debug)]
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.into_inner(), "Hello".to_string());
/// ```
#[inline(always)]
pub fn into_inner(self) -> T {
self.0
}
}
/// Default limit for JSON is 1MB.
const LIMIT: u64 = 1 << 20;
impl<T: DeserializeOwned> FromData for Json<T> {
type Error = SerdeError;
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, SerdeError> {
if !request.content_type().map_or(false, |ct| ct.is_json()) {
error_!("Content-Type is not JSON.");
return Outcome::Forward(data);
}
let size_limit = request.limits().get("json").unwrap_or(LIMIT);
serde_json::from_reader(data.open().take(size_limit))
.map(|val| Json(val))
.map_err(|e| { error_!("Couldn't parse JSON body: {:?}", e); e })
.into_outcome(Status::BadRequest)
}
}
/// Serializes the wrapped value into JSON. Returns a response with Content-Type
/// JSON and a fixed-size body with the serialized value. If serialization
/// fails, an `Err` of `Status::InternalServerError` is returned.
impl<T: Serialize> Responder<'static> for Json<T> {
fn respond_to(self, req: &Request) -> response::Result<'static> {
serde_json::to_string(&self.0).map(|string| {
content::Json(string).respond_to(req).unwrap()
}).map_err(|e| {
error_!("JSON failed to serialize: {:?}", e);
Status::InternalServerError
})
}
}
impl<T> Deref for Json<T> {
type Target = T;
#[inline(always)]
fn deref<'a>(&'a self) -> &'a T {
&self.0
}
}
impl<T> DerefMut for Json<T> {
#[inline(always)]
fn deref_mut<'a>(&'a mut self) -> &'a mut T {
&mut self.0
}
}
/// An arbitrary JSON value.
///
/// This structure wraps `serde`'s [`Value`] type. Importantly, unlike `Value`,
/// this type implements [`Responder`], allowing a value of this type to be
/// returned directly from a handler.
///
/// [`Value`]: https://docs.rs/serde_json/1.0.2/serde_json/value/enum.Value.html
/// [`Responder`]: /rocket/response/trait.Responder.html
///
/// # `Responder`
///
/// The `Responder` implementation for `JsonValue` serializes the represented
/// value into a JSON string and sets the string as the body of a fixed-sized
/// response with a `Content-Type` of `application/json`.
///
/// # Usage
///
/// A value of this type is constructed via the
/// [`json!`](/rocket_contrib/macro.json.html) macro. The macro and this type
/// are typically used to construct JSON values in an ad-hoc fashion during
/// request handling. This looks something like:
///
/// ```rust,ignore
/// use rocket_contrib::JsonValue;
///
/// #[get("/item")]
/// fn get_item() -> JsonValue {
/// json!({
/// "id": 83,
/// "values": [1, 2, 3, 4]
/// })
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Default)]
pub struct JsonValue(pub serde_json::Value);
impl JsonValue {
#[inline(always)]
fn into_inner(self) -> serde_json::Value {
self.0
}
}
impl Deref for JsonValue {
type Target = serde_json::Value;
#[inline(always)]
fn deref<'a>(&'a self) -> &'a Self::Target {
&self.0
}
}
impl DerefMut for JsonValue {
#[inline(always)]
fn deref_mut<'a>(&'a mut self) -> &'a mut Self::Target {
&mut self.0
}
}
impl Into<serde_json::Value> for JsonValue {
#[inline(always)]
fn into(self) -> serde_json::Value {
self.into_inner()
}
}
impl From<serde_json::Value> for JsonValue {
#[inline(always)]
fn from(value: serde_json::Value) -> JsonValue {
JsonValue(value)
}
}
/// Serializes the value into JSON. Returns a response with Content-Type JSON
/// and a fixed-size body with the serialized value.
impl<'a> Responder<'a> for JsonValue {
#[inline]
fn respond_to(self, req: &Request) -> response::Result<'a> {
content::Json(self.0.to_string()).respond_to(req)
}
}
/// A macro to create ad-hoc JSON serializable values using JSON syntax.
///
/// # Usage
///
/// To import the macro, add the `#[macro_use]` attribute to the `extern crate
/// rocket_contrib` invocation:
///
/// ```rust,ignore
/// #[macro_use] extern crate rocket_contrib;
/// ```
///
/// The return type of a `json!` invocation is
/// [`JsonValue`](/rocket_contrib/struct.JsonValue.html). A value created with
/// this macro can be returned from a handler as follows:
///
/// ```rust,ignore
/// use rocket_contrib::JsonValue;
///
/// #[get("/json")]
/// fn get_json() -> JsonValue {
/// json!({
/// "key": "value",
/// "array": [1, 2, 3, 4]
/// })
/// }
/// ```
///
/// The `Responder` implementation for `JsonValue` serializes the value into a
/// JSON string and sets it as the body of the response with a `Content-Type` of
/// `application/json`.
///
/// # Examples
///
/// Create a simple JSON object with two keys: `"username"` and `"id"`:
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let value = json!({
/// "username": "mjordan",
/// "id": 23
/// });
/// # }
/// ```
///
/// Create a more complex object with a nested object and array:
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let value = json!({
/// "code": 200,
/// "success": true,
/// "payload": {
/// "features": ["serde", "json"],
/// "ids": [12, 121],
/// },
/// });
/// # }
/// ```
///
/// Variables or expressions can be interpolated into the JSON literal. Any type
/// interpolated into an array element or object value must implement Serde's
/// `Serialize` trait, while any type interpolated into a object key must
/// implement `Into<String>`.
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let code = 200;
/// let features = vec!["serde", "json"];
///
/// let value = json!({
/// "code": code,
/// "success": code == 200,
/// "payload": {
/// features[0]: features[1]
/// }
/// });
/// # }
/// ```
///
/// Trailing commas are allowed inside both arrays and objects.
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let value = json!([
/// "notice",
/// "the",
/// "trailing",
/// "comma -->",
/// ]);
/// # }
/// ```
#[macro_export]
macro_rules! json {
($($json:tt)+) => {
$crate::JsonValue(json_internal!($($json)+))
};
}

View File

@ -1,220 +0,0 @@
use std::ops::{Deref, DerefMut};
use std::io::Read;
use rocket::outcome::Outcome;
use rocket::request::Request;
use rocket::data::{self, Data, FromData};
use rocket::response::{self, Responder, content};
use rocket::http::Status;
use serde::{Serialize, Deserialize};
use serde_json;
pub use serde_json::Value;
pub use serde_json::error::Error as SerdeError;
/// The JSON type: implements `FromData` and `Responder`, allowing you to easily
/// consume and respond with JSON.
///
/// If you're receiving JSON data, simply add a `data` parameter to your route
/// arguments and ensure the type of the parameter is a `JSON<T>`, 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", data = "<user>")]
/// 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 `Content-Type` header value will
/// not be routed to the handler.
///
/// If you're responding with JSON data, return a `JSON<T>` type, where `T`
/// implements `Serialize` from [Serde](https://github.com/serde-rs/json). The
/// content type of the response 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)
/// }
/// ```
///
#[derive(Debug)]
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.into_inner(), "Hello".to_string());
/// ```
pub fn into_inner(self) -> T {
self.0
}
}
/// Maximum size of JSON is 1MB.
/// TODO: Determine this size from some configuration parameter.
const MAX_SIZE: u64 = 1048576;
impl<T: Deserialize> FromData for JSON<T> {
type Error = SerdeError;
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, SerdeError> {
if !request.content_type().map_or(false, |ct| ct.is_json()) {
error_!("Content-Type is not JSON.");
return Outcome::Forward(data);
}
let reader = data.open().take(MAX_SIZE);
match serde_json::from_reader(reader).map(|val| JSON(val)) {
Ok(value) => Outcome::Success(value),
Err(e) => {
error_!("Couldn't parse JSON body: {:?}", e);
Outcome::Failure((Status::BadRequest, e))
}
}
}
}
// Serializes the wrapped value into JSON. Returns a response with Content-Type
// JSON and a fixed-size body with the serialization. If serialization fails, an
// `Err` of `Status::InternalServerError` is returned.
impl<T: Serialize> Responder<'static> for JSON<T> {
fn respond(self) -> response::Result<'static> {
serde_json::to_string(&self.0).map(|string| {
content::JSON(string).respond().unwrap()
}).map_err(|e| {
error_!("JSON failed to serialize: {:?}", e);
Status::InternalServerError
})
}
}
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 macro to create ad-hoc JSON serializable values using JSON syntax.
///
/// # Usage
///
/// To import the macro, add the `#[macro_use]` attribute to the `extern crate
/// rocket_contrib` invocation:
///
/// ```rust,ignore
/// #[macro_use] extern crate rocket_contrib;
/// ```
///
/// The return type of a macro invocation is
/// [Value](/rocket_contrib/enum.Value.html). A value created with this macro
/// can be returned from a handler as follows:
///
/// ```rust,ignore
/// use rocket_contrib::{JSON, Value};
///
/// #[get("/json")]
/// fn get_json() -> JSON<Value> {
/// JSON(json!({
/// "key": "value",
/// "array": [1, 2, 3, 4]
/// }))
/// }
/// ```
///
/// # Examples
///
/// Create a simple JSON object with two keys: `"username"` and `"id"`:
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let value = json!({
/// "username": "mjordan",
/// "id": 23
/// });
/// # }
/// ```
///
/// Create a more complex object with a nested object and array:
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let value = json!({
/// "code": 200,
/// "success": true,
/// "payload": {
/// "features": ["serde", "json"],
/// "ids": [12, 121],
/// },
/// });
/// # }
/// ```
///
/// Variables or expressions can be interpolated into the JSON literal. Any type
/// interpolated into an array element or object value must implement Serde's
/// `Serialize` trait, while any type interpolated into a object key must
/// implement `Into<String>`.
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let code = 200;
/// let features = vec!["serde", "json"];
///
/// let value = json!({
/// "code": code,
/// "success": code == 200,
/// "payload": {
/// features[0]: features[1]
/// }
/// });
/// # }
/// ```
///
/// Trailing commas are allowed inside both arrays and objects.
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let value = json!([
/// "notice",
/// "the",
/// "trailing",
/// "comma -->",
/// ]);
/// # }
/// ```
#[macro_export]
macro_rules! json {
($($json:tt)+) => {
json_internal!($($json)+)
};
}

View File

@ -1,4 +1,8 @@
#![feature(drop_types_in_const, macro_reexport)]
#![cfg_attr(feature = "templates", feature(conservative_impl_trait))]
// TODO: Version URLs.
#![doc(html_root_url = "https://api.rocket.rs/rocket_contrib/")]
//! This crate contains officially sanctioned contributor libraries that provide
//! functionality commonly used by Rocket applications.
@ -13,7 +17,8 @@
//! common modules exposed by default. The present feature list is below, with
//! an asterisk next to the features that are enabled by default:
//!
//! * [json*](struct.JSON.html)
//! * [json*](struct.Json.html)
//! * [msgpack](struct.MsgPack.html)
//! * [handlebars_templates](struct.Template.html)
//! * [tera_templates](struct.Template.html)
//! * [uuid](struct.UUID.html)
@ -36,10 +41,6 @@
#[macro_use] extern crate log;
#[macro_use] extern crate rocket;
#[cfg_attr(feature = "lazy_static_macro", macro_use)]
#[cfg(feature = "lazy_static_macro")]
extern crate lazy_static;
#[cfg(feature = "serde")]
extern crate serde;
@ -53,7 +54,14 @@ extern crate serde_json;
pub mod json;
#[cfg(feature = "json")]
pub use json::{JSON, SerdeError, Value};
pub use json::{Json, SerdeError, JsonValue};
#[cfg(feature = "msgpack")]
#[doc(hidden)]
pub mod msgpack;
#[cfg(feature = "msgpack")]
pub use msgpack::{MsgPack, MsgPackError};
#[cfg(feature = "templates")]
mod templates;

157
contrib/src/msgpack.rs Normal file
View File

@ -0,0 +1,157 @@
extern crate rmp_serde;
use std::ops::{Deref, DerefMut};
use std::io::{Cursor, Read};
use rocket::outcome::{Outcome, IntoOutcome};
use rocket::request::Request;
use rocket::data::{self, Data, FromData};
use rocket::response::{self, Responder, Response};
use rocket::http::{ContentType, Status};
use serde::Serialize;
use serde::de::DeserializeOwned;
pub use self::rmp_serde::decode::Error as MsgPackError;
/// The `MsgPack` type: implements `FromData` and `Responder`, allowing you to
/// easily consume and respond with MessagePack data.
///
/// ## Receiving MessagePack
///
/// If you're receiving MessagePack data, simply add a `data` parameter to your
/// route arguments and ensure the type of the parameter is a `MsgPack<T>`,
/// where `T` is some type you'd like to parse from MessagePack. `T` must
/// implement `Deserialize` or `DeserializeOwned` from
/// [Serde](https://github.com/serde-rs/serde). The data is parsed from the HTTP
/// request body.
///
/// ```rust,ignore
/// #[post("/users/", format = "application/msgpack", data = "<user>")]
/// fn new_user(user: MsgPack<User>) {
/// ...
/// }
/// ```
///
/// You don't _need_ to use `format = "application/msgpack"`, but it _may_ be
/// what you want. Using `format = application/msgpack` means that any request
/// that doesn't specify "application/msgpack" as its first `Content-Type:`
/// header parameter will not be routed to this handler. By default, Rocket will
/// accept a Content Type of any of the following for MessagePack data:
/// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, or
/// `bin/x-msgpack`.
///
/// ## Sending MessagePack
///
/// If you're responding with MessagePack data, return a `MsgPack<T>` type,
/// where `T` implements `Serialize` from
/// [Serde](https://github.com/serde-rs/serde). The content type of the response
/// is set to `application/msgpack` automatically.
///
/// ```rust,ignore
/// #[get("/users/<id>")]
/// fn user(id: usize) -> MsgPack<User> {
/// let user_from_id = User::from(id);
/// ...
/// MsgPack(user_from_id)
/// }
/// ```
///
/// ## Incoming Data Limits
///
/// The default size limit for incoming MessagePack data is 1MiB. Setting a
/// limit protects your application from denial of service (DOS) attacks and
/// from resource exhaustion through high memory consumption. The limit can be
/// increased by setting the `limits.msgpack` configuration parameter. For
/// instance, to increase the MessagePack limit to 5MiB for all environments,
/// you may add the following to your `Rocket.toml`:
///
/// ```toml
/// [global.limits]
/// msgpack = 5242880
/// ```
#[derive(Debug)]
pub struct MsgPack<T>(pub T);
impl<T> MsgPack<T> {
/// Consumes the `MsgPack` wrapper and returns the wrapped item.
///
/// # Example
///
/// ```rust
/// # use rocket_contrib::MsgPack;
/// let string = "Hello".to_string();
/// let my_msgpack = MsgPack(string);
/// assert_eq!(my_msgpack.into_inner(), "Hello".to_string());
/// ```
#[inline(always)]
pub fn into_inner(self) -> T {
self.0
}
}
/// Default limit for MessagePack is 1MB.
const LIMIT: u64 = 1 << 20;
/// Accepted content types are: `application/msgpack`, `application/x-msgpack`,
/// `bin/msgpack`, and `bin/x-msgpack`.
#[inline(always)]
fn is_msgpack_content_type(ct: &ContentType) -> bool {
(ct.top() == "application" || ct.top() == "bin")
&& (ct.sub() == "msgpack" || ct.sub() == "x-msgpack")
}
impl<T: DeserializeOwned> FromData for MsgPack<T> {
type Error = MsgPackError;
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
if !request.content_type().map_or(false, |ct| is_msgpack_content_type(&ct)) {
error_!("Content-Type is not MessagePack.");
return Outcome::Forward(data);
}
let mut buf = Vec::new();
let size_limit = request.limits().get("msgpack").unwrap_or(LIMIT);
if let Err(e) = data.open().take(size_limit).read_to_end(&mut buf) {
let e = MsgPackError::InvalidDataRead(e);
error_!("Couldn't read request data: {:?}", e);
return Outcome::Failure((Status::BadRequest, e));
};
rmp_serde::from_slice(&buf).map(|val| MsgPack(val))
.map_err(|e| { error_!("Couldn't parse MessagePack body: {:?}", e); e })
.into_outcome(Status::BadRequest)
}
}
/// Serializes the wrapped value into MessagePack. Returns a response with
/// Content-Type `MsgPack` and a fixed-size body with the serialization. If
/// serialization fails, an `Err` of `Status::InternalServerError` is returned.
impl<T: Serialize> Responder<'static> for MsgPack<T> {
fn respond_to(self, _: &Request) -> response::Result<'static> {
rmp_serde::to_vec(&self.0).map_err(|e| {
error_!("MsgPack failed to serialize: {:?}", e);
Status::InternalServerError
}).and_then(|buf| {
Response::build()
.sized_body(Cursor::new(buf))
.ok()
})
}
}
impl<T> Deref for MsgPack<T> {
type Target = T;
#[inline(always)]
fn deref<'a>(&'a self) -> &'a T {
&self.0
}
}
impl<T> DerefMut for MsgPack<T> {
#[inline(always)]
fn deref_mut<'a>(&'a mut self) -> &'a mut T {
&mut self.0
}
}

View File

@ -0,0 +1,131 @@
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use super::{Engines, TemplateInfo};
use super::glob;
use rocket::http::ContentType;
pub struct Context {
/// The root of the template directory.
pub root: PathBuf,
/// Mapping from template name to its information.
pub templates: HashMap<String, TemplateInfo>,
/// Mapping from template name to its information.
pub engines: Engines
}
impl Context {
pub fn initialize(root: PathBuf) -> Option<Context> {
let mut templates: HashMap<String, TemplateInfo> = HashMap::new();
for ext in Engines::ENABLED_EXTENSIONS {
let mut glob_path = root.join("**").join("*");
glob_path.set_extension(ext);
let glob_path = glob_path.to_str().expect("valid glob path string");
for path in glob(glob_path).unwrap().filter_map(Result::ok) {
let (name, data_type_str) = split_path(&root, &path);
if let Some(info) = templates.get(&*name) {
warn_!("Template name '{}' does not have a unique path.", name);
info_!("Existing path: {:?}", info.path);
info_!("Additional path: {:?}", path);
warn_!("Using existing path for template '{}'.", name);
continue;
}
let data_type = data_type_str.as_ref()
.and_then(|ext| ContentType::from_extension(ext))
.unwrap_or(ContentType::HTML);
templates.insert(name, TemplateInfo {
path: path.to_path_buf(),
extension: ext.to_string(),
data_type: data_type,
});
}
}
Engines::init(&templates).map(|engines| {
Context { root, templates, engines }
})
}
}
/// Removes the file path's extension or does nothing if there is none.
fn remove_extension<P: AsRef<Path>>(path: P) -> PathBuf {
let path = path.as_ref();
let stem = match path.file_stem() {
Some(stem) => stem,
None => return path.to_path_buf()
};
match path.parent() {
Some(parent) => parent.join(stem),
None => PathBuf::from(stem)
}
}
/// Splits a path into a name that may be used to identify the template, and the
/// template's data type, if any.
fn split_path(root: &Path, path: &Path) -> (String, Option<String>) {
let rel_path = path.strip_prefix(root).unwrap().to_path_buf();
let path_no_ext = remove_extension(&rel_path);
let data_type = path_no_ext.extension();
let mut name = remove_extension(&path_no_ext).to_string_lossy().into_owned();
// Ensure template name consistency on Windows systems
if cfg!(windows) {
name = name.replace("\\", "/");
}
(name, data_type.map(|d| d.to_string_lossy().into_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn template_path_index_html() {
for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] {
for filename in &["index.html.hbs", "index.html.tera"] {
let path = Path::new(root).join(filename);
let (name, data_type) = split_path(Path::new(root), &path);
assert_eq!(name, "index");
assert_eq!(data_type, Some("html".into()));
}
}
}
#[test]
fn template_path_subdir_index_html() {
for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] {
for sub in &["a/", "a/b/", "a/b/c/", "a/b/c/d/"] {
for filename in &["index.html.hbs", "index.html.tera"] {
let path = Path::new(root).join(sub).join(filename);
let (name, data_type) = split_path(Path::new(root), &path);
let expected_name = format!("{}index", sub);
assert_eq!(name, expected_name.as_str());
assert_eq!(data_type, Some("html".into()));
}
}
}
}
#[test]
fn template_path_doc_examples() {
fn name_for(path: &str) -> String {
split_path(Path::new("templates/"), &Path::new("templates/").join(path)).0
}
assert_eq!(name_for("index.html.hbs"), "index");
assert_eq!(name_for("index.tera"), "index");
assert_eq!(name_for("index.hbs"), "index");
assert_eq!(name_for("dir/index.hbs"), "dir/index");
assert_eq!(name_for("dir/index.html.tera"), "dir/index");
assert_eq!(name_for("index.template.html.hbs"), "index.template");
assert_eq!(name_for("subdir/index.template.html.hbs"), "subdir/index.template");
}
}

View File

@ -0,0 +1,72 @@
use std::collections::HashMap;
use super::serde::Serialize;
use super::TemplateInfo;
#[cfg(feature = "tera_templates")] use super::tera_templates::Tera;
#[cfg(feature = "handlebars_templates")] use super::handlebars_templates::Handlebars;
pub trait Engine: Send + Sync + 'static {
const EXT: &'static str;
fn init(templates: &[(&str, &TemplateInfo)]) -> Option<Self> where Self: Sized;
fn render<C: Serialize>(&self, name: &str, context: C) -> Option<String>;
}
pub struct Engines {
#[cfg(feature = "tera_templates")]
tera: Tera,
#[cfg(feature = "handlebars_templates")]
handlebars: Handlebars,
}
impl Engines {
pub const ENABLED_EXTENSIONS: &'static [&'static str] = &[
#[cfg(feature = "tera_templates")] Tera::EXT,
#[cfg(feature = "handlebars_templates")] Handlebars::EXT,
];
pub fn init(templates: &HashMap<String, TemplateInfo>) -> Option<Engines> {
fn inner<E: Engine>(templates: &HashMap<String, TemplateInfo>) -> Option<E> {
let named_templates = templates.iter()
.filter(|&(_, i)| i.extension == E::EXT)
.map(|(k, i)| (k.as_str(), i))
.collect::<Vec<_>>();
E::init(&*named_templates)
}
Some(Engines {
#[cfg(feature = "tera_templates")]
tera: match inner::<Tera>(templates) {
Some(tera) => tera,
None => return None
},
#[cfg(feature = "handlebars_templates")]
handlebars: match inner::<Handlebars>(templates) {
Some(hb) => hb,
None => return None
},
})
}
pub fn render<C>(&self, name: &str, info: &TemplateInfo, c: C) -> Option<String>
where C: Serialize
{
#[cfg(feature = "tera_templates")]
{
if info.extension == Tera::EXT {
return Engine::render(&self.tera, name, c);
}
}
#[cfg(feature = "handlebars_templates")]
{
if info.extension == Handlebars::EXT {
return Engine::render(&self.handlebars, name, c);
}
}
None
}
}

View File

@ -1,57 +1,40 @@
extern crate handlebars;
use super::serde::Serialize;
use super::TemplateInfo;
use super::{Engine, TemplateInfo};
use self::handlebars::Handlebars;
pub use self::handlebars::Handlebars;
static mut HANDLEBARS: Option<Handlebars> = None;
pub const EXT: &'static str = "hbs";
// This function must be called a SINGLE TIME from A SINGLE THREAD for safety to
// hold here and in `render`.
pub unsafe fn register(templates: &[(&str, &TemplateInfo)]) -> bool {
if HANDLEBARS.is_some() {
error_!("Internal error: reregistering handlebars!");
return false;
}
impl Engine for Handlebars {
const EXT: &'static str = "hbs";
fn init(templates: &[(&str, &TemplateInfo)]) -> Option<Handlebars> {
let mut hb = Handlebars::new();
let mut success = true;
for &(name, info) in templates {
let path = &info.full_path;
let path = &info.path;
if let Err(e) = hb.register_template_file(name, path) {
error_!("Handlebars template '{}' failed registry: {:?}", name, e);
success = false;
}
}
HANDLEBARS = Some(hb);
success
}
pub fn render<T>(name: &str, _info: &TemplateInfo, context: &T) -> Option<String>
where T: Serialize
{
let hb = match unsafe { HANDLEBARS.as_ref() } {
Some(hb) => hb,
None => {
error_!("Internal error: `render` called before handlebars init.");
error!("Error in Handlebars template '{}'.", name);
info_!("{}", e);
info_!("Template path: '{}'.", path.to_string_lossy());
return None;
}
};
}
if hb.get_template(name).is_none() {
Some(hb)
}
fn render<C: Serialize>(&self, name: &str, context: C) -> Option<String> {
if self.get_template(name).is_none() {
error_!("Handlebars template '{}' does not exist.", name);
return None;
}
match hb.render(name, context) {
match Handlebars::render(self, name, &context) {
Ok(string) => Some(string),
Err(e) => {
error_!("Error rendering Handlebars template '{}': {}", name, e);
None
}
}
}
}

View File

@ -1,53 +0,0 @@
/// Returns a hashset with the extensions of all of the enabled template
/// engines from the set of template engined passed in.
macro_rules! engine_set {
($($feature:expr => $engine:ident),+,) => ({
type RegisterFn = for<'a, 'b> unsafe fn(&'a [(&'b str, &TemplateInfo)]) -> bool;
let mut set = Vec::new();
$(
#[cfg(feature = $feature)]
fn $engine(set: &mut Vec<(&'static str, RegisterFn)>) {
set.push(($engine::EXT, $engine::register));
}
#[cfg(not(feature = $feature))]
fn $engine(_: &mut Vec<(&'static str, RegisterFn)>) { }
$engine(&mut set);
)+
set
});
}
/// Renders the template named `name` with the given template info `info` and
/// context `ctxt` using one of the templates in the template set passed in. It
/// does this by checking if the template's extension matches the engine's
/// extension, and if so, calls the engine's `render` method. All of this only
/// happens for engine's that have been enabled as features by the user.
macro_rules! render_set {
($name:expr, $info:expr, $ctxt:expr, $($feature:expr => $engine:ident),+,) => ({
$(
#[cfg(feature = $feature)]
fn $engine<T: Serialize>(name: &str, info: &TemplateInfo, c: &T)
-> Option<Template> {
if info.extension == $engine::EXT {
let rendered = $engine::render(name, info, c);
return Some(Template(rendered, info.data_type.clone()));
}
None
}
#[cfg(not(feature = $feature))]
fn $engine<T: Serialize>(_: &str, _: &TemplateInfo, _: &T)
-> Option<Template> { None }
if let Some(template) = $engine($name, &$info, $ctxt) {
return template
}
)+
});
}

View File

@ -2,34 +2,38 @@ extern crate serde;
extern crate serde_json;
extern crate glob;
#[cfg(feature = "tera_templates")]
mod tera_templates;
#[cfg(feature = "handlebars_templates")]
mod handlebars_templates;
#[macro_use] mod macros;
#[cfg(feature = "tera_templates")] mod tera_templates;
#[cfg(feature = "handlebars_templates")] mod handlebars_templates;
mod engine;
mod context;
use self::engine::{Engine, Engines};
use self::context::Context;
use self::serde::Serialize;
use self::serde_json::{Value, to_value};
use self::glob::glob;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::fmt;
use rocket::config::{self, ConfigError};
use rocket::State;
use rocket::request::Request;
use rocket::fairing::{Fairing, AdHoc};
use rocket::response::{self, Content, Responder};
use rocket::http::{ContentType, Status};
const DEFAULT_TEMPLATE_DIR: &'static str = "templates";
/// The Template type implements generic support for template rendering in
/// Rocket.
///
/// Templating in Rocket words by first discovering all of the templates inside
/// Templating in Rocket works by first discovering all of the templates inside
/// the template directory. The template directory is configurable via the
/// `template_dir` configuration parameter and defaults to `templates/`. The
/// path set in `template_dir` should be relative to the Rocket configuration
/// file. See the [configuration chapter](https://rocket.rs/guide/overview/#configuration)
/// of the guide for more information on configuration.
/// file. See the [configuration
/// chapter](https://rocket.rs/guide/overview/#configuration) of the guide for
/// more information on configuration.
///
/// Templates are discovered according to their extension. At present, this
/// library supports the following templates and extensions:
@ -55,6 +59,13 @@ use rocket::http::{ContentType, Status};
/// type, and one for the template extension. This means that template
/// extensions should look like: `.html.hbs`, `.html.tera`, `.xml.hbs`, etc.
///
/// Template discovery is actualized by the template fairing, which itself is
/// created via the
/// [`Template::fairing()`](/rocket_contrib/struct.Template.html#method.fairing)
/// method. In order for _any_ templates to be rendered, the template fairing
/// must be [attached](/rocket/struct.Rocket.html#method.attach) to the running
/// Rocket instance.
///
/// Templates are rendered with the `render` method. The method takes in the
/// name of a template and a context to render the template with. The context
/// can be any type that implements `Serialize` from
@ -74,7 +85,25 @@ use rocket::http::{ContentType, Status};
/// features = ["handlebars_templates", "tera_templates"]
/// ```
///
/// The Template type implements Rocket's `Responder` trait, so it can be
/// Then, ensure that the template [fairing](/rocket/fairing/) is attached to
/// your Rocket application:
///
/// ```rust
/// extern crate rocket;
/// extern crate rocket_contrib;
///
/// use rocket_contrib::Template;
///
/// fn main() {
/// rocket::ignite()
/// // ...
/// .attach(Template::fairing())
/// // ...
/// # ;
/// }
/// ```
///
/// The `Template` type implements Rocket's `Responder` trait, so it can be
/// returned from a request handler directly:
///
/// ```rust,ignore
@ -84,49 +113,68 @@ use rocket::http::{ContentType, Status};
/// Template::render("index", &context)
/// }
/// ```
// Fields are: (optionally rendered template, template extension)
#[derive(Debug)]
pub struct Template(Option<String>, Option<String>);
pub struct Template {
name: Cow<'static, str>,
value: Option<Value>
}
#[derive(Debug)]
pub struct TemplateInfo {
/// The complete path, including `template_dir`, to this template.
full_path: PathBuf,
/// The complete path, without `template_dir`, to this template.
path: PathBuf,
/// The extension for the engine of this template.
extension: String,
/// The extension before the engine extension in the template, if any.
data_type: Option<String>
}
const DEFAULT_TEMPLATE_DIR: &'static str = "templates";
lazy_static! {
static ref TEMPLATES: HashMap<String, TemplateInfo> = discover_templates();
static ref TEMPLATE_DIR: PathBuf = {
let default_dir_path = config::active().ok_or(ConfigError::NotFound)
.map(|config| config.root().join(DEFAULT_TEMPLATE_DIR))
.map_err(|_| {
warn_!("No configuration is active!");
warn_!("Using default template directory: {:?}", DEFAULT_TEMPLATE_DIR);
})
.unwrap_or(PathBuf::from(DEFAULT_TEMPLATE_DIR));
config::active().ok_or(ConfigError::NotFound)
.and_then(|config| config.get_str("template_dir"))
.map(|user_dir| PathBuf::from(user_dir))
.map_err(|e| {
if !e.is_not_found() {
e.pretty_print();
warn_!("Using default directory '{:?}'", default_dir_path);
}
})
.unwrap_or(default_dir_path)
};
data_type: ContentType
}
impl Template {
/// Returns a fairing that intializes and maintains templating state.
///
/// This fairing _must_ be attached to any `Rocket` instance that wishes to
/// render templates. Failure to attach this fairing will result in a
/// "Uninitialized template context: missing fairing." error message when a
/// template is attempted to be rendered.
///
/// # Example
///
/// To attach this fairing, simple call `attach` on the application's
/// `Rocket` instance with `Template::fairing()`:
///
/// ```rust
/// extern crate rocket;
/// extern crate rocket_contrib;
///
/// use rocket_contrib::Template;
///
/// fn main() {
/// rocket::ignite()
/// // ...
/// .attach(Template::fairing())
/// // ...
/// # ;
/// }
/// ```
pub fn fairing() -> impl Fairing {
AdHoc::on_attach(|rocket| {
let mut template_root = rocket.config().root().join(DEFAULT_TEMPLATE_DIR);
match rocket.config().get_str("template_dir") {
Ok(dir) => template_root = rocket.config().root().join(dir),
Err(ref e) if !e.is_not_found() => {
e.pretty_print();
warn_!("Using default directory '{:?}'", template_root);
}
Err(_) => { }
};
match Context::initialize(template_root) {
Some(ctxt) => Ok(rocket.manage(ctxt)),
None => Err(rocket)
}
})
}
/// Render the template named `name` with the context `context`. The
/// `context` can be of any type that implements `Serialize`. This is
/// typically a `HashMap` or a custom `struct`.
@ -141,30 +189,73 @@ impl Template {
/// let mut context = HashMap::new();
///
/// # context.insert("test", "test");
/// let template = Template::render("index", &context);
/// # assert_eq!(template.to_string(), "");
pub fn render<S, T>(name: S, context: &T) -> Template
where S: AsRef<str>, T: Serialize
/// # #[allow(unused_variables)]
/// let template = Template::render("index", context);
#[inline]
pub fn render<S, C>(name: S, context: C) -> Template
where S: Into<Cow<'static, str>>, C: Serialize
{
let name = name.as_ref();
let template = TEMPLATES.get(name);
if template.is_none() {
let names: Vec<_> = TEMPLATES.keys().map(|s| s.as_str()).collect();
error_!("Template '{}' does not exist.", name);
info_!("Known templates: {}", names.join(","));
info_!("Searched in '{:?}'.", *TEMPLATE_DIR);
return Template(None, None);
Template { name: name.into(), value: to_value(context).ok() }
}
// Keep this set in-sync with the `engine_set` invocation. The macro
// `return`s a `Template` if the extenion in `template` matches an
// engine in the set. Otherwise, control will fall through.
render_set!(name, template.unwrap(), context,
"tera_templates" => tera_templates,
"handlebars_templates" => handlebars_templates,
);
/// Render the template named `name` located at the path `root` with the
/// context `context` into a `String`. This method is _very slow_ and should
/// **not** be used in any running Rocket application. This method should
/// only be used during testing to validate `Template` responses. For other
/// uses, use [`render`](#method.render) instead.
///
/// The `context` can be of any type that implements `Serialize`. This is
/// typically a `HashMap` or a custom `struct`. The path `root` can be
/// relative, in which case it is relative to the current working directory,
/// or absolute.
///
/// Returns `Some` if the template could be rendered. Otherwise, returns
/// `None`. If rendering fails, error output is printed to the console.
///
/// # Example
///
/// ```rust
/// use std::collections::HashMap;
/// use rocket_contrib::Template;
///
/// // Create a `context`. Here, just an empty `HashMap`.
/// let mut context = HashMap::new();
///
/// # context.insert("test", "test");
/// # #[allow(unused_variables)]
/// let template = Template::show("templates/", "index", context);
#[inline]
pub fn show<P, S, C>(root: P, name: S, context: C) -> Option<String>
where P: AsRef<Path>, S: Into<Cow<'static, str>>, C: Serialize
{
let root = root.as_ref().to_path_buf();
Context::initialize(root).and_then(|ctxt| {
Template::render(name, context).finalize(&ctxt).ok().map(|v| v.0)
})
}
unreachable!("A template extension was discovered but not rendered.")
#[inline(always)]
fn finalize(self, ctxt: &Context) -> Result<(String, ContentType), Status> {
let name = &*self.name;
let info = ctxt.templates.get(name).ok_or_else(|| {
let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect();
error_!("Template '{}' does not exist.", name);
info_!("Known templates: {}", ts.join(","));
info_!("Searched in '{:?}'.", ctxt.root);
Status::InternalServerError
})?;
let value = self.value.ok_or_else(|| {
error_!("The provided template context failed to serialize.");
Status::InternalServerError
})?;
let string = ctxt.engines.render(name, &info, value).ok_or_else(|| {
error_!("Template '{}' failed to render.", name);
Status::InternalServerError
})?;
Ok((string, info.data_type.clone()))
}
}
@ -172,155 +263,15 @@ impl Template {
/// extension and a fixed-size body containing the rendered template. If
/// rendering fails, an `Err` of `Status::InternalServerError` is returned.
impl Responder<'static> for Template {
fn respond(self) -> response::Result<'static> {
let content_type = match self.1 {
Some(ref ext) => ContentType::from_extension(ext),
None => ContentType::HTML
};
fn respond_to(self, req: &Request) -> response::Result<'static> {
let ctxt = req.guard::<State<Context>>().succeeded().ok_or_else(|| {
error_!("Uninitialized template context: missing fairing.");
info_!("To use templates, you must attach `Template::fairing()`.");
info_!("See the `Template` documentation for more information.");
Status::InternalServerError
})?;
match self.0 {
Some(render) => Content(content_type, render).respond(),
None => Err(Status::InternalServerError)
}
}
}
/// Renders `self`. If the template cannot be rendered, nothing is written.
impl fmt::Display for Template {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.0 {
Some(ref render) => render.fmt(f),
None => Ok(())
}
}
}
/// Removes the file path's extension or does nothing if there is none.
fn remove_extension<P: AsRef<Path>>(path: P) -> PathBuf {
let path = path.as_ref();
let stem = match path.file_stem() {
Some(stem) => stem,
None => return path.to_path_buf()
};
match path.parent() {
Some(parent) => parent.join(stem),
None => PathBuf::from(stem)
}
}
/// Splits a path into a relative path from TEMPLATE_DIR, a name that
/// may be used to identify the template, and the template's data type.
fn split_path(path: &Path) -> (PathBuf, String, Option<String>) {
let rel_path = path.strip_prefix(&*TEMPLATE_DIR).unwrap().to_path_buf();
let path_no_ext = remove_extension(&rel_path);
let data_type = path_no_ext.extension();
let mut name = remove_extension(&path_no_ext).to_string_lossy().into_owned();
// Ensure template name consistency on Windows systems
if cfg!(windows) {
name = name.replace("\\", "/");
}
(rel_path, name, data_type.map(|d| d.to_string_lossy().into_owned()))
}
/// Returns a HashMap of `TemplateInfo`'s for all of the templates in
/// `TEMPLATE_DIR`. Templates are all files that match one of the extensions for
/// engine's in `engine_set`.
///
/// **WARNING:** This function should be called ONCE from a SINGLE THREAD.
fn discover_templates() -> HashMap<String, TemplateInfo> {
// Keep this set in-sync with the `render_set` invocation.
let engines = engine_set![
"tera_templates" => tera_templates,
"handlebars_templates" => handlebars_templates,
];
let mut templates: HashMap<String, TemplateInfo> = HashMap::new();
for &(ext, _) in &engines {
let mut glob_path: PathBuf = TEMPLATE_DIR.join("**").join("*");
glob_path.set_extension(ext);
for path in glob(glob_path.to_str().unwrap()).unwrap().filter_map(Result::ok) {
let (rel_path, name, data_type) = split_path(&path);
if let Some(info) = templates.get(&*name) {
warn_!("Template name '{}' does not have a unique path.", name);
info_!("Existing path: {:?}", info.full_path);
info_!("Additional path: {:?}", path);
warn_!("Using existing path for template '{}'.", name);
continue;
}
templates.insert(name, TemplateInfo {
full_path: path.to_path_buf(),
path: rel_path,
extension: ext.to_string(),
data_type: data_type,
});
}
}
for &(ext, register_fn) in &engines {
let named_templates = templates.iter()
.filter(|&(_, i)| i.extension == ext)
.map(|(k, i)| (k.as_str(), i))
.collect::<Vec<_>>();
unsafe { register_fn(&*named_templates); }
};
templates
}
#[cfg(test)]
mod tests {
use super::*;
/// Combines a `relative_path` and the `TEMPLATE_DIR` path into a full path.
fn template_path(relative_path: &str) -> PathBuf {
let mut path = PathBuf::from(&*TEMPLATE_DIR);
path.push(relative_path);
path
}
/// Returns the template system name, given a relative path to a file.
fn relative_path_to_name(relative_path: &str) -> String {
let path = template_path(relative_path);
let (_, name, _) = split_path(&path);
name
}
#[test]
fn template_path_index_html() {
let path = template_path("index.html.hbs");
let (rel_path, name, data_type) = split_path(&path);
assert_eq!(rel_path.to_string_lossy(), "index.html.hbs");
assert_eq!(name, "index");
assert_eq!(data_type, Some("html".to_owned()));
}
#[test]
fn template_path_subdir_index_html() {
let path = template_path("subdir/index.html.hbs");
let (rel_path, name, data_type) = split_path(&path);
assert_eq!(rel_path.to_string_lossy(), "subdir/index.html.hbs");
assert_eq!(name, "subdir/index");
assert_eq!(data_type, Some("html".to_owned()));
}
#[test]
fn template_path_doc_examples() {
assert_eq!(relative_path_to_name("index.html.hbs"), "index");
assert_eq!(relative_path_to_name("index.tera"), "index");
assert_eq!(relative_path_to_name("index.hbs"), "index");
assert_eq!(relative_path_to_name("dir/index.hbs"), "dir/index");
assert_eq!(relative_path_to_name("dir/index.html.tera"), "dir/index");
assert_eq!(relative_path_to_name("index.template.html.hbs"),
"index.template");
assert_eq!(relative_path_to_name("subdir/index.template.html.hbs"),
"subdir/index.template");
let (render, content_type) = self.finalize(&ctxt)?;
Content(content_type, render).respond_to(req)
}
}

View File

@ -1,63 +1,53 @@
extern crate tera;
use super::serde::Serialize;
use super::TemplateInfo;
use super::{Engine, TemplateInfo};
use self::tera::Tera;
pub use self::tera::Tera;
static mut TERA: Option<Tera> = None;
pub const EXT: &'static str = "tera";
// This function must be called a SINGLE TIME from A SINGLE THREAD for safety to
// hold here and in `render`.
pub unsafe fn register(templates: &[(&str, &TemplateInfo)]) -> bool {
if TERA.is_some() {
error_!("Internal error: reregistering Tera!");
return false;
}
impl Engine for Tera {
const EXT: &'static str = "tera";
fn init(templates: &[(&str, &TemplateInfo)]) -> Option<Tera> {
// Create the Tera instance.
let mut tera = Tera::default();
let ext = [".html.tera", ".htm.tera", ".xml.tera", ".html", ".htm", ".xml"];
tera.autoescape_on(ext.to_vec());
// Collect into a tuple of (name, path) for Tera.
let tera_templates = templates.iter()
.map(|&(name, info)| (&info.full_path, Some(name)))
.map(|&(name, info)| (&info.path, Some(name)))
.collect::<Vec<_>>();
// Finally try to tell Tera about all of the templates.
let mut success = true;
if let Err(e) = tera.add_template_files(tera_templates) {
error_!("Failed to initialize Tera templates: {:?}", e);
success = false;
error!("Failed to initialize Tera templating.");
for error in e.iter() {
info_!("{}", error);
}
TERA = Some(tera);
success
}
pub fn render<T>(name: &str, _: &TemplateInfo, context: &T) -> Option<String>
where T: Serialize
{
let tera = match unsafe { TERA.as_ref() } {
Some(tera) => tera,
None => {
error_!("Internal error: `render` called before Tera init.");
return None;
None
} else {
Some(tera)
}
}
};
if tera.get_template(name).is_err() {
fn render<C: Serialize>(&self, name: &str, context: C) -> Option<String> {
if self.get_template(name).is_err() {
error_!("Tera template '{}' does not exist.", name);
return None;
};
match tera.value_render(name, context) {
match Tera::render(self, name, &context) {
Ok(string) => Some(string),
Err(e) => {
error_!("Error rendering Tera template '{}': {}", name, e);
error_!("Error rendering Tera template '{}'.", name);
for error in e.iter() {
error_!("{}", error);
}
None
}
}
}
}

View File

@ -5,6 +5,7 @@ use std::str::FromStr;
use std::ops::Deref;
use rocket::request::{FromParam, FromFormValue};
use rocket::http::RawStr;
pub use self::uuid_ext::ParseError as UuidParseError;
@ -83,17 +84,18 @@ impl<'a> FromParam<'a> for UUID {
/// A value is successfully parsed if `param` is a properly formatted UUID.
/// Otherwise, a `UuidParseError` is returned.
#[inline(always)]
fn from_param(param: &'a str) -> Result<UUID, Self::Error> {
fn from_param(param: &'a RawStr) -> Result<UUID, Self::Error> {
param.parse()
}
}
impl<'v> FromFormValue<'v> for UUID {
type Error = &'v str;
type Error = &'v RawStr;
/// A value is successfully parsed if `form_value` is a properly formatted
/// UUID. Otherwise, the raw form value is returned.
fn from_form_value(form_value: &'v str) -> Result<UUID, &'v str> {
#[inline(always)]
fn from_form_value(form_value: &'v RawStr) -> Result<UUID, &'v RawStr> {
form_value.parse().map_err(|_| form_value)
}
}
@ -139,14 +141,14 @@ mod test {
#[test]
fn test_from_param() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
let uuid_wrapper = UUID::from_param(uuid_str).unwrap();
let uuid_wrapper = UUID::from_param(uuid_str.into()).unwrap();
assert_eq!(uuid_str, uuid_wrapper.to_string())
}
#[test]
fn test_into_inner() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
let uuid_wrapper = UUID::from_param(uuid_str).unwrap();
let uuid_wrapper = UUID::from_param(uuid_str.into()).unwrap();
let real_uuid: uuid_ext::Uuid = uuid_str.parse().unwrap();
let inner_uuid: uuid_ext::Uuid = uuid_wrapper.into_inner();
assert_eq!(real_uuid, inner_uuid)
@ -155,7 +157,7 @@ mod test {
#[test]
fn test_partial_eq() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
let uuid_wrapper = UUID::from_param(uuid_str).unwrap();
let uuid_wrapper = UUID::from_param(uuid_str.into()).unwrap();
let real_uuid: uuid_ext::Uuid = uuid_str.parse().unwrap();
assert_eq!(uuid_wrapper, real_uuid)
}
@ -163,7 +165,7 @@ mod test {
#[test]
fn test_from_param_invalid() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2p";
let uuid_result = UUID::from_param(uuid_str);
let uuid_result = UUID::from_param(uuid_str.into());
assert_eq!(uuid_result, Err(UuidParseError::InvalidLength(37)));
}
}

View File

@ -1,19 +1,13 @@
extern crate rocket;
extern crate rocket_contrib;
use std::env;
use rocket::config::Config;
use rocket::config::Environment::*;
use std::path::PathBuf;
fn init() {
fn template_root() -> PathBuf {
let cwd = env::current_dir().expect("current working directory");
let tests_dir = cwd.join("tests");
let config = Config::build(Development).root(tests_dir).unwrap();
rocket::custom(config, true);
cwd.join("tests").join("templates")
}
// FIXME: Do something about overlapping configs.
#[cfg(feature = "tera_templates")]
mod tera_tests {
use super::*;
@ -27,19 +21,17 @@ mod tera_tests {
#[test]
fn test_tera_templates() {
init();
let mut map = HashMap::new();
map.insert("title", "_test_");
map.insert("content", "<script />");
// Test with a txt file, which shouldn't escape.
let template = Template::render("tera/txt_test", &map);
assert_eq!(&template.to_string(), UNESCAPED_EXPECTED);
let template = Template::show(template_root(), "tera/txt_test", &map);
assert_eq!(template, Some(UNESCAPED_EXPECTED.into()));
// Now with an HTML file, which should.
let template = Template::render("tera/html_test", &map);
assert_eq!(&template.to_string(), ESCAPED_EXPECTED);
let template = Template::show(template_root(), "tera/html_test", &map);
assert_eq!(template, Some(ESCAPED_EXPECTED.into()));
}
}
@ -54,15 +46,13 @@ mod handlebars_tests {
#[test]
fn test_handlebars_templates() {
init();
let mut map = HashMap::new();
map.insert("title", "_test_");
map.insert("content", "<script /> hi");
// Test with a txt file, which shouldn't escape.
let template = Template::render("hbs/test", &map);
assert_eq!(&template.to_string(), EXPECTED);
let template = Template::show(template_root(), "hbs/test", &map);
assert_eq!(template, Some(EXPECTED.into()));
}
}

View File

@ -1,11 +1,8 @@
[package]
name = "config"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -1,13 +1,18 @@
# Except for the session key, none of these are actually needed; Rocket has sane
# Except for the secret key, none of these are actually needed; Rocket has sane
# defaults. We show all of them here explicitly for demonstrative purposes.
[global.limits]
forms = 32768
json = 1048576 # this is an extra used by the json contrib module
msgpack = 1048576 # this is an extra used by the msgpack contrib module
[development]
address = "localhost"
port = 8000
workers = 1
log = "normal"
hi = "Hello!"
is_extra = true
hi = "Hello!" # this is an unused extra; maybe application specific?
is_extra = true # this is an unused extra; maybe application specific?
[staging]
address = "0.0.0.0"
@ -15,7 +20,7 @@ port = 80
log = "normal"
workers = 8
# don't use this key! generate your own and keep it private!
session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5"
secret_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="
[production]
address = "0.0.0.0"
@ -23,4 +28,4 @@ port = 80
workers = 12
log = "critical"
# don't use this key! generate your own and keep it private!
session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz"
secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk="

View File

@ -1,10 +0,0 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
#[get("/")]
#[allow(unmounted_route)]
pub fn hello() -> &'static str {
"Hello, world!"
}

View File

@ -2,9 +2,8 @@
#![plugin(rocket_codegen)]
extern crate rocket;
extern crate config;
// This example's illustration is the Rocket.toml file.
fn main() {
rocket::ignite().mount("/hello", routes![config::hello]).launch()
rocket::ignite().launch();
}

View File

@ -1,21 +1,21 @@
extern crate rocket;
extern crate config as lib;
use rocket::{self, State};
use rocket::fairing::AdHoc;
use rocket::config::{self, Config, Environment, LoggingLevel};
use rocket::http::Status;
use rocket::local::Client;
use rocket::config::{self, Environment};
use rocket::http::Method;
use rocket::LoggingLevel;
use rocket::testing::MockRequest;
struct LocalConfig(Config);
pub fn test_config(environment: Environment) {
// Manually set the config environment variable. Rocket will initialize the
// environment in `ignite()`.
::std::env::set_var("ROCKET_ENV", environment.to_string());
rocket::ignite().mount("/hello", routes![lib::hello]);
#[get("/check_config")]
fn check_config(config: State<LocalConfig>) -> Option<()> {
let environment = match ::std::env::var("ROCKET_ENV") {
Ok(name) => name,
Err(_) => return None
};
// Get the active environment and ensure that it matches our expectations.
let config = config::active().unwrap();
match environment {
Environment::Development => {
let config = &config.0;
match &*environment {
"development" => {
assert_eq!(config.address, "localhost".to_string());
assert_eq!(config.port, 8000);
assert_eq!(config.workers, 1);
@ -25,7 +25,7 @@ pub fn test_config(environment: Environment) {
assert_eq!(config.get_str("hi"), Ok("Hello!"));
assert_eq!(config.get_bool("is_extra"), Ok(true));
}
Environment::Staging => {
"staging" => {
assert_eq!(config.address, "0.0.0.0".to_string());
assert_eq!(config.port, 80);
assert_eq!(config.workers, 8);
@ -33,7 +33,7 @@ pub fn test_config(environment: Environment) {
assert_eq!(config.environment, config::Environment::Staging);
assert_eq!(config.extras().count(), 0);
}
Environment::Production => {
"production" => {
assert_eq!(config.address, "0.0.0.0".to_string());
assert_eq!(config.port, 80);
assert_eq!(config.workers, 12);
@ -41,17 +41,28 @@ pub fn test_config(environment: Environment) {
assert_eq!(config.environment, config::Environment::Production);
assert_eq!(config.extras().count(), 0);
}
_ => {
panic!("Unknown environment in envvar: {}", environment);
}
}
// Rocket `take`s the key, so this should always be `None`.
assert_eq!(config.take_session_key(), None);
Some(())
}
pub fn test_hello() {
let rocket = rocket::ignite().mount("/hello", routes![lib::hello]);
let mut request = MockRequest::new(Method::Get, "/hello");
let mut response = request.dispatch_with(&rocket);
pub fn test_config(environment: Environment) {
// Manually set the config environment variable. Rocket will initialize the
// environment in `ignite()`. We'll read this back in the handler to config.
::std::env::set_var("ROCKET_ENV", environment.to_string());
assert_eq!(response.body().and_then(|b| b.into_string()),
Some("Hello, world!".to_string()));
let rocket = rocket::ignite()
.attach(AdHoc::on_attach(|rocket| {
println!("Attaching local config.");
let config = rocket.config().clone();
Ok(rocket.manage(LocalConfig(config)))
}))
.mount("/", routes![check_config]);
let client = Client::new(rocket).unwrap();
let response = client.get("/check_config").dispatch();
assert_eq!(response.status(), Status::Ok);
}

View File

@ -8,5 +8,4 @@ mod common;
#[test]
fn test_development_config() {
common::test_config(rocket::config::Environment::Development);
common::test_hello();
}

View File

@ -8,5 +8,4 @@ mod common;
#[test]
fn test_production_config() {
common::test_config(rocket::config::Environment::Production);
common::test_hello();
}

View File

@ -8,5 +8,4 @@ mod common;
#[test]
fn test_staging_config() {
common::test_config(rocket::config::Environment::Staging);
common::test_hello();
}

View File

@ -1,14 +1,11 @@
[package]
name = "content_types"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
serde = "0.9"
serde_json = "0.9"
serde_derive = "0.9"
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"

View File

@ -9,45 +9,49 @@ extern crate serde_derive;
#[cfg(test)] mod tests;
use rocket::Request;
use rocket::http::ContentType;
use rocket::response::content;
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
age: i8,
age: u8,
}
// This shows how to manually serialize some JSON, but in a real application,
// we'd use the JSON contrib type.
// In a `GET` request and all other non-payload supporting request types, the
// preferred media type in the Accept header is matched against the `format` in
// the route attribute.
#[get("/<name>/<age>", format = "application/json")]
fn hello(content_type: ContentType, name: String, age: i8) -> content::JSON<String> {
let person = Person {
name: name,
age: age,
};
fn get_hello(name: String, age: u8) -> content::Json<String> {
// In a real application, we'd use the JSON contrib type.
let person = Person { name: name, age: age, };
content::Json(serde_json::to_string(&person).unwrap())
}
println!("ContentType: {}", content_type);
content::JSON(serde_json::to_string(&person).unwrap())
// In a `POST` request and all other payload supporting request types, the
// content type is matched against the `format` in the route attribute.
#[post("/<age>", format = "text/plain", data = "<name>")]
fn post_hello(age: u8, name: String) -> content::Json<String> {
let person = Person { name: name, age: age, };
content::Json(serde_json::to_string(&person).unwrap())
}
#[error(404)]
fn not_found(request: &Request) -> content::HTML<String> {
let html = match request.content_type() {
Some(ref ct) if !ct.is_json() => {
format!("<p>This server only supports JSON requests, not '{}'.</p>", ct)
fn not_found(request: &Request) -> content::Html<String> {
let html = match request.format() {
Some(ref mt) if !mt.is_json() && !mt.is_plain() => {
format!("<p>'{}' requests are not supported.</p>", mt)
}
_ => format!("<p>Sorry, '{}' is an invalid path! Try \
/hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
request.uri())
};
content::HTML(html)
content::Html(html)
}
fn main() {
rocket::ignite()
.mount("/hello", routes![hello])
.mount("/hello", routes![get_hello, post_hello])
.catch(errors![not_found])
.launch();
}

View File

@ -1,40 +1,47 @@
use super::rocket;
use super::serde_json;
use super::Person;
use rocket::http::{ContentType, Method, Status};
use rocket::testing::MockRequest;
use rocket::http::{Accept, ContentType, Header, MediaType, Method, Status};
use rocket::local::Client;
fn test(uri: &str, content_type: ContentType, status: Status, body: String) {
fn test<H>(method: Method, uri: &str, header: H, status: Status, body: String)
where H: Into<Header<'static>>
{
let rocket = rocket::ignite()
.mount("/hello", routes![super::hello])
.mount("/hello", routes![super::get_hello, super::post_hello])
.catch(errors![super::not_found]);
let mut request = MockRequest::new(Method::Get, uri).header(content_type);
let mut response = request.dispatch_with(&rocket);
let client = Client::new(rocket).unwrap();
let mut response = client.req(method, uri).header(header).dispatch();
assert_eq!(response.status(), status);
assert_eq!(response.body().and_then(|b| b.into_string()), Some(body));
assert_eq!(response.body_string(), Some(body));
}
#[test]
fn test_hello() {
let person = Person {
name: "Michael".to_string(),
age: 80,
};
let person = Person { name: "Michael".to_string(), age: 80, };
let body = serde_json::to_string(&person).unwrap();
test("/hello/Michael/80", ContentType::JSON, Status::Ok, body);
test(Method::Get, "/hello/Michael/80", Accept::JSON, Status::Ok, body.clone());
test(Method::Get, "/hello/Michael/80", Accept::Any, Status::Ok, body.clone());
// No `Accept` header is an implicit */*.
test(Method::Get, "/hello/Michael/80", ContentType::XML, Status::Ok, body);
let person = Person { name: "".to_string(), age: 99, };
let body = serde_json::to_string(&person).unwrap();
test(Method::Post, "/hello/99", ContentType::Plain, Status::Ok, body);
}
#[test]
fn test_hello_invalid_content_type() {
let body = format!("<p>This server only supports JSON requests, not '{}'.</p>",
ContentType::HTML);
test("/hello/Michael/80", ContentType::HTML, Status::NotFound, body);
let b = format!("<p>'{}' requests are not supported.</p>", MediaType::HTML);
test(Method::Get, "/hello/Michael/80", Accept::HTML, Status::NotFound, b.clone());
test(Method::Post, "/hello/80", ContentType::HTML, Status::NotFound, b);
}
#[test]
fn test_404() {
let body = "<p>Sorry, '/unknown' is an invalid path! Try \
/hello/&lt;name&gt;/&lt;age&gt; instead.</p>";
test("/unknown", ContentType::JSON, Status::NotFound, body.to_string());
test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string());
}

View File

@ -1,6 +1,6 @@
[package]
name = "cookies"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]
@ -11,6 +11,3 @@ rocket_codegen = { path = "../../codegen" }
path = "../../contrib"
default-features = false
features = ["handlebars_templates"]
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -1,4 +1,4 @@
#![feature(plugin, custom_derive, custom_attribute)]
#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]
extern crate rocket_contrib;
@ -20,14 +20,14 @@ struct Message {
}
#[post("/submit", data = "<message>")]
fn submit(cookies: &Cookies, message: Form<Message>) -> Redirect {
fn submit(mut cookies: Cookies, message: Form<Message>) -> Redirect {
cookies.add(Cookie::new("message", message.into_inner().message));
Redirect::to("/")
}
#[get("/")]
fn index(cookies: &Cookies) -> Template {
let cookie = cookies.find("message");
fn index(cookies: Cookies) -> Template {
let cookie = cookies.get("message");
let mut context = HashMap::new();
if let Some(ref cookie) = cookie {
context.insert("message", cookie.value());
@ -36,6 +36,10 @@ fn index(cookies: &Cookies) -> Template {
Template::render("index", &context)
}
fn main() {
rocket::ignite().mount("/", routes![submit, index]).launch()
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![submit, index]).attach(Template::fairing())
}
fn main() {
rocket().launch();
}

View File

@ -1,19 +1,22 @@
use std::collections::HashMap;
use super::rocket;
use rocket::testing::MockRequest;
use rocket::local::Client;
use rocket::http::*;
use rocket_contrib::Template;
const TEMPLATE_ROOT: &'static str = "templates/";
#[test]
fn test_submit() {
let rocket = rocket::ignite().mount("/", routes![super::index, super::submit]);
let mut request = MockRequest::new(Method::Post, "/submit")
let client = Client::new(rocket()).unwrap();
let response = client.post("/submit")
.header(ContentType::Form)
.body("message=Hello from Rocket!");
let response = request.dispatch_with(&rocket);
let cookie_headers: Vec<_> = response.header_values("Set-Cookie").collect();
let location_headers: Vec<_> = response.header_values("Location").collect();
.body("message=Hello from Rocket!")
.dispatch();
let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect();
let location_headers: Vec<_> = response.headers().get("Location").collect();
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(cookie_headers, vec!["message=Hello%20from%20Rocket!".to_string()]);
@ -21,33 +24,26 @@ fn test_submit() {
}
fn test_body(optional_cookie: Option<Cookie<'static>>, expected_body: String) {
let rocket = rocket::ignite().mount("/", routes![super::index, super::submit]);
let mut request = MockRequest::new(Method::Get, "/");
// Attach a cookie if one is given.
if let Some(cookie) = optional_cookie {
request = request.cookie(cookie);
}
let client = Client::new(rocket()).unwrap();
let mut response = match optional_cookie {
Some(cookie) => client.get("/").cookie(cookie).dispatch(),
None => client.get("/").dispatch(),
};
let mut response = request.dispatch_with(&rocket);
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.body().and_then(|b| b.into_string()), Some(expected_body));
assert_eq!(response.body_string(), Some(expected_body));
}
#[test]
fn test_index() {
// Render the template with an empty context to test against.
// Render the template with an empty context.
let mut context: HashMap<&str, &str> = HashMap::new();
let template = Template::render("index", &context);
// Test the route without sending the "message" cookie.
test_body(None, template.to_string());
let template = Template::show(TEMPLATE_ROOT, "index", &context).unwrap();
test_body(None, template);
// Render the template with a context that contains the message.
context.insert("message", "Hello from Rocket!");
// Test the route with the "message" cookie.
let cookie = Cookie::new("message", "Hello from Rocket!");
let template = Template::render("index", &context);
test_body(Some(cookie), template.to_string());
let template = Template::show(TEMPLATE_ROOT, "index", &context).unwrap();
test_body(Some(Cookie::new("message", "Hello from Rocket!")), template);
}

View File

@ -1,11 +1,8 @@
[package]
name = "errors"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -8,20 +8,24 @@ extern crate rocket;
use rocket::response::content;
#[get("/hello/<name>/<age>")]
fn hello(name: &str, age: i8) -> String {
fn hello(name: String, age: i8) -> String {
format!("Hello, {} year old named {}!", age, name)
}
#[error(404)]
fn not_found(req: &rocket::Request) -> content::HTML<String> {
content::HTML(format!("<p>Sorry, but '{}' is not a valid path!</p>
fn not_found(req: &rocket::Request) -> content::Html<String> {
content::Html(format!("<p>Sorry, but '{}' is not a valid path!</p>
<p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
req.uri()))
}
fn main() {
rocket::ignite()
let e = rocket::ignite()
// .mount("/", routes![hello, hello]) // uncoment this to get an error
.mount("/", routes![hello])
.catch(errors![not_found])
.launch();
println!("Whoops! Rocket didn't launch!");
println!("This went wrong: {}", e);
}

View File

@ -1,16 +1,16 @@
use super::rocket;
use rocket::testing::MockRequest;
use rocket::http::{Method, Status};
use rocket::local::Client;
use rocket::http::Status;
fn test(uri: &str, status: Status, body: String) {
let rocket = rocket::ignite()
.mount("/", routes![super::hello])
.catch(errors![super::not_found]);
let mut req = MockRequest::new(Method::Get, uri);
let mut response = req.dispatch_with(&rocket);
let client = Client::new(rocket).unwrap();
let mut response = client.get(uri).dispatch();
assert_eq!(response.status(), status);
assert_eq!(response.body().and_then(|b| b.into_string()), Some(body));
assert_eq!(response.body_string(), Some(body));
}
#[test]

View File

@ -1,6 +1,6 @@
[package]
name = "extended_validation"
version = "0.0.1"
name = "fairings"
version = "0.0.0"
workspace = "../../"
[dependencies]

View File

@ -0,0 +1,2 @@
[global]
token = 123

View File

@ -0,0 +1,95 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
use std::io::Cursor;
use std::sync::atomic::{AtomicUsize, Ordering};
use rocket::{Request, State, Data, Response};
use rocket::fairing::{AdHoc, Fairing, Info, Kind};
use rocket::http::{Method, ContentType, Status};
struct Token(i64);
#[cfg(test)] mod tests;
#[derive(Default)]
struct Counter {
get: AtomicUsize,
post: AtomicUsize,
}
impl Fairing for Counter {
fn info(&self) -> Info {
Info {
name: "GET/POST Counter",
kind: Kind::Request | Kind::Response
}
}
fn on_request(&self, request: &mut Request, _: &Data) {
if request.method() == Method::Get {
self.get.fetch_add(1, Ordering::Relaxed);
} else if request.method() == Method::Post {
self.post.fetch_add(1, Ordering::Relaxed);
}
}
fn on_response(&self, request: &Request, response: &mut Response) {
if response.status() != Status::NotFound {
return
}
if request.method() == Method::Get && request.uri().path() == "/counts" {
let get_count = self.get.load(Ordering::Relaxed);
let post_count = self.post.load(Ordering::Relaxed);
let body = format!("Get: {}\nPost: {}", get_count, post_count);
response.set_status(Status::Ok);
response.set_header(ContentType::Plain);
response.set_sized_body(Cursor::new(body));
}
}
}
#[put("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[get("/token")]
fn token(token: State<Token>) -> String {
format!("{}", token.0)
}
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![hello, token])
.attach(Counter::default())
.attach(AdHoc::on_attach(|rocket| {
println!("Adding token managed state...");
let token_val = rocket.config().get_int("token").unwrap_or(-1);
Ok(rocket.manage(Token(token_val)))
}))
.attach(AdHoc::on_launch(|_| {
println!("Rocket is about to launch!");
}))
.attach(AdHoc::on_request(|req, _| {
println!(" => Incoming request: {}", req);
if req.uri().path() == "/" {
println!(" => Changing method to `PUT`.");
req.set_method(Method::Put);
}
}))
.attach(AdHoc::on_response(|req, res| {
if req.uri().path() == "/" {
println!(" => Rewriting response body.");
res.set_sized_body(Cursor::new("Hello, fairings!"));
}
}))
}
fn main() {
rocket().launch();
}

View File

@ -0,0 +1,38 @@
use super::rocket;
use rocket::local::Client;
#[test]
fn rewrite_get_put() {
let client = Client::new(rocket()).unwrap();
let mut response = client.get("/").dispatch();
assert_eq!(response.body_string(), Some("Hello, fairings!".into()));
}
#[test]
fn counts() {
let client = Client::new(rocket()).unwrap();
// Issue 1 GET request.
client.get("/").dispatch();
// Check the GET count, taking into account _this_ GET request.
let mut response = client.get("/counts").dispatch();
assert_eq!(response.body_string(), Some("Get: 2\nPost: 0".into()));
// Issue 1 more GET request and a POST.
client.get("/").dispatch();
client.post("/").dispatch();
// Check the counts.
let mut response = client.get("/counts").dispatch();
assert_eq!(response.body_string(), Some("Get: 4\nPost: 1".into()));
}
#[test]
fn token() {
let client = Client::new(rocket()).unwrap();
// Ensure the token is '123', which is what we have in `Rocket.toml`.
let mut res = client.get("/token").dispatch();
assert_eq!(res.body_string(), Some("123".into()));
}

View File

@ -1,6 +1,6 @@
[package]
name = "form_kitchen_sink"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]

View File

@ -3,9 +3,13 @@
extern crate rocket;
use std::io;
use rocket::request::{Form, FromFormValue};
use rocket::response::NamedFile;
use std::io;
use rocket::http::RawStr;
#[cfg(test)] mod tests;
// TODO: Make deriving `FromForm` for this enum possible.
#[derive(Debug)]
@ -14,10 +18,10 @@ enum FormOption {
}
impl<'v> FromFormValue<'v> for FormOption {
type Error = &'v str;
type Error = &'v RawStr;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
let variant = match v {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
let variant = match v.as_str() {
"a" => FormOption::A,
"b" => FormOption::B,
"c" => FormOption::C,
@ -29,17 +33,19 @@ impl<'v> FromFormValue<'v> for FormOption {
}
#[derive(Debug, FromForm)]
struct FormInput {
struct FormInput<'r> {
checkbox: bool,
number: usize,
#[form(field = "type")]
radio: FormOption,
password: String,
textarea: String,
password: &'r RawStr,
#[form(field = "textarea")]
text_area: String,
select: FormOption,
}
#[post("/", data = "<sink>")]
fn sink(sink: Result<Form<FormInput>, Option<String>>) -> String {
fn sink<'r>(sink: Result<Form<'r, FormInput<'r>>, Option<String>>) -> String {
match sink {
Ok(form) => format!("{:?}", form.get()),
Err(Some(f)) => format!("Invalid form input: {}", f),
@ -52,8 +58,10 @@ fn index() -> io::Result<NamedFile> {
NamedFile::open("static/index.html")
}
fn main() {
rocket::ignite()
.mount("/", routes![index, sink])
.launch();
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![index, sink])
}
fn main() {
rocket().launch();
}

View File

@ -0,0 +1,182 @@
use std::fmt;
use super::{rocket, FormInput, FormOption};
use rocket::local::Client;
use rocket::http::ContentType;
impl fmt::Display for FormOption {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
FormOption::A => write!(f, "a"),
FormOption::B => write!(f, "b"),
FormOption::C => write!(f, "c"),
}
}
}
fn assert_form_eq(client: &Client, form_str: &str, expected: String) {
let mut res = client.post("/")
.header(ContentType::Form)
.body(form_str)
.dispatch();
assert_eq!(res.body_string(), Some(expected));
}
fn assert_valid_form(client: &Client, input: &FormInput) {
let f = format!("checkbox={}&number={}&type={}&password={}&textarea={}&select={}",
input.checkbox, input.number, input.radio, input.password,
input.text_area, input.select);
assert_form_eq(client, &f, format!("{:?}", input));
}
fn assert_valid_raw_form(client: &Client, form_str: &str, input: &FormInput) {
assert_form_eq(client, form_str, format!("{:?}", input));
}
#[test]
fn test_good_forms() {
let client = Client::new(rocket()).unwrap();
let mut input = FormInput {
checkbox: true,
number: 310,
radio: FormOption::A,
password: "beep".into(),
text_area: "bop".to_string(),
select: FormOption::B
};
assert_valid_form(&client, &input);
input.checkbox = false;
assert_valid_form(&client, &input);
input.number = 0;
assert_valid_form(&client, &input);
input.number = 120;
assert_valid_form(&client, &input);
input.number = 133;
assert_valid_form(&client, &input);
input.radio = FormOption::B;
assert_valid_form(&client, &input);
input.radio = FormOption::C;
assert_valid_form(&client, &input);
input.password = "".into();
assert_valid_form(&client, &input);
input.password = "----90138490285u2o3hndslkv".into();
assert_valid_form(&client, &input);
input.password = "hi".into();
assert_valid_form(&client, &input);
input.text_area = "".to_string();
assert_valid_form(&client, &input);
input.text_area = "----90138490285u2o3hndslkv".to_string();
assert_valid_form(&client, &input);
input.text_area = "hey".to_string();
assert_valid_form(&client, &input);
input.select = FormOption::A;
assert_valid_form(&client, &input);
input.select = FormOption::C;
assert_valid_form(&client, &input);
// checkbox need not be present; defaults to false; accepts 'on' and 'off'
assert_valid_raw_form(&client,
"number=133&type=c&password=hi&textarea=hey&select=c",
&input);
assert_valid_raw_form(&client,
"checkbox=off&number=133&type=c&password=hi&textarea=hey&select=c",
&input);
input.checkbox = true;
assert_valid_raw_form(&client,
"checkbox=on&number=133&type=c&password=hi&textarea=hey&select=c",
&input);
}
fn assert_invalid_form(client: &Client, vals: &mut [&str; 6]) {
let s = format!("checkbox={}&number={}&type={}&password={}&textarea={}&select={}",
vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]);
assert_form_eq(client, &s, format!("Invalid form input: {}", s));
*vals = ["true", "1", "a", "hi", "hey", "b"];
}
fn assert_invalid_raw_form(client: &Client, form_str: &str) {
assert_form_eq(client, form_str, format!("Invalid form input: {}", form_str));
}
#[test]
fn check_semantically_invalid_forms() {
let client = Client::new(rocket()).unwrap();
let mut form_vals = ["true", "1", "a", "hi", "hey", "b"];
form_vals[0] = "not true";
assert_invalid_form(&client, &mut form_vals);
form_vals[0] = "bing";
assert_invalid_form(&client, &mut form_vals);
form_vals[0] = "true0";
assert_invalid_form(&client, &mut form_vals);
form_vals[0] = " false";
assert_invalid_form(&client, &mut form_vals);
form_vals[1] = "-1";
assert_invalid_form(&client, &mut form_vals);
form_vals[1] = "1e10";
assert_invalid_form(&client, &mut form_vals);
form_vals[1] = "-1-1";
assert_invalid_form(&client, &mut form_vals);
form_vals[1] = "NaN";
assert_invalid_form(&client, &mut form_vals);
form_vals[2] = "A";
assert_invalid_form(&client, &mut form_vals);
form_vals[2] = "B";
assert_invalid_form(&client, &mut form_vals);
form_vals[2] = "d";
assert_invalid_form(&client, &mut form_vals);
form_vals[2] = "100";
assert_invalid_form(&client, &mut form_vals);
form_vals[2] = "";
assert_invalid_form(&client, &mut form_vals);
// password and textarea are always valid, so we skip them
form_vals[5] = "A";
assert_invalid_form(&client, &mut form_vals);
form_vals[5] = "b ";
assert_invalid_form(&client, &mut form_vals);
form_vals[5] = "d";
assert_invalid_form(&client, &mut form_vals);
form_vals[5] = "-a";
assert_invalid_form(&client, &mut form_vals);
form_vals[5] = "";
assert_invalid_form(&client, &mut form_vals);
// now forms with missing fields
assert_invalid_raw_form(&client, "number=10&type=a&password=hi&textarea=hey");
assert_invalid_raw_form(&client, "number=10&radio=a&password=hi&textarea=hey&select=b");
assert_invalid_raw_form(&client, "number=10&password=hi&select=b");
assert_invalid_raw_form(&client, "number=10&select=b");
assert_invalid_raw_form(&client, "password=hi&select=b");
assert_invalid_raw_form(&client, "password=hi");
assert_invalid_raw_form(&client, "");
}
#[test]
fn check_structurally_invalid_forms() {
let client = Client::new(rocket()).unwrap();
assert_invalid_raw_form(&client, "==&&&&&&==");
assert_invalid_raw_form(&client, "a&=b");
assert_invalid_raw_form(&client, "=");
}
#[test]
fn check_bad_utf8() {
let client = Client::new(rocket()).unwrap();
unsafe {
let bad_str = ::std::str::from_utf8_unchecked(b"a=\xff");
assert_form_eq(&client, bad_str, "Form input was invalid UTF8.".into());
}
}

View File

@ -14,15 +14,15 @@
</label>
<br /><br />
<label for="radio">Radios:
<label for="type">Type:
<label for="a">A
<input type="radio" name="radio" id="a" value="a" />
<input type="radio" name="type" id="a" value="a" />
</label>
<label for="b">B
<input type="radio" name="radio" id="b" value="b" checked />
<input type="radio" name="type" id="b" value="b" checked />
</label>
<label for="c">C
<input type="radio" name="radio" id="c" value="c" />
<input type="radio" name="type" id="c" value="c" />
</label>
</label>
<br /><br />

View File

@ -1,11 +1,8 @@
[package]
name = "forms"
version = "0.0.1"
name = "form_validation"
version = "0.0.0"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -4,9 +4,11 @@
extern crate rocket;
mod files;
#[cfg(test)] mod tests;
use rocket::response::Redirect;
use rocket::request::{Form, FromFormValue};
use rocket::http::RawStr;
#[derive(Debug)]
struct StrongPassword<'r>(&'r str);
@ -16,7 +18,7 @@ struct AdultAge(isize);
#[derive(FromForm)]
struct UserLogin<'r> {
username: &'r str,
username: &'r RawStr,
password: Result<StrongPassword<'r>, &'static str>,
age: Result<AdultAge, &'static str>,
}
@ -24,11 +26,11 @@ struct UserLogin<'r> {
impl<'v> FromFormValue<'v> for StrongPassword<'v> {
type Error = &'static str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
if v.len() < 8 {
Err("Too short!")
Err("too short!")
} else {
Ok(StrongPassword(v))
Ok(StrongPassword(v.as_str()))
}
}
}
@ -36,15 +38,15 @@ impl<'v> FromFormValue<'v> for StrongPassword<'v> {
impl<'v> FromFormValue<'v> for AdultAge {
type Error = &'static str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> {
let age = match isize::from_form_value(v) {
Ok(v) => v,
Err(_) => return Err("Age value is not a number."),
Err(_) => return Err("value is not a number."),
};
match age > 20 {
true => Ok(AdultAge(age)),
false => Err("Must be at least 21."),
false => Err("must be at least 21."),
}
}
}
@ -73,12 +75,15 @@ fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result<Redirect, String> {
}
#[get("/user/<username>")]
fn user_page(username: &str) -> String {
fn user_page(username: &RawStr) -> String {
format!("This is {}'s page.", username)
}
fn main() {
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![files::index, files::files, user_page, login])
.launch();
}
fn main() {
rocket().launch();
}

View File

@ -0,0 +1,83 @@
use super::rocket;
use rocket::local::Client;
use rocket::http::{ContentType, Status};
fn test_login<T>(user: &str, pass: &str, age: &str, status: Status, body: T)
where T: Into<Option<&'static str>>
{
let client = Client::new(rocket()).unwrap();
let query = format!("username={}&password={}&age={}", user, pass, age);
let mut response = client.post("/login")
.header(ContentType::Form)
.body(&query)
.dispatch();
assert_eq!(response.status(), status);
if let Some(expected_str) = body.into() {
let body_str = response.body_string();
assert!(body_str.map_or(false, |s| s.contains(expected_str)));
}
}
#[test]
fn test_good_login() {
test_login("Sergio", "password", "30", Status::SeeOther, None);
}
#[test]
fn test_invalid_user() {
test_login("-1", "password", "30", Status::Ok, "Unrecognized user");
test_login("Mike", "password", "30", Status::Ok, "Unrecognized user");
}
#[test]
fn test_invalid_password() {
test_login("Sergio", "password1", "30", Status::Ok, "Wrong password!");
test_login("Sergio", "ok", "30", Status::Ok, "Password is invalid: too short!");
}
#[test]
fn test_invalid_age() {
test_login("Sergio", "password", "20", Status::Ok, "must be at least 21.");
test_login("Sergio", "password", "-100", Status::Ok, "must be at least 21.");
test_login("Sergio", "password", "hi", Status::Ok, "value is not a number");
}
fn check_bad_form(form_str: &str, status: Status) {
let client = Client::new(rocket()).unwrap();
let response = client.post("/login")
.header(ContentType::Form)
.body(form_str)
.dispatch();
assert_eq!(response.status(), status);
}
#[test]
fn test_bad_form_abnromal_inputs() {
check_bad_form("&", Status::BadRequest);
check_bad_form("=", Status::BadRequest);
check_bad_form("&&&===&", Status::BadRequest);
}
#[test]
fn test_bad_form_missing_fields() {
let bad_inputs: [&str; 6] = [
"username=Sergio",
"password=pass",
"age=30",
"username=Sergio&password=pass",
"username=Sergio&age=30",
"password=pass&age=30"
];
for bad_input in bad_inputs.into_iter() {
check_bad_form(bad_input, Status::UnprocessableEntity);
}
}
#[test]
fn test_bad_form_additional_fields() {
check_bad_form("username=Sergio&password=pass&age=30&addition=1",
Status::UnprocessableEntity);
}

View File

@ -1,14 +0,0 @@
use rocket::response::NamedFile;
use std::io;
use std::path::{Path, PathBuf};
#[get("/")]
fn index() -> io::Result<NamedFile> {
NamedFile::open("static/index.html")
}
#[get("/<file..>", rank = 5)]
fn files(file: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(Path::new("static/").join(file))
}

View File

@ -1,52 +0,0 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]
extern crate rocket;
mod files;
#[cfg(test)] mod tests;
use rocket::request::Form;
use rocket::response::Redirect;
#[derive(FromForm)]
struct UserLogin<'r> {
username: &'r str,
password: String,
age: Result<usize, &'r str>,
}
#[post("/login", data = "<user_form>")]
fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result<Redirect, String> {
let user = user_form.get();
match user.age {
Ok(age) if age < 21 => return Err(format!("Sorry, {} is too young!", age)),
Ok(age) if age > 120 => return Err(format!("Are you sure you're {}?", age)),
Err(e) => return Err(format!("'{}' is not a valid integer.", e)),
Ok(_) => { /* Move along, adult. */ }
};
if user.username == "Sergio" {
match user.password.as_str() {
"password" => Ok(Redirect::to("/user/Sergio")),
_ => Err("Wrong password!".to_string())
}
} else {
Err(format!("Unrecognized user, '{}'.", user.username))
}
}
#[get("/user/<username>")]
fn user_page(username: &str) -> String {
format!("This is {}'s page.", username)
}
pub fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![files::index, files::files, user_page, login])
}
fn main() {
rocket().launch()
}

View File

@ -1,62 +0,0 @@
use rocket::testing::MockRequest;
use rocket::http::Method::*;
use rocket::http::{ContentType, Status};
use super::rocket;
fn test_login(username: &str, password: &str, age: isize, status: Status,
body: Option<&'static str>) {
let rocket = rocket();
let mut req = MockRequest::new(Post, "/login")
.header(ContentType::Form)
.body(&format!("username={}&password={}&age={}", username, password, age));
let mut response = req.dispatch_with(&rocket);
let body_str = response.body().and_then(|body| body.into_string());
println!("Checking: {:?}/{:?}/{:?}/{:?}", username, password, age, body_str);
assert_eq!(response.status(), status);
if let Some(string) = body {
assert!(body_str.map_or(true, |s| s.contains(string)));
}
}
#[test]
fn test_good_login() {
test_login("Sergio", "password", 30, Status::SeeOther, None);
}
const OK: Status = self::Status::Ok;
#[test]
fn test_bad_login() {
test_login("Sergio", "password", 20, OK, Some("Sorry, 20 is too young!"));
test_login("Sergio", "password", 200, OK, Some("Are you sure you're 200?"));
test_login("Sergio", "jk", -100, OK, Some("'-100' is not a valid integer."));
test_login("Sergio", "ok", 30, OK, Some("Wrong password!"));
test_login("Mike", "password", 30, OK, Some("Unrecognized user, 'Mike'."));
}
fn check_bad_form(form_str: &str, status: Status) {
let rocket = rocket();
let mut req = MockRequest::new(Post, "/login")
.header(ContentType::Form)
.body(form_str);
let response = req.dispatch_with(&rocket);
assert_eq!(response.status(), status);
}
#[test]
fn test_bad_form() {
check_bad_form("&", Status::BadRequest);
check_bad_form("=", Status::BadRequest);
check_bad_form("&&&===&", Status::BadRequest);
check_bad_form("username=Sergio", Status::UnprocessableEntity);
check_bad_form("username=Sergio&", Status::UnprocessableEntity);
check_bad_form("username=Sergio&pass=something", Status::UnprocessableEntity);
check_bad_form("user=Sergio&password=something", Status::UnprocessableEntity);
check_bad_form("password=something", Status::UnprocessableEntity);
}

View File

@ -1,8 +0,0 @@
<h1>Login</h1>
<form action="/login" method="post" accept-charset="utf-8">
Username:<input type="text" name="username">
Password:<input type="password" name="password">
Age:<input type="number" name="age">
<input type="submit" value="Login">
</form>

View File

@ -1,19 +1,16 @@
[package]
name = "handlebars_templates"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
serde = "0.9"
serde_derive = "0.9"
serde_json = "0.9"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
[dependencies.rocket_contrib]
path = "../../contrib"
default-features = false
features = ["handlebars_templates"]
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -3,7 +3,6 @@
extern crate rocket_contrib;
extern crate rocket;
extern crate serde_json;
#[macro_use] extern crate serde_derive;
#[cfg(test)] mod tests;
@ -40,9 +39,13 @@ fn not_found(req: &Request) -> Template {
Template::render("error/404", &map)
}
fn main() {
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![index, get])
.attach(Template::fairing())
.catch(errors![not_found])
.launch();
}
fn main() {
rocket().launch();
}

View File

@ -1,18 +1,15 @@
use rocket;
use rocket::testing::MockRequest;
use super::rocket;
use rocket::local::{Client, LocalResponse};
use rocket::http::Method::*;
use rocket::http::Status;
use rocket::Response;
use rocket_contrib::Template;
macro_rules! run_test {
($req:expr, $test_fn:expr) => ({
let rocket = rocket::ignite()
.mount("/", routes![super::index, super::get])
.catch(errors![super::not_found]);
const TEMPLATE_ROOT: &'static str = "templates/";
let mut req = $req;
$test_fn(req.dispatch_with(&rocket));
macro_rules! dispatch {
($method:expr, $path:expr, $test_fn:expr) => ({
let client = Client::new(rocket()).unwrap();
$test_fn(client.req($method, $path).dispatch());
})
}
@ -20,28 +17,24 @@ macro_rules! run_test {
fn test_root() {
// Check that the redirect works.
for method in &[Get, Head] {
let req = MockRequest::new(*method, "/");
run_test!(req, |mut response: Response| {
dispatch!(*method, "/", |mut response: LocalResponse| {
assert_eq!(response.status(), Status::SeeOther);
assert!(response.body().is_none());
let location_headers: Vec<_> = response.header_values("Location").collect();
assert_eq!(location_headers, vec!["/hello/Unknown"]);
let location: Vec<_> = response.headers().get("Location").collect();
assert_eq!(location, vec!["/hello/Unknown"]);
});
}
// Check that other request methods are not accepted (and instead caught).
for method in &[Post, Put, Delete, Options, Trace, Connect, Patch] {
let req = MockRequest::new(*method, "/");
run_test!(req, |mut response: Response| {
assert_eq!(response.status(), Status::NotFound);
dispatch!(*method, "/", |mut response: LocalResponse| {
let mut map = ::std::collections::HashMap::new();
map.insert("path", "/");
let expected = Template::render("error/404", &map).to_string();
let expected = Template::show(TEMPLATE_ROOT, "error/404", &map).unwrap();
let body_string = response.body().and_then(|body| body.into_string());
assert_eq!(body_string, Some(expected));
assert_eq!(response.status(), Status::NotFound);
assert_eq!(response.body_string(), Some(expected));
});
}
}
@ -49,33 +42,27 @@ fn test_root() {
#[test]
fn test_name() {
// Check that the /hello/<name> route works.
let req = MockRequest::new(Get, "/hello/Jack");
run_test!(req, |mut response: Response| {
assert_eq!(response.status(), Status::Ok);
dispatch!(Get, "/hello/Jack", |mut response: LocalResponse| {
let context = super::TemplateContext {
name: "Jack".to_string(),
items: vec!["One", "Two", "Three"].iter().map(|s| s.to_string()).collect()
};
let expected = Template::render("index", &context).to_string();
let body_string = response.body().and_then(|body| body.into_string());
assert_eq!(body_string, Some(expected));
let expected = Template::show(TEMPLATE_ROOT, "index", &context).unwrap();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.body_string(), Some(expected));
});
}
#[test]
fn test_404() {
// Check that the error catcher works.
let req = MockRequest::new(Get, "/hello/");
run_test!(req, |mut response: Response| {
assert_eq!(response.status(), Status::NotFound);
dispatch!(Get, "/hello/", |mut response: LocalResponse| {
let mut map = ::std::collections::HashMap::new();
map.insert("path", "/hello/");
let expected = Template::render("error/404", &map).to_string();
let body_string = response.body().and_then(|body| body.into_string());
assert_eq!(body_string, Some(expected));
let expected = Template::show(TEMPLATE_ROOT, "error/404", &map).unwrap();
assert_eq!(response.status(), Status::NotFound);
assert_eq!(response.body_string(), Some(expected));
});
}

View File

@ -1,11 +0,0 @@
[package]
name = "hello_alt_methods"
version = "0.0.1"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -1,26 +0,0 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
use std::io;
use rocket::response::NamedFile;
#[cfg(test)] mod tests;
#[get("/")]
fn index() -> io::Result<NamedFile> {
NamedFile::open("static/index.html")
}
#[put("/")]
fn put() -> &'static str {
"Hello, PUT request!"
}
fn main() {
rocket::ignite()
.mount("/", routes![index, put])
.launch();
}

View File

@ -1,25 +0,0 @@
use super::rocket;
use rocket::testing::MockRequest;
use rocket::http::Method;
use rocket::http::Status;
fn test(method: Method, status: Status, body_prefix: Option<&str>) {
let rocket = rocket::ignite()
.mount("/", routes![super::index, super::put]);
let mut req = MockRequest::new(method, "/");
let mut response = req.dispatch_with(&rocket);
assert_eq!(response.status(), status);
if let Some(expected_body_string) = body_prefix {
let body_str = response.body().and_then(|body| body.into_string()).unwrap();
assert!(body_str.starts_with(expected_body_string));
}
}
#[test]
fn hello_world_alt_methods() {
test(Method::Get, Status::Ok, Some("<!DOCTYPE html>"));
test(Method::Put, Status::Ok, Some("Hello, PUT request!"));
test(Method::Post, Status::NotFound, None);
}

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Hello Alt Methods</title>
</head>
<body>
<form action="/" method="post" accept-charset="utf-8">
<input type="hidden" name="_method" id="_method" value="put" />
<input type="submit" name="Submit" id="Submit" value="submit" />
</form>
</body>
</html>

View File

@ -1,11 +1,8 @@
[package]
name = "hello_person"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -6,12 +6,12 @@ extern crate rocket;
#[cfg(test)] mod tests;
#[get("/hello/<name>/<age>")]
fn hello(name: &str, age: u8) -> String {
fn hello(name: String, age: u8) -> String {
format!("Hello, {} year old named {}!", age, name)
}
#[get("/hello/<name>")]
fn hi<'r>(name: &'r str) -> &'r str {
fn hi(name: String) -> String {
name
}

View File

@ -1,21 +1,19 @@
use super::rocket;
use rocket::testing::MockRequest;
use rocket::http::Method::*;
use rocket::local::Client;
use rocket::http::Status;
fn test(uri: &str, expected: String) {
let rocket = rocket::ignite().mount("/", routes![super::hello, super::hi]);
let mut req = MockRequest::new(Get, uri);
let mut response = req.dispatch_with(&rocket);
fn client() -> Client {
Client::new(rocket::ignite().mount("/", routes![super::hello, super::hi])).unwrap()
}
assert_eq!(response.body().and_then(|b| b.into_string()), Some(expected));
fn test(uri: &str, expected: String) {
let client = client();
assert_eq!(client.get(uri).dispatch().body_string(), Some(expected));
}
fn test_404(uri: &str) {
let rocket = rocket::ignite().mount("/", routes![super::hello, super::hi]);
let mut req = MockRequest::new(Get, uri);
let response = req.dispatch_with(&rocket);
assert_eq!(response.status(), Status::NotFound);
let client = client();
assert_eq!(client.get(uri).dispatch().status(), Status::NotFound);
}
#[test]

View File

@ -1,11 +1,8 @@
[package]
name = "hello_world"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -1,13 +1,10 @@
use super::rocket;
use rocket::testing::MockRequest;
use rocket::http::Method::*;
use rocket::local::Client;
#[test]
fn hello_world() {
let rocket = rocket::ignite().mount("/", routes![super::hello]);
let mut req = MockRequest::new(Get, "/");
let mut response = req.dispatch_with(&rocket);
let body_str = response.body().and_then(|body| body.into_string());
assert_eq!(body_str, Some("Hello, world!".to_string()));
let client = Client::new(rocket).unwrap();
let mut response = client.get("/").dispatch();
assert_eq!(response.body_string(), Some("Hello, world!".into()));
}

View File

@ -1,20 +1,16 @@
[package]
name = "json"
version = "0.0.1"
version = "0.0.0"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
serde = "0.9"
serde_json = "0.9"
serde_derive = "0.9"
lazy_static = "*"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
[dependencies.rocket_contrib]
path = "../../contrib"
default-features = false
features = ["json"]
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -2,14 +2,13 @@
#![plugin(rocket_codegen)]
extern crate rocket;
extern crate serde_json;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
#[cfg(test)] mod tests;
use rocket_contrib::{JSON, Value};
use rocket_contrib::{Json, JsonValue};
use rocket::State;
use std::collections::HashMap;
use std::sync::Mutex;
@ -17,9 +16,7 @@ use std::sync::Mutex;
type ID = usize;
// 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());
}
type MessageMap = Mutex<HashMap<ID, String>>;
#[derive(Serialize, Deserialize)]
struct Message {
@ -27,37 +24,37 @@ struct Message {
contents: String
}
// TODO: This example can be improved by using `route` with muliple HTTP verbs.
// TODO: This example can be improved by using `route` with multiple HTTP verbs.
#[post("/<id>", format = "application/json", data = "<message>")]
fn new(id: ID, message: JSON<Message>) -> JSON<Value> {
let mut hashmap = MAP.lock().expect("map lock.");
fn new(id: ID, message: Json<Message>, map: State<MessageMap>) -> JsonValue {
let mut hashmap = map.lock().expect("map lock.");
if hashmap.contains_key(&id) {
JSON(json!({
json!({
"status": "error",
"reason": "ID exists. Try put."
}))
})
} else {
hashmap.insert(id, message.0.contents);
JSON(json!({ "status": "ok" }))
json!({ "status": "ok" })
}
}
#[put("/<id>", format = "application/json", data = "<message>")]
fn update(id: ID, message: JSON<Message>) -> Option<JSON<Value>> {
let mut hashmap = MAP.lock().unwrap();
fn update(id: ID, message: Json<Message>, map: State<MessageMap>) -> Option<JsonValue> {
let mut hashmap = map.lock().unwrap();
if hashmap.contains_key(&id) {
hashmap.insert(id, message.0.contents);
Some(JSON(json!({ "status": "ok" })))
Some(json!({ "status": "ok" }))
} else {
None
}
}
#[get("/<id>", format = "application/json")]
fn get(id: ID) -> Option<JSON<Message>> {
let hashmap = MAP.lock().unwrap();
fn get(id: ID, map: State<MessageMap>) -> Option<Json<Message>> {
let hashmap = map.lock().unwrap();
hashmap.get(&id).map(|contents| {
JSON(Message {
Json(Message {
id: Some(id),
contents: contents.clone()
})
@ -65,16 +62,20 @@ fn get(id: ID) -> Option<JSON<Message>> {
}
#[error(404)]
fn not_found() -> JSON<Value> {
JSON(json!({
fn not_found() -> JsonValue {
json!({
"status": "error",
"reason": "Resource was not found."
}))
})
}
fn main() {
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/message", routes![new, update, get])
.catch(errors![not_found])
.launch();
.manage(Mutex::new(HashMap::<ID, String>::new()))
}
fn main() {
rocket().launch();
}

Some files were not shown because too many files have changed in this diff Show More