mirror of https://github.com/rwf2/Rocket.git
commit
d36687c136
|
@ -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.
|
|
@ -22,3 +22,6 @@ scripts/upload-docs.sh
|
|||
|
||||
# Backup files.
|
||||
*.bak
|
||||
|
||||
# Uploads in pastebin example.
|
||||
examples/pastebin/upload/*
|
||||
|
|
|
@ -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
|
||||
|
|
390
CHANGELOG.md
390
CHANGELOG.md
|
@ -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.
|
||||
|
|
19
Cargo.toml
19
Cargo.toml
|
@ -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",
|
||||
]
|
||||
|
|
12
README.md
12
README.md
|
@ -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).
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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."));
|
||||
|
|
|
@ -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 })
|
||||
});
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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?");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() { }
|
||||
|
|
|
@ -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() { }
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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() { }
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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() { }
|
|
@ -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
|
||||
|
|
|
@ -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() { }
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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 { }));
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
#![feature(plugin)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![allow(dead_code)]
|
||||
#![deny(unmounted_route)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
extern crate rocket;
|
||||
|
||||
#[get("/<todo>")]
|
||||
fn todo(todo: &str) -> &str {
|
||||
fn todo(todo: String) -> String {
|
||||
todo
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#![feature(plugin)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![allow(dead_code, unused_variables)]
|
||||
#![deny(unmounted_routes, unmanaged_state)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#![feature(plugin)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![allow(dead_code, unused_variables)]
|
||||
#![deny(unmounted_route, unmanaged_state)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)+))
|
||||
};
|
||||
}
|
|
@ -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)+)
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,53 +1,35 @@
|
|||
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);
|
||||
|
@ -55,3 +37,4 @@ pub fn render<T>(name: &str, _info: &TemplateInfo, context: &T) -> Option<String
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)+
|
||||
});
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
None
|
||||
} else {
|
||||
Some(tera)
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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="
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
#![feature(plugin)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
#[get("/")]
|
||||
#[allow(unmounted_route)]
|
||||
pub fn hello() -> &'static str {
|
||||
"Hello, world!"
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -8,5 +8,4 @@ mod common;
|
|||
#[test]
|
||||
fn test_development_config() {
|
||||
common::test_config(rocket::config::Environment::Development);
|
||||
common::test_hello();
|
||||
}
|
||||
|
|
|
@ -8,5 +8,4 @@ mod common;
|
|||
#[test]
|
||||
fn test_production_config() {
|
||||
common::test_config(rocket::config::Environment::Production);
|
||||
common::test_hello();
|
||||
}
|
||||
|
|
|
@ -8,5 +8,4 @@ mod common;
|
|||
#[test]
|
||||
fn test_staging_config() {
|
||||
common::test_config(rocket::config::Environment::Staging);
|
||||
common::test_hello();
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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/<name>/<age> 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();
|
||||
}
|
||||
|
|
|
@ -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/<name>/<age> instead.</p>";
|
||||
test("/unknown", ContentType::JSON, Status::NotFound, body.to_string());
|
||||
test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string());
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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/<name>/<age> 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);
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "extended_validation"
|
||||
version = "0.0.1"
|
||||
name = "fairings"
|
||||
version = "0.0.0"
|
||||
workspace = "../../"
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,2 @@
|
|||
[global]
|
||||
token = 123
|
|
@ -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();
|
||||
}
|
|
@ -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()));
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "form_kitchen_sink"
|
||||
version = "0.0.1"
|
||||
version = "0.0.0"
|
||||
workspace = "../../"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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 />
|
||||
|
|
|
@ -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"] }
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
|
@ -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"] }
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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"] }
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue