Completely revamp, redo examples.

The new examples directory...

  * Contains a `README.md` explaining each example.
  * Consolidates examples into more complete chunks.
  * Is just better.

Resolves #1447.
This commit is contained in:
Sergio Benitez 2021-04-07 19:01:48 -07:00
parent cfd5af38fe
commit 50c9e88cf9
141 changed files with 2036 additions and 1842 deletions

View File

@ -0,0 +1,36 @@
use rocket::{get, routes};
use rocket::local::blocking::Client;
mod inner {
use rocket::uri;
#[rocket::get("/")]
pub fn hello() -> String {
format!("Hello! Try {}.", uri!(super::hello_name: "Rust 2018"))
}
}
#[get("/<name>")]
fn hello_name(name: String) -> String {
format!("Hello, {}! This is {}.", name, rocket::uri!(hello_name: &name))
}
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![hello_name])
.mount("/", rocket::routes![inner::hello])
}
#[test]
fn test_inner_hello() {
let client = Client::debug(rocket()).unwrap();
let response = client.get("/").dispatch();
assert_eq!(response.into_string(), Some("Hello! Try /Rust%202018.".into()));
}
#[test]
fn test_hello_name() {
let client = Client::debug(rocket()).unwrap();
let response = client.get("/Rust%202018").dispatch();
assert_eq!(response.into_string().unwrap(), "Hello, Rust 2018! This is /Rust%202018.");
}

View File

@ -1,35 +1,22 @@
[workspace] [workspace]
members = [ members = [
"cookies",
"errors",
"forms",
"hello_person",
"query_params",
"hello_world",
"manual_routes",
"optional_redirect",
"redirect",
"static_files",
"todo",
"content_types",
"ranking",
"testing",
"request_local_state",
"request_guard",
"stream",
"json",
"msgpack",
"handlebars_templates",
"tera_templates",
"config", "config",
"raw_upload", "cookies",
"pastebin", "databases",
"state", "error-handling",
"managed_queue",
"uuid",
"session",
"raw_sqlite",
"tls",
"fairings", "fairings",
"hello_2018", "forms",
"hello",
"manual-routing",
"responders",
"serialization",
"state",
"static-files",
"templating",
"testing",
"tls",
"uuid",
"pastebin",
"todo",
] ]

85
examples/README.md Normal file
View File

@ -0,0 +1,85 @@
# Rocket Examples
This directory contains projects showcasing Rocket's features.
## Applications
* **[`pastebin`](./pastebin)**
A simple, API-only pastebin application, similar to https://paste.rs. Stores
pastes locally on the file system. Implements a custom parameter guard,
`PasteId`, to parse and validate paste identifiers.
* **[`todo`](./todo)**
A todo app with a web UI to add, delete, and mark/unmark items. Uses a
SQLite database driven by diesel. Runs migrations automatically at start-up.
Uses tera to render templates.
## Feature Examples
* **[`config`](./config)** - Illustrates how to extract values from a Rocket
`Figment`, how to store and retrieve an application specific configuration
in managed state using `AdHoc::config()`, and how to set configuration
values in `Rocket.toml`.
* **[`cookies`](./cookies)** - Uses cookies to create a client-side message
box. Uses private cookies for a session-based authentication.
* **[`databases`](./databases)** - Implements a CRUD-like "blog" JSON API
backed by a SQLite database driven by each of `sqlx`, `diesel`, and
`rusqlite`. Runs migrations automatically for the former two drivers. Uses
`contrib` database support for the latter two drivers.
* **[`error-handling`](./error-handling)** - Exhibits the use of scoped
catchers; contains commented out lines that will cause a launch-time error
with code to custom-display the error.
* **[`fairings`](./fairings)** - Exemplifies creating a custom `Counter`
fairing and using `AdHoc` fairings.
* **[`forms`](./forms)** - Showcases all of Rocket's form support features
including multipart file uploads, ad-hoc validations, field renaming, and
use of form context for staged forms.
* **[`hello`](./hello)** - Basic example of Rocket's core features: route
declaration with path and query parameters, both simple and compound,
mounting, launching, testing, and returning simple responses. Also showcases
using UTF-8 in route declarations and responses.
* **[`manual-routing`](./manual-routing)** - An example eschewing Rocket's
codegen in favor of manual routing. This should be seen as last-ditch
effort, much like `unsafe` in Rust, as manual routing _also_ eschews many of
Rocket's automatic web security guarantees.
* **[`responders`](./responders)** - Illustrates the use of many of Rocket's
built-in responders: `Stream`, `Redirect`, `File`, `NamedFile`, `content`
for manually setting Content-Types, and `Either`. In the process, showcases
using `TempFile` for raw uploads. Also illustrates the creation of a custom,
derived `Responder`.
* **[`serialization`](./serialization)** - Showcases JSON and MessagePack
(de)serialization support in `contrib` by implementing a CRUD-like message
API in JSON and a simply read/echo API in MessagePack.
* **[`state`](./state)** - Illustrates the use of request-local state and
managed state. Uses request-local state to cache "expensive" per-request
operations. Uses managed state to implement a simple index hit counter. Also
uses managed state to store, retrieve, and push/pop from a concurrent queue.
* **[`static-files`](./static-files)** - Uses `contrib` `StaticFiles` serve
static files. Also creates a `second` manual yet safe version.
* **[`templating`](./templating)** - Illustrates using `contrib` `templates`
support with identical examples for handlebars and tera.
* **[`testing`](./testing)** - Uses Rocket's `local` libraries to test an
application. Showcases necessary use of the `async` `Client`. Note that all
examples contains tests, themselves serving as examples for how to test
Rocket applications.
* **[`tls`](./tls)** - Illustrates configuring TLS with a variety of key pair
kinds.
* **[`uuid`](./uuid)** - Uses UUID support in `contrib`, converting between
`contrib::Uuid` type and the `uuid` crate `Uuid`.

View File

@ -7,3 +7,4 @@ publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib", features = ["secrets"] } rocket = { path = "../../core/lib", features = ["secrets"] }
serde = { version = "1", features = ["derive"] }

View File

@ -1,20 +1,22 @@
# Except for the secret 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. # defaults. We show all of them here explicitly for demonstrative purposes.
[global.limits] [default.limits]
forms = "64 kB" forms = "64 kB"
json = "1 MiB" json = "1 MiB"
msgpack = "2 MiB" msgpack = "2 MiB"
"file/jpg" = "5 MiB" "file/jpg" = "5 MiB"
[default]
key = "a default app-key"
extra = false
[debug] [debug]
address = "127.0.0.1" address = "127.0.0.1"
port = 8000 port = 8000
workers = 1 workers = 1
keep_alive = 0 keep_alive = 0
log_level = "normal" log_level = "normal"
hi = "Hello!" # this is an unused extra; maybe application specific?
is_extra = true # this is an unused extra; maybe application specific?
[release] [release]
address = "127.0.0.1" address = "127.0.0.1"
@ -24,3 +26,5 @@ keep_alive = 5
log_level = "critical" log_level = "critical"
# don't use this key! generate your own and keep it private! # don't use this key! generate your own and keep it private!
secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk="
key = "a release app-key"
extra = false

View File

@ -1,14 +1,29 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests; #[cfg(test)] mod tests;
use rocket::{State, Config};
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
// This example's illustration is the Rocket.toml file. Running this server will use serde::Deserialize;
// print the config, however.
#[rocket::launch] #[derive(Debug, Deserialize)]
fn rocket() -> rocket::Rocket { struct AppConfig {
rocket::ignite() key: String,
.attach(AdHoc::on_liftoff("Config Reader", |rocket| Box::pin(async move { port: u16
let value = rocket.figment().find_value("").unwrap(); }
println!("{:#?}", value);
}))) #[get("/")]
fn read_config(rocket_config: &Config, app_config: State<'_, AppConfig>) -> String {
format!("{:#?}\n{:#?}", app_config, rocket_config)
}
// See Rocket.toml file. Running this server will print the config. Try running
// with `ROCKET_PROFILE=release` manually by setting the environment variable
// and automatically by compiling with `--release`.
#[launch]
fn rocket() -> _ {
rocket::ignite()
.mount("/", routes![read_config])
.attach(AdHoc::config::<AppConfig>())
} }

View File

@ -1,11 +0,0 @@
[package]
name = "content_types"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@ -1,67 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use std::io;
use rocket::request::Request;
use rocket::data::{Data, ToByteUnit};
use rocket::response::content::{Json, Html};
use serde::{Serialize, Deserialize};
// NOTE: This example explicitly uses the `Json` type from `response::content`
// for demonstration purposes. In a real application, _always_ prefer to use
// `rocket_contrib::json::Json` instead!
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
age: u8,
}
// 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. Note: if this was a real application, we'd use
// `rocket_contrib`'s built-in JSON support and return a `JsonValue` instead.
#[get("/<name>/<age>", format = "json")]
fn get_hello(name: String, age: u8) -> io::Result<Json<String>> {
// NOTE: In a real application, we'd use `rocket_contrib::json::Json`.
let person = Person { name, age };
Ok(Json(serde_json::to_string(&person)?))
}
// In a `POST` request and all other payload supporting request types, the
// content type is matched against the `format` in the route attribute.
//
// Note that `content::Json` simply sets the content-type to `application/json`.
// In a real application, we wouldn't use `serde_json` directly; instead, we'd
// use `contrib::Json` to automatically serialize a type into JSON.
#[post("/<age>", format = "plain", data = "<name_data>")]
async fn post_hello(age: u8, name_data: Data) -> io::Result<Json<String>> {
let name = name_data.open(64.bytes()).into_string().await?;
let person = Person { name: name.into_inner(), age };
// NOTE: In a real application, we'd use `rocket_contrib::json::Json`.
Ok(Json(serde_json::to_string(&person)?))
}
#[catch(404)]
fn not_found(request: &Request<'_>) -> Html<String> {
let html = match request.format() {
Some(ref mt) if !mt.is_json() && !mt.is_plain() => {
format!("<p>'{}' requests are not supported.</p>", mt)
}
_ => format!("<p>Sorry, '{}' is an invalid path! Try \
/hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
request.uri())
};
Html(html)
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/hello", routes![get_hello, post_hello])
.register("/", catchers![not_found])
}

View File

@ -1,41 +0,0 @@
use super::Person;
use rocket::http::{Accept, ContentType, Header, MediaType, Method, Status};
use rocket::local::blocking::Client;
fn test<H>(method: Method, uri: &str, header: H, status: Status, body: String)
where H: Into<Header<'static>>
{
let client = Client::tracked(super::rocket()).unwrap();
let response = client.req(method, uri).header(header).dispatch();
assert_eq!(response.status(), status);
assert_eq!(response.into_string(), Some(body));
}
#[test]
fn test_hello() {
let person = Person { name: "Michael".to_string(), age: 80, };
let body = serde_json::to_string(&person).unwrap();
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 b = format!("<p>'{}' requests are not supported.</p>", MediaType::HTML);
test(Method::Get, "/hello/Michael/80", Accept::HTML, Status::NotFound, b.clone());
test(Method::Post, "/hello/80", ContentType::HTML, Status::NotFound, b);
}
#[test]
fn test_404() {
let body = "<p>Sorry, '/unknown' is an invalid path! Try \
/hello/&lt;name&gt;/&lt;age&gt; instead.</p>";
test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string());
}

View File

@ -6,7 +6,7 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib", features = ["secrets"] }
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
path = "../../contrib/lib" path = "../../contrib/lib"

View File

@ -1,33 +1,23 @@
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
#[cfg(test)] #[cfg(test)] mod tests;
mod tests;
use std::collections::HashMap; mod session;
mod message;
use rocket::form::Form; use rocket::response::content::Html;
use rocket::response::Redirect;
use rocket::http::{Cookie, CookieJar};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
#[post("/submit", data = "<message>")]
fn submit(cookies: &CookieJar<'_>, message: Form<String>) -> Redirect {
cookies.add(Cookie::new("message", message.into_inner()));
Redirect::to("/")
}
#[get("/")] #[get("/")]
fn index(cookies: &CookieJar<'_>) -> Template { fn index() -> Html<&'static str> {
let cookie = cookies.get("message"); Html(r#"<a href="message">Set a Message</a> or <a href="session">Use Sessions</a>."#)
let mut context = HashMap::new();
if let Some(ref cookie) = cookie {
context.insert("message", cookie.value());
}
Template::render("index", &context)
} }
#[launch] #[launch]
fn rocket() -> rocket::Rocket { fn rocket() -> _ {
rocket::ignite().mount("/", routes![submit, index]).attach(Template::fairing()) rocket::ignite()
.attach(Template::fairing())
.mount("/", routes![index])
.mount("/message", message::routes())
.mount("/session", session::routes())
} }

View File

@ -0,0 +1,34 @@
use std::collections::HashMap;
use rocket::form::Form;
use rocket::response::Redirect;
use rocket::http::{Cookie, CookieJar};
use rocket_contrib::templates::Template;
#[macro_export]
macro_rules! message_uri {
($($t:tt)*) => (rocket::uri!("/message", $crate::message:: $($t)*))
}
pub use message_uri as uri;
#[post("/", data = "<message>")]
fn submit(cookies: &CookieJar<'_>, message: Form<&str>) -> Redirect {
cookies.add(Cookie::new("message", message.to_string()));
Redirect::to(uri!(index))
}
#[get("/")]
fn index(cookies: &CookieJar<'_>) -> Template {
let cookie = cookies.get("message");
let mut context = HashMap::new();
if let Some(ref cookie) = cookie {
context.insert("message", cookie.value());
}
Template::render("message", &context)
}
pub fn routes() -> Vec<rocket::Route> {
routes![submit, index]
}

View File

@ -1,7 +1,3 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use std::collections::HashMap; use std::collections::HashMap;
use rocket::outcome::IntoOutcome; use rocket::outcome::IntoOutcome;
@ -13,9 +9,9 @@ use rocket::form::Form;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
#[derive(FromForm)] #[derive(FromForm)]
struct Login { struct Login<'r> {
username: String, username: &'r str,
password: String password: &'r str
} }
#[derive(Debug)] #[derive(Debug)]
@ -29,13 +25,42 @@ impl<'r> FromRequest<'r> for User {
request.cookies() request.cookies()
.get_private("user_id") .get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok()) .and_then(|cookie| cookie.value().parse().ok())
.map(|id| User(id)) .map(User)
.or_forward(()) .or_forward(())
} }
} }
#[macro_export]
macro_rules! session_uri {
($($t:tt)*) => (rocket::uri!("/session", $crate::session:: $($t)*))
}
pub use session_uri as uri;
#[get("/")]
fn index(user: User) -> Template {
let mut context = HashMap::new();
context.insert("user_id", user.0);
Template::render("session", &context)
}
#[get("/", rank = 2)]
fn no_auth_index() -> Redirect {
Redirect::to(uri!(login_page))
}
#[get("/login")]
fn login(_user: User) -> Redirect {
Redirect::to(uri!(index))
}
#[get("/login", rank = 2)]
fn login_page(flash: Option<FlashMessage<'_>>) -> Template {
Template::render("login", &flash)
}
#[post("/login", data = "<login>")] #[post("/login", data = "<login>")]
fn login(cookies: &CookieJar<'_>, login: Form<Login>) -> Result<Redirect, Flash<Redirect>> { fn post_login(cookies: &CookieJar<'_>, login: Form<Login<'_>>) -> Result<Redirect, Flash<Redirect>> {
if login.username == "Sergio" && login.password == "password" { if login.username == "Sergio" && login.password == "password" {
cookies.add_private(Cookie::new("user_id", 1.to_string())); cookies.add_private(Cookie::new("user_id", 1.to_string()));
Ok(Redirect::to(uri!(index))) Ok(Redirect::to(uri!(index)))
@ -50,39 +75,6 @@ fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.") Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.")
} }
#[get("/login")] pub fn routes() -> Vec<rocket::Route> {
fn login_user(_user: User) -> Redirect { routes![index, no_auth_index, login, login_page, post_login, logout]
Redirect::to(uri!(index))
}
#[get("/login", rank = 2)]
fn login_page(flash: Option<FlashMessage<'_>>) -> Template {
let mut context = HashMap::new();
if let Some(ref msg) = flash {
context.insert("flash", msg.msg());
if msg.name() == "error" {
context.insert("flash_type", "Error");
}
}
Template::render("login", &context)
}
#[get("/")]
fn user_index(user: User) -> Template {
let mut context = HashMap::new();
context.insert("user_id", user.0);
Template::render("index", &context)
}
#[get("/", rank = 2)]
fn index() -> Redirect {
Redirect::to(uri!(login_page))
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite()
.attach(Template::fairing())
.mount("/", routes![index, user_index, login, logout, login_user, login_page])
} }

View File

@ -1,14 +1,89 @@
use std::collections::HashMap; use super::{rocket, session, message};
use rocket::local::blocking::{Client, LocalResponse};
use rocket::http::{Status, Cookie, ContentType};
use super::rocket; fn user_id_cookie(response: &LocalResponse<'_>) -> Option<Cookie<'static>> {
use rocket::local::blocking::Client; let cookie = response.headers()
use rocket::http::*; .get("Set-Cookie")
use rocket_contrib::templates::Template; .filter(|v| v.starts_with("user_id"))
.nth(0)
.and_then(|val| Cookie::parse_encoded(val).ok());
cookie.map(|c| c.into_owned())
}
fn login(client: &Client, user: &str, pass: &str) -> Option<Cookie<'static>> {
let response = client.post(session::uri!(login))
.header(ContentType::Form)
.body(format!("username={}&password={}", user, pass))
.dispatch();
user_id_cookie(&response)
}
#[test] #[test]
fn test_submit() { fn redirect_logged_out_session() {
let client = Client::tracked(rocket()).unwrap(); let client = Client::tracked(rocket()).unwrap();
let response = client.post("/submit") let response = client.get(session::uri!(index)).dispatch();
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(login));
let response = client.get(session::uri!(login_page)).dispatch();
assert_eq!(response.status(), Status::Ok);
let body = response.into_string().unwrap();
assert!(body.contains("Please login to continue."));
}
#[test]
fn login_fails() {
let client = Client::tracked(rocket()).unwrap();
assert!(login(&client, "Seergio", "password").is_none());
assert!(login(&client, "Sergio", "idontknow").is_none());
}
#[test]
fn login_logout_succeeds() {
let client = Client::tracked(rocket()).unwrap();
let login_cookie = login(&client, "Sergio", "password").expect("logged in");
// Ensure we're logged in.
let response = client.get(session::uri!(index)).cookie(login_cookie.clone()).dispatch();
assert_eq!(response.status(), Status::Ok);
let body = response.into_string().unwrap();
assert!(body.contains("Logged in with user ID 1"));
// One more.
let response = client.get(session::uri!(login)).cookie(login_cookie.clone()).dispatch();
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(index));
// Logout.
let response = client.post(session::uri!(logout)).cookie(login_cookie).dispatch();
let cookie = user_id_cookie(&response).expect("logout cookie");
assert!(cookie.value().is_empty());
// The user should be redirected back to the login page.
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(login));
// The page should show the success message, and no errors.
let response = client.get(session::uri!(login)).dispatch();
assert_eq!(response.status(), Status::Ok);
let body = response.into_string().unwrap();
assert!(body.contains("success: Successfully logged out."));
assert!(!body.contains("Error"));
}
#[test]
fn test_message() {
let client = Client::tracked(rocket()).unwrap();
// Check that there's no message initially.
let response = client.get(message::uri!(index)).dispatch();
assert!(response.into_string().unwrap().contains("No message yet."));
// Now set a message; we should get a cookie back.
let response = client.post(message::uri!(submit))
.header(ContentType::Form) .header(ContentType::Form)
.body("message=Hello from Rocket!") .body("message=Hello from Rocket!")
.dispatch(); .dispatch();
@ -16,35 +91,10 @@ fn test_submit() {
let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect(); let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect();
assert_eq!(cookie_headers.len(), 1); assert_eq!(cookie_headers.len(), 1);
assert!(cookie_headers[0].starts_with("message=Hello%20from%20Rocket!")); assert!(cookie_headers[0].starts_with("message=Hello%20from%20Rocket!"));
assert_eq!(response.headers().get_one("Location").unwrap(), &message::uri!(index));
let location_headers: Vec<_> = response.headers().get("Location").collect();
assert_eq!(location_headers, vec!["/".to_string()]);
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
}
// Check that the message is reflected.
fn test_body(optional_cookie: Option<Cookie<'static>>, expected_body: String) { let response = client.get(message::uri!(index)).dispatch();
// Attach a cookie if one is given. assert!(response.into_string().unwrap().contains("Hello from Rocket!"));
let client = Client::tracked(rocket()).unwrap();
let response = match optional_cookie {
Some(cookie) => client.get("/").cookie(cookie).dispatch(),
None => client.get("/").dispatch(),
};
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some(expected_body));
}
#[test]
fn test_index() {
let client = Client::tracked(rocket()).unwrap();
// Render the template with an empty context.
let mut context: HashMap<&str, &str> = HashMap::new();
let template = Template::show(client.rocket(), "index", &context).unwrap();
test_body(None, template);
// Render the template with a context that contains the message.
context.insert("message", "Hello from Rocket!");
let template = Template::show(client.rocket(), "index", &context).unwrap();
test_body(Some(Cookie::new("message", "Hello from Rocket!")), template);
} }

View File

@ -10,16 +10,18 @@
<p>Please login to continue.</p> <p>Please login to continue.</p>
{{#if flash}} {{#if message}}
<p>{{#if flash_type}}{{flash_type}}: {{/if}}{{ flash }}</p> <p>{{#if kind}}{{kind}}: {{/if}}{{ message }}</p>
{{/if}} {{/if}}
<form action="/login" method="post" accept-charset="utf-8"> <form action="/session/login" method="post" accept-charset="utf-8">
<label for="username">username</label> <label for="username">username</label>
<input type="text" name="username" id="username" value="" /> <input type="text" name="username" id="username" value="" />
<label for="password">password</label> <label for="password">password</label>
<input type="password" name="password" id="password" value="" /> <input type="password" name="password" id="password" value="" />
<p><input type="submit" value="login"></p> <p><input type="submit" value="login"></p>
</form> </form>
<a href="/">Home</a>
</body> </body>
</html> </html>

View File

@ -3,20 +3,22 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<title>Rocket: Cookie Examples</title> <title>Rocket: Cookie Message</title>
</head> </head>
<body> <body>
<h1>Rocket Cookie Examples</h1> <h1>Rocket Cookie Message</h1>
{{#if message }} {{#if message }}
<p>{{message}}</p> <p>{{message}}</p>
{{else}} {{else}}
<p>No message yet.</p> <p>No message yet.</p>
{{/if}} {{/if}}
<form action="/submit" method="post" accept-charset="utf-8"> <form action="/message" method="post" accept-charset="utf-8">
<textarea placeholder="Your message here..." <textarea placeholder="Your message here..."
name="message" rows="10" cols="50"></textarea> name="message" rows="10" cols="50"></textarea>
<p><input type="submit" value="Set Cookie"></p> <p><input type="submit" value="Set Cookie"></p>
</form> </form>
<a href="/">Home</a>
</body> </body>
</html> </html>

View File

@ -8,8 +8,10 @@
<body> <body>
<h1>Rocket Session Example</h1> <h1>Rocket Session Example</h1>
<p>Logged in with user ID {{ user_id }}.</p> <p>Logged in with user ID {{ user_id }}.</p>
<form action="/logout" method="post" accept-charset="utf-8"> <form action="/session/logout" method="post" accept-charset="utf-8">
<input type="submit" name="logout" id="logout" value="logout" /> <input type="submit" name="logout" id="logout" value="logout" />
</form> </form>
<a href="/">Home</a>
</body> </body>
</html> </html>

View File

@ -0,0 +1,23 @@
[package]
name = "databases"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diesel = { version = "1.3", features = ["sqlite", "r2d2"] }
diesel_migrations = "1.3"
[dependencies.sqlx]
version = "0.5.1"
default-features = false
features = ["runtime-tokio-rustls", "sqlite", "macros", "offline", "migrate"]
[dependencies.rocket_contrib]
path = "../../contrib/lib"
default-features = false
features = ["diesel_sqlite_pool", "sqlite_pool", "json"]

View File

@ -0,0 +1,8 @@
[default.databases.rusqlite]
url = "file:rusqlite?mode=memory&cache=shared"
[default.databases.sqlx]
url = "db/sqlx/db.sqlite"
[default.databases.diesel]
url = "db/diesel/db.sqlite"

View File

@ -0,0 +1 @@
DROP TABLE posts;

View File

@ -0,0 +1,6 @@
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR NOT NULL,
text VARCHAR NOT NULL,
published BOOLEAN NOT NULL DEFAULT 0
);

View File

@ -0,0 +1,6 @@
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR NOT NULL,
text VARCHAR NOT NULL,
published BOOLEAN NOT NULL DEFAULT 0
);

View File

@ -0,0 +1,81 @@
{
"db": "SQLite",
"11e3096becb72f427c8d3911ef4327afd9516143806981e11f8e34d069c14472": {
"query": "SELECT id, title, text FROM posts WHERE id = ?",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "title",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "text",
"ordinal": 2,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
}
},
"3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf": {
"query": "INSERT INTO posts (title, text) VALUES (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
}
},
"4415c35941e52a981b10707fe2e1ceb0bad0e473701e51ef21ecb2973c76b4df": {
"query": "SELECT id FROM posts",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false
]
}
},
"668690acaca0a0c0b4ac306b14d82aa1bee940f0776fae3f9962639b78328858": {
"query": "DELETE FROM posts",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
}
},
"79301b44b77802e0096efd73b1e9adac27b27a3cf7bf853af3a9f130b1684d91": {
"query": "DELETE FROM posts WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
}
}
}

View File

@ -0,0 +1,104 @@
use rocket::Rocket;
use rocket::fairing::AdHoc;
use rocket::response::{Debug, status::Created};
use rocket_contrib::databases::diesel;
use rocket_contrib::json::Json;
use self::diesel::prelude::*;
use serde::{Serialize, Deserialize};
#[database("diesel")]
struct Db(diesel::SqliteConnection);
type Result<T, E = Debug<diesel::result::Error>> = std::result::Result<T, E>;
#[derive(Debug, Clone, Deserialize, Serialize, Queryable, Insertable)]
#[table_name="posts"]
struct Post {
#[serde(skip_deserializing)]
id: Option<i32>,
title: String,
text: String,
#[serde(skip_deserializing)]
published: bool,
}
table! {
posts (id) {
id -> Nullable<Integer>,
title -> Text,
text -> Text,
published -> Bool,
}
}
#[post("/", data = "<post>")]
async fn create(db: Db, post: Json<Post>) -> Result<Created<Json<Post>>> {
let post_value = post.clone();
db.run(move |conn| {
diesel::insert_into(posts::table)
.values(&post_value)
.execute(conn)
}).await?;
Ok(Created::new("/").body(post))
}
#[get("/")]
async fn list(db: Db) -> Result<Json<Vec<Option<i32>>>> {
let ids: Vec<Option<i32>> = db.run(move |conn| {
posts::table
.select(posts::id)
.load(conn)
}).await?;
Ok(Json(ids))
}
#[get("/<id>")]
async fn read(db: Db, id: i32) -> Option<Json<Post>> {
db.run(move |conn| {
posts::table
.filter(posts::id.eq(id))
.first(conn)
}).await.map(Json).ok()
}
#[delete("/<id>")]
async fn delete(db: Db, id: i32) -> Result<Option<()>> {
let affected = db.run(move |conn| {
diesel::delete(posts::table)
.filter(posts::id.eq(id))
.execute(conn)
}).await?;
Ok((affected == 1).then(|| ()))
}
#[delete("/")]
async fn destroy(db: Db) -> Result<()> {
db.run(move |conn| diesel::delete(posts::table).execute(conn)).await?;
Ok(())
}
async fn run_migrations(rocket: Rocket) -> Rocket {
// This macro from `diesel_migrations` defines an `embedded_migrations`
// module containing a function named `run` that runs the migrations in the
// specified directory, initializing the database.
embed_migrations!("db/diesel/migrations");
let conn = Db::get_one(&rocket).await.expect("database connection");
conn.run(|c| embedded_migrations::run(c)).await.expect("diesel migrations");
rocket
}
pub fn stage() -> AdHoc {
AdHoc::on_launch("Diesel SQLite Stage", |rocket| async {
rocket.attach(Db::fairing())
.attach(AdHoc::on_launch("Diesel Migrations", run_migrations))
.mount("/diesel", routes![list, read, create, delete, destroy])
})
}

View File

@ -0,0 +1,18 @@
#[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate diesel_migrations;
#[macro_use] extern crate diesel;
#[cfg(test)] mod tests;
mod sqlx;
mod diesel_sqlite;
mod rusqlite;
#[launch]
fn rocket() -> _ {
rocket::ignite()
.attach(sqlx::stage())
.attach(rusqlite::stage())
.attach(diesel_sqlite::stage())
}

View File

@ -0,0 +1,94 @@
use rocket::Rocket;
use rocket::fairing::AdHoc;
use rocket_contrib::databases::rusqlite;
use rocket::response::{Debug, status::Created};
use rocket_contrib::json::Json;
use self::rusqlite::params;
use serde::{Serialize, Deserialize};
#[database("rusqlite")]
struct Db(rusqlite::Connection);
#[derive(Debug, Clone, Deserialize, Serialize)]
struct Post {
#[serde(skip_deserializing, skip_serializing_if = "Option::is_none")]
id: Option<i64>,
title: String,
text: String,
}
type Result<T, E = Debug<rusqlite::Error>> = std::result::Result<T, E>;
#[post("/", data = "<post>")]
async fn create(db: Db, post: Json<Post>) -> Result<Created<Json<Post>>> {
let qpost = post.clone();
db.run(move |conn| {
conn.execute("INSERT INTO posts (title, text) VALUES (?1, ?2)",
params![qpost.title, qpost.text])
}).await?;
Ok(Created::new("/").body(Json(post.into_inner())))
}
#[get("/")]
async fn list(db: Db) -> Result<Json<Vec<i64>>> {
let ids = db.run(|conn| {
conn.prepare("SELECT id FROM posts")?
.query_map(params![], |row| row.get(0))?
.collect::<Result<Vec<i64>, _>>()
}).await?;
Ok(Json(ids))
}
#[get("/<id>")]
async fn read(db: Db, id: i64) -> Option<Json<Post>> {
let post = db.run(move |conn| {
conn.query_row("SELECT id, title, text FROM posts WHERE id = ?1", params![id],
|r| Ok(Post { id: Some(r.get(0)?), title: r.get(1)?, text: r.get(2)? }))
}).await.ok()?;
Some(Json(post))
}
#[delete("/<id>")]
async fn delete(db: Db, id: i64) -> Result<Option<()>> {
let affected = db.run(move |conn| {
conn.execute("DELETE FROM posts WHERE id = ?1", params![id])
}).await?;
Ok((affected == 1).then(|| ()))
}
#[delete("/")]
async fn destroy(db: Db) -> Result<()> {
db.run(move |conn| conn.execute("DELETE FROM posts", params![])).await?;
Ok(())
}
async fn init_db(rocket: Rocket) -> Rocket {
Db::get_one(&rocket).await
.expect("database mounted")
.run(|conn| {
conn.execute(r#"
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR NOT NULL,
text VARCHAR NOT NULL,
published BOOLEAN NOT NULL DEFAULT 0
)"#, params![])
}).await
.expect("can init rusqlite DB");
rocket
}
pub fn stage() -> AdHoc {
AdHoc::on_launch("Rusqlite Stage", |rocket| async {
rocket.attach(Db::fairing())
.attach(AdHoc::on_launch("Rusqlite Init", init_db))
.mount("/rusqlite", routes![list, create, read, delete, destroy])
})
}

View File

@ -0,0 +1,107 @@
use rocket::{Rocket, State, futures};
use rocket::fairing::AdHoc;
use rocket::response::status::Created;
use rocket_contrib::json::Json;
use futures::stream::TryStreamExt;
use futures::future::TryFutureExt;
use serde::{Serialize, Deserialize};
use sqlx::ConnectOptions;
type Db = sqlx::SqlitePool;
type Result<T, E = rocket::response::Debug<sqlx::Error>> = std::result::Result<T, E>;
#[derive(Debug, Clone, Deserialize, Serialize)]
struct Post {
#[serde(skip_deserializing, skip_serializing_if = "Option::is_none")]
id: Option<i64>,
title: String,
text: String,
}
#[post("/", data = "<post>")]
async fn create(db: State<'_, Db>, post: Json<Post>) -> Result<Created<Json<Post>>> {
// There is no support for `RETURNING`.
sqlx::query!("INSERT INTO posts (title, text) VALUES (?, ?)", post.title, post.text)
.execute(&*db)
.await?;
Ok(Created::new("/").body(post))
}
#[get("/")]
async fn list(db: State<'_, Db>) -> Result<Json<Vec<i64>>> {
let ids = sqlx::query!("SELECT id FROM posts")
.fetch(&*db)
.map_ok(|record| record.id)
.try_collect::<Vec<_>>()
.await?;
Ok(Json(ids))
}
#[get("/<id>")]
async fn read(db: State<'_, Db>, id: i64) -> Option<Json<Post>> {
sqlx::query!("SELECT id, title, text FROM posts WHERE id = ?", id)
.fetch_one(&*db)
.map_ok(|r| Json(Post { id: Some(r.id), title: r.title, text: r.text }))
.await
.ok()
}
#[delete("/<id>")]
async fn delete(db: State<'_, Db>, id: i64) -> Result<Option<()>> {
let result = sqlx::query!("DELETE FROM posts WHERE id = ?", id)
.execute(&*db)
.await?;
Ok((result.rows_affected() == 1).then(|| ()))
}
#[delete("/")]
async fn destroy(db: State<'_, Db>) -> Result<()> {
sqlx::query!("DELETE FROM posts").execute(&*db).await?;
Ok(())
}
async fn init_db(rocket: Rocket) -> Result<Rocket, Rocket> {
use rocket_contrib::databases::Config;
let config = match Config::from("sqlx", &rocket) {
Ok(config) => config,
Err(e) => {
error!("Failed to read SQLx config: {}", e);
return Err(rocket);
}
};
let mut opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(&config.url)
.create_if_missing(true);
opts.disable_statement_logging();
let db = match Db::connect_with(opts).await {
Ok(db) => db,
Err(e) => {
error!("Failed to connect to SQLx database: {}", e);
return Err(rocket);
}
};
if let Err(e) = sqlx::migrate!("db/sqlx/migrations").run(&db).await {
error!("Failed to initialize SQLx database: {}", e);
return Err(rocket);
}
Ok(rocket.manage(db))
}
pub fn stage() -> AdHoc {
AdHoc::on_launch("SQLx Stage", |rocket| async {
rocket
.attach(AdHoc::try_on_launch("SQLx Database", init_db))
.mount("/sqlx", routes![list, create, read, delete, destroy])
})
}

View File

@ -0,0 +1,99 @@
use rocket::fairing::AdHoc;
use rocket::local::blocking::{Client, LocalResponse, LocalRequest};
use rocket::http::{Status, ContentType};
use serde::{Serialize, Deserialize};
// Make it easier to work with JSON.
trait LocalResponseExt {
fn into_json<T: serde::de::DeserializeOwned>(self) -> Option<T>;
}
trait LocalRequestExt {
fn json<T: serde::Serialize>(self, value: &T) -> Self;
}
impl LocalResponseExt for LocalResponse<'_> {
fn into_json<T: serde::de::DeserializeOwned>(self) -> Option<T> {
serde_json::from_reader(self).ok()
}
}
impl LocalRequestExt for LocalRequest<'_> {
fn json<T: serde::Serialize>(self, value: &T) -> Self {
let json = serde_json::to_string(value).expect("JSON serialization");
self.header(ContentType::JSON).body(json)
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
struct Post {
title: String,
text: String,
}
fn test(base: &str, stage: AdHoc) {
// Number of posts we're going to create/read/delete.
const N: usize = 20;
// NOTE: If we had more than one test running concurently that dispatches
// DB-accessing requests, we'd need transactions or to serialize all tests.
let client = Client::tracked(rocket::ignite().attach(stage)).unwrap();
// Clear everything from the database.
assert_eq!(client.delete(base).dispatch().status(), Status::Ok);
assert_eq!(client.get(base).dispatch().into_json::<Vec<i64>>(), Some(vec![]));
// Add some random posts, ensure they're listable and readable.
for i in 1..=N{
let title = format!("My Post - {}", i);
let text = format!("Once upon a time, at {}'o clock...", i);
let post = Post { title: title.clone(), text: text.clone() };
// Create a new post.
let response = client.post(base).json(&post).dispatch().into_json::<Post>();
assert_eq!(response.unwrap(), post);
// Ensure the index shows one more post.
let list = client.get(base).dispatch().into_json::<Vec<i64>>().unwrap();
assert_eq!(list.len(), i);
// The last in the index is the new one; ensure contents match.
let last = list.last().unwrap();
let response = client.get(format!("{}/{}", base, last)).dispatch();
assert_eq!(response.into_json::<Post>().unwrap(), post);
}
// Now delete all of the posts.
for _ in 1..=N {
// Get a valid ID from the index.
let list = client.get(base).dispatch().into_json::<Vec<i64>>().unwrap();
let id = list.get(0).expect("have post");
// Delete that post.
let response = client.delete(format!("{}/{}", base, id)).dispatch();
assert_eq!(response.status(), Status::Ok);
}
// Ensure they're all gone.
let list = client.get(base).dispatch().into_json::<Vec<i64>>().unwrap();
assert!(list.is_empty());
// Trying to delete should now 404.
let response = client.delete(format!("{}/{}", base, 1)).dispatch();
assert_eq!(response.status(), Status::NotFound);
}
#[test]
fn test_sqlx() {
test("/sqlx", crate::sqlx::stage())
}
#[test]
fn test_diesel() {
test("/diesel", crate::diesel_sqlite::stage())
}
#[test]
fn test_rusqlite() {
test("/rusqlite", crate::rusqlite::stage())
}

View File

@ -1,5 +1,5 @@
[package] [package]
name = "hello_world" name = "error-handling"
version = "0.0.0" version = "0.0.0"
workspace = "../" workspace = "../"
edition = "2018" edition = "2018"

View File

@ -58,11 +58,11 @@ fn token(token: State<'_, Token>) -> String {
} }
#[launch] #[launch]
fn rocket() -> rocket::Rocket { fn rocket() -> _ {
rocket::ignite() rocket::ignite()
.mount("/", routes![hello, token]) .mount("/", routes![hello, token])
.attach(Counter::default()) .attach(Counter::default())
.attach(AdHoc::on_launch("Token State", |rocket| async { .attach(AdHoc::try_on_launch("Token State", |rocket| async {
println!("Adding token managed state..."); println!("Adding token managed state...");
match rocket.figment().extract_inner("token") { match rocket.figment().extract_inner("token") {
Ok(value) => Ok(rocket.manage(Token(value))), Ok(value) => Ok(rocket.manage(Token(value))),

View File

@ -81,7 +81,7 @@ fn submit<'r>(form: Form<Contextual<'r, Submit<'r>>>) -> (Status, Template) {
} }
#[launch] #[launch]
fn rocket() -> rocket::Rocket { fn rocket() -> _ {
rocket::ignite() rocket::ignite()
.mount("/", routes![index, submit]) .mount("/", routes![index, submit])
.attach(Template::fairing()) .attach(Template::fairing())

View File

@ -1,16 +0,0 @@
[package]
name = "handlebars_templates"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies.rocket_contrib]
path = "../../contrib/lib"
default-features = false
features = ["handlebars_templates"]

View File

@ -1,76 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use rocket::Request;
use rocket::response::Redirect;
use rocket_contrib::templates::{Template, handlebars};
#[derive(serde::Serialize)]
struct TemplateContext {
title: &'static str,
name: Option<String>,
items: Vec<&'static str>,
// This key tells handlebars which template is the parent.
parent: &'static str,
}
#[get("/")]
fn index() -> Redirect {
Redirect::to("/hello/Unknown")
}
#[get("/hello/<name>")]
fn hello(name: String) -> Template {
Template::render("index", &TemplateContext {
title: "Hello",
name: Some(name),
items: vec!["One", "Two", "Three"],
parent: "layout",
})
}
#[get("/about")]
fn about() -> Template {
Template::render("about", &TemplateContext {
title: "About",
name: None,
items: vec!["Four", "Five", "Six"],
parent: "layout",
})
}
#[catch(404)]
fn not_found(req: &Request<'_>) -> Template {
let mut map = std::collections::HashMap::new();
map.insert("path", req.uri().path());
Template::render("error/404", &map)
}
use self::handlebars::{Helper, Handlebars, Context, RenderContext, Output, HelperResult, JsonRender};
fn wow_helper(
h: &Helper<'_, '_>,
_: &Handlebars,
_: &Context,
_: &mut RenderContext<'_, '_>,
out: &mut dyn Output
) -> HelperResult {
if let Some(param) = h.param(0) {
out.write("<b><i>")?;
out.write(&param.value().render())?;
out.write("</b></i>")?;
}
Ok(())
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![index, hello, about])
.register("/", catchers![not_found])
.attach(Template::custom(|engines| {
engines.handlebars.register_helper("wow", Box::new(wow_helper));
}))
}

View File

@ -1,70 +0,0 @@
use super::{rocket, TemplateContext};
use rocket::local::blocking::Client;
use rocket::http::Method::*;
use rocket::http::Status;
use rocket_contrib::templates::Template;
macro_rules! dispatch {
($method:expr, $path:expr, |$client:ident, $response:ident| $body:expr) => ({
let $client = Client::tracked(rocket()).unwrap();
let $response = $client.req($method, $path).dispatch();
$body
})
}
#[test]
fn test_root() {
// Check that the redirect works.
for method in &[Get, Head] {
dispatch!(*method, "/", |client, response| {
assert_eq!(response.status(), Status::SeeOther);
assert!(response.body().is_none());
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] {
dispatch!(*method, "/", |client, response| {
let mut map = std::collections::HashMap::new();
map.insert("path", "/");
let expected = Template::show(client.rocket(), "error/404", &map).unwrap();
assert_eq!(response.status(), Status::NotFound);
assert_eq!(response.into_string(), Some(expected));
});
}
}
#[test]
fn test_name() {
// Check that the /hello/<name> route works.
dispatch!(Get, "/hello/Jack%20Daniels", |client, response| {
let context = TemplateContext {
title: "Hello",
name: Some("Jack Daniels".into()),
items: vec!["One", "Two", "Three"],
parent: "layout",
};
let expected = Template::show(client.rocket(), "index", &context).unwrap();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some(expected));
});
}
#[test]
fn test_404() {
// Check that the error catcher works.
dispatch!(Get, "/hello/", |client, response| {
let mut map = std::collections::HashMap::new();
map.insert("path", "/hello/");
let expected = Template::show(client.rocket(), "error/404", &map).unwrap();
assert_eq!(response.status(), Status::NotFound);
assert_eq!(response.into_string(), Some(expected));
});
}

View File

@ -1,3 +0,0 @@
<footer>
<p>This is a footer partial.</p>
</footer>

View File

@ -1 +0,0 @@
<a href="/hello/Unknown">Hello</a> | <a href="/about">About</a>

View File

@ -1,5 +1,5 @@
[package] [package]
name = "errors" name = "hello"
version = "0.0.0" version = "0.0.0"
workspace = "../" workspace = "../"
edition = "2018" edition = "2018"

View File

@ -0,0 +1,64 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
#[get("/world")]
fn world() -> &'static str {
"Hello, world!"
}
#[get("/мир")]
fn mir() -> &'static str {
"Привет, мир!"
}
#[get("/<name>/<age>")]
fn wave(name: &str, age: u8) -> String {
format!("👋 Hello, {} year old named {}!", age, name)
}
#[derive(FromFormField)]
enum Lang {
#[field(value = "en")]
English,
#[field(value = "ru")]
#[field(value = "ру")]
Russian
}
#[derive(FromForm)]
struct Options<'r> {
emoji: bool,
name: Option<&'r str>,
}
// Note: without the `..` in `opt..`, we'd need to pass `opt.emoji`, `opt.name`.
#[get("/?<lang>&<opt..>")]
fn hello(lang: Option<Lang>, opt: Options<'_>) -> String {
let mut greeting = String::new();
if opt.emoji {
greeting.push_str("👋 ");
}
match lang {
Some(Lang::Russian) => greeting.push_str("Привет"),
Some(Lang::English) => greeting.push_str("Hello"),
None => greeting.push_str("Hi"),
}
if let Some(name) = opt.name {
greeting.push_str(", ");
greeting.push_str(name);
}
greeting.push('!');
greeting
}
#[launch]
fn rocket() -> _ {
rocket::ignite()
.mount("/", routes![hello])
.mount("/hello", routes![world, mir])
.mount("/wave", routes![wave])
}

View File

@ -0,0 +1,71 @@
use rocket::local::blocking::Client;
use rocket::http::{RawStr, Status};
#[test]
fn hello() {
let langs = &["", "ru", "%D1%80%D1%83", "en", "unknown"];
let ex_lang = &["Hi", "Привет", "Привет", "Hello", "Hi"];
let emojis = &["", "on", "true", "false", "no", "yes", "off"];
let ex_emoji = &["", "👋 ", "👋 ", "", "", "👋 ", ""];
let names = &["", "Bob", "Bob+Smith"];
let ex_name = &["!", ", Bob!", ", Bob Smith!"];
let client = Client::tracked(super::rocket()).unwrap();
for n in 0..(langs.len() * emojis.len() * names.len()) {
let i = n / (emojis.len() * names.len());
let j = n % (emojis.len() * names.len()) / names.len();
let k = n % (emojis.len() * names.len()) % names.len();
let (lang, ex_lang) = (langs[i], ex_lang[i]);
let (emoji, ex_emoji) = (emojis[j], ex_emoji[j]);
let (name, ex_name) = (names[k], ex_name[k]);
let expected = format!("{}{}{}", ex_emoji, ex_lang, ex_name);
let q = |name, s: &str| match s.is_empty() {
true => "".into(),
false => format!("&{}={}", name, s)
};
let uri = format!("/?{}{}{}", q("lang", lang), q("emoji", emoji), q("name", name));
let response = client.get(uri).dispatch();
assert_eq!(response.into_string().unwrap(), expected);
let uri = format!("/?{}{}{}", q("emoji", emoji), q("name", name), q("lang", lang));
let response = client.get(uri).dispatch();
assert_eq!(response.into_string().unwrap(), expected);
}
}
#[test]
fn hello_world() {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get("/hello/world").dispatch();
assert_eq!(response.into_string(), Some("Hello, world!".into()));
}
#[test]
fn hello_mir() {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get("/hello/%D0%BC%D0%B8%D1%80").dispatch();
assert_eq!(response.into_string(), Some("Привет, мир!".into()));
}
#[test]
fn wave() {
let client = Client::tracked(super::rocket()).unwrap();
for &(name, age) in &[("Bob%20Smith", 22), ("Michael", 80), ("A", 0), ("a", 127)] {
let uri = format!("/wave/{}/{}", name, age);
let real_name = RawStr::new(name).percent_decode_lossy();
let expected = format!("👋 Hello, {} year old named {}!", age, real_name);
let response = client.get(uri).dispatch();
assert_eq!(response.into_string().unwrap(), expected);
for bad_age in &["1000", "-1", "bird", "?"] {
let bad_uri = format!("/wave/{}/{}", name, bad_age);
let response = client.get(bad_uri).dispatch();
assert_eq!(response.status(), Status::NotFound);
}
}
}

View File

@ -1,9 +0,0 @@
[package]
name = "hello_2018"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }

View File

@ -1,19 +0,0 @@
#![warn(rust_2018_idioms)]
#[cfg(test)] mod tests;
#[rocket::get("/")]
fn hello() -> &'static str {
"Hello, Rust 2018!"
}
#[rocket::launch]
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", rocket::routes![hello])
.register("/", rocket::catchers![not_found])
}
#[rocket::catch(404)]
fn not_found(_req: &'_ rocket::Request<'_>) -> &'static str {
"404 Not Found"
}

View File

@ -1,49 +0,0 @@
use rocket::{self, local::blocking::Client};
#[test]
fn hello_world() {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get("/").dispatch();
assert_eq!(response.into_string(), Some("Hello, Rust 2018!".into()));
}
// Tests unrelated to the example.
mod scoped_uri_tests {
use rocket::{get, routes};
mod inner {
use rocket::uri;
#[rocket::get("/")]
pub fn hello() -> String {
format!("Hello! Try {}.", uri!(super::hello_name: "Rust 2018"))
}
}
#[get("/<name>")]
fn hello_name(name: String) -> String {
format!("Hello, {}! This is {}.", name, rocket::uri!(hello_name: &name))
}
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![hello_name])
.mount("/", rocket::routes![inner::hello])
}
use rocket::local::blocking::Client;
#[test]
fn test_inner_hello() {
let client = Client::tracked(rocket()).unwrap();
let response = client.get("/").dispatch();
assert_eq!(response.into_string(), Some("Hello! Try /Rust%202018.".into()));
}
#[test]
fn test_hello_name() {
let client = Client::tracked(rocket()).unwrap();
let response = client.get("/Rust%202018").dispatch();
assert_eq!(response.into_string().unwrap(), "Hello, Rust 2018! This is /Rust%202018.");
}
}

View File

@ -1,18 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
#[get("/hello/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
format!("Hello, {} year old named {}!", age, name)
}
#[get("/hello/<name>")]
fn hi(name: &str) -> &str {
name
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![hello, hi])
}

View File

@ -1,34 +0,0 @@
use rocket::local::blocking::Client;
use rocket::http::Status;
fn test(uri: String, expected: String) {
let client = Client::tracked(super::rocket()).unwrap();
assert_eq!(client.get(&uri).dispatch().into_string(), Some(expected));
}
fn test_404(uri: &'static str) {
let client = Client::tracked(super::rocket()).unwrap();
assert_eq!(client.get(uri).dispatch().status(), Status::NotFound);
}
#[test]
fn test_hello() {
for &(name, age) in &[("Mike", 22), ("Michael", 80), ("A", 0), ("a", 127)] {
test(format!("/hello/{}/{}", name, age),
format!("Hello, {} year old named {}!", age, name));
}
}
#[test]
fn test_failing_hello() {
test_404("/hello/Mike/1000");
test_404("/hello/Mike/-129");
test_404("/hello/Mike/-1");
}
#[test]
fn test_hi() {
for name in &["Mike", "A", "123", "hi", "c"] {
test(format!("/hello/{}", name), name.to_string());
}
}

View File

@ -1,29 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
#[get("/?<lang>")]
fn hello(lang: Option<&str>) -> &'static str {
match lang {
Some("en") | None => world(),
Some("русский") => mir(),
_ => "Hello, voyager!"
}
}
#[get("/world")]
fn world() -> &'static str {
"Hello, world!"
}
#[get("/мир")]
fn mir() -> &'static str {
"Привет, мир!"
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![hello])
.mount("/hello", routes![world, mir])
}

View File

@ -1,18 +0,0 @@
use rocket::local::blocking::Client;
#[test]
fn hello_world() {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get("/").dispatch();
assert_eq!(response.into_string(), Some("Hello, world!".into()));
let response = client.get("/hello/world").dispatch();
assert_eq!(response.into_string(), Some("Hello, world!".into()));
}
#[test]
fn hello_mir() {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get("/hello/%D0%BC%D0%B8%D1%80").dispatch();
assert_eq!(response.into_string(), Some("Привет, мир!".into()));
}

View File

@ -1,16 +0,0 @@
[package]
name = "json"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies.rocket_contrib]
path = "../../contrib/lib"
default-features = false
features = ["json"]

View File

@ -1,80 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use std::collections::HashMap;
use std::borrow::Cow;
use rocket::State;
use rocket::tokio::sync::Mutex;
use rocket_contrib::json::{Json, JsonValue, json};
use serde::{Serialize, Deserialize};
// The type to represent the ID of a message.
type Id = usize;
// We're going to store all of the messages here. No need for a DB.
type MessageMap<'r> = State<'r, Mutex<HashMap<Id, String>>>;
#[derive(Serialize, Deserialize)]
struct Message<'r> {
id: Option<Id>,
contents: Cow<'r, str>
}
#[post("/<id>", format = "json", data = "<message>")]
async fn new(id: Id, message: Json<Message<'_>>, map: MessageMap<'_>) -> JsonValue {
let mut hashmap = map.lock().await;
if hashmap.contains_key(&id) {
json!({
"status": "error",
"reason": "ID exists. Try put."
})
} else {
hashmap.insert(id, message.contents.to_string());
json!({ "status": "ok" })
}
}
#[put("/<id>", format = "json", data = "<message>")]
async fn update(id: Id, message: Json<Message<'_>>, map: MessageMap<'_>) -> Option<JsonValue> {
let mut hashmap = map.lock().await;
if hashmap.contains_key(&id) {
hashmap.insert(id, message.contents.to_string());
Some(json!({ "status": "ok" }))
} else {
None
}
}
#[get("/<id>", format = "json")]
async fn get<'r>(id: Id, map: MessageMap<'r>) -> Option<Json<Message<'r>>> {
let hashmap = map.lock().await;
let contents = hashmap.get(&id)?.clone();
Some(Json(Message {
id: Some(id),
contents: contents.into()
}))
}
#[get("/echo", data = "<msg>")]
fn echo<'r>(msg: Json<Message<'r>>) -> Cow<'r, str> {
msg.into_inner().contents
}
#[catch(404)]
fn not_found() -> JsonValue {
json!({
"status": "error",
"reason": "Resource was not found."
})
}
#[launch]
fn rocket() -> _ {
rocket::ignite()
.mount("/message", routes![new, update, get, echo])
.register("/", catchers![not_found])
.manage(Mutex::new(HashMap::<Id, String>::new()))
}

View File

@ -1,71 +0,0 @@
use crate::rocket;
use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType};
#[test]
fn bad_get_put() {
let client = Client::tracked(rocket()).unwrap();
// Try to get a message with an ID that doesn't exist.
let res = client.get("/message/99").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::NotFound);
let body = res.into_string().unwrap();
assert!(body.contains("error"));
assert!(body.contains("Resource was not found."));
// Try to get a message with an invalid ID.
let res = client.get("/message/hi").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::NotFound);
assert!(res.into_string().unwrap().contains("error"));
// Try to put a message without a proper body.
let res = client.put("/message/80").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::BadRequest);
// Try to put a message for an ID that doesn't exist.
let res = client.put("/message/80")
.header(ContentType::JSON)
.body(r#"{ "contents": "Bye bye, world!" }"#)
.dispatch();
assert_eq!(res.status(), Status::NotFound);
}
#[test]
fn post_get_put_get() {
let client = Client::tracked(rocket()).unwrap();
// Check that a message with ID 1 doesn't exist.
let res = client.get("/message/1").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::NotFound);
// Add a new message with ID 1.
let res = client.post("/message/1")
.header(ContentType::JSON)
.body(r#"{ "contents": "Hello, world!" }"#)
.dispatch();
assert_eq!(res.status(), Status::Ok);
// Check that the message exists with the correct contents.
let res = client.get("/message/1").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::Ok);
let body = res.into_string().unwrap();
assert!(body.contains("Hello, world!"));
// Change the message contents.
let res = client.put("/message/1")
.header(ContentType::JSON)
.body(r#"{ "contents": "Bye bye, world!" }"#)
.dispatch();
assert_eq!(res.status(), Status::Ok);
// Check that the message exists with the updated contents.
let res = client.get("/message/1").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::Ok);
let body = res.into_string().unwrap();
assert!(!body.contains("Hello, world!"));
assert!(body.contains("Bye bye, world!"));
}

View File

@ -1,10 +0,0 @@
[package]
name = "managed_queue"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
crossbeam = "0.7"
rocket = { path = "../../core/lib" }

View File

@ -1,25 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use rocket::State;
use crossbeam::queue::SegQueue;
struct LogChannel(SegQueue<String>);
#[put("/push?<event>")]
fn push(event: String, queue: State<'_, LogChannel>) {
queue.0.push(event);
}
#[get("/pop")]
fn pop(queue: State<'_, LogChannel>) -> Option<String> {
queue.0.pop().ok()
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite()
.mount("/", routes![push, pop])
.manage(LogChannel(SegQueue::new()))
}

View File

@ -1,13 +0,0 @@
use rocket::local::blocking::Client;
use rocket::http::Status;
#[test]
fn test_push_pop() {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.put("/push?event=test1").dispatch();
assert_eq!(response.status(), Status::Ok);
let response = client.get("/pop").dispatch();
assert_eq!(response.into_string(), Some("test1".to_string()));
}

View File

@ -93,7 +93,7 @@ impl Handler for CustomHandler {
} }
#[rocket::launch] #[rocket::launch]
fn rocket() -> rocket::Rocket { fn rocket() -> _ {
let always_forward = Route::ranked(1, Get, "/", forward); let always_forward = Route::ranked(1, Get, "/", forward);
let hello = Route::ranked(2, Get, "/", hi); let hello = Route::ranked(2, Get, "/", hi);

View File

@ -1,36 +0,0 @@
use crate::rocket;
use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Message {
id: usize,
contents: String
}
#[test]
fn msgpack_get() {
let client = Client::tracked(rocket()).unwrap();
let res = client.get("/message/1").header(ContentType::MsgPack).dispatch();
assert_eq!(res.status(), Status::Ok);
assert_eq!(res.content_type(), Some(ContentType::MsgPack));
// Check that the message is `[1, "Hello, world!"]`
assert_eq!(&res.into_bytes().unwrap(),
&[146, 1, 173, 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]);
}
#[test]
fn msgpack_post() {
// Dispatch request with a message of `[2, "Goodbye, world!"]`.
let client = Client::tracked(rocket()).unwrap();
let res = client.post("/message")
.header(ContentType::MsgPack)
.body(&[146, 2, 175, 71, 111, 111, 100, 98, 121, 101, 44, 32, 119, 111, 114, 108, 100, 33])
.dispatch();
assert_eq!(res.status(), Status::Ok);
assert_eq!(res.into_string(), Some("Goodbye, world!".into()));
}

View File

@ -1,9 +0,0 @@
[package]
name = "optional_redirect"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }

View File

@ -1,29 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)]
mod tests;
use rocket::response::Redirect;
#[get("/")]
fn root() -> Redirect {
Redirect::to("/users/login")
}
#[get("/users/<name>")]
fn user(name: &str) -> Result<&'static str, Redirect> {
match name {
"Sergio" => Ok("Hello, Sergio!"),
_ => Err(Redirect::to("/users/login")),
}
}
#[get("/users/login")]
fn login() -> &'static str {
"Hi! That user doesn't exist. Maybe you need to log in?"
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![root, user, login])
}

View File

@ -1,30 +0,0 @@
use rocket::local::blocking::Client;
use rocket::http::Status;
fn test_200(uri: &str, expected_body: &str) {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some(expected_body.to_string()));
}
fn test_303(uri: &str, expected_location: &str) {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get(uri).dispatch();
let location_headers: Vec<_> = response.headers().get("Location").collect();
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(location_headers, vec![expected_location]);
}
#[test]
fn test() {
test_200("/users/Sergio", "Hello, Sergio!");
test_200("/users/login",
"Hi! That user doesn't exist. Maybe you need to log in?");
}
#[test]
fn test_redirects() {
test_303("/", "/users/login");
test_303("/users/unknown", "/users/login");
}

View File

@ -9,7 +9,7 @@ use rocket::State;
use rocket::data::{Data, ToByteUnit}; use rocket::data::{Data, ToByteUnit};
use rocket::http::uri::Absolute; use rocket::http::uri::Absolute;
use rocket::response::content::Plain; use rocket::response::content::Plain;
use rocket::tokio::fs::File; use rocket::tokio::fs::{self, File};
use crate::paste_id::PasteId; use crate::paste_id::PasteId;
@ -31,6 +31,11 @@ async fn retrieve(id: PasteId<'_>) -> Option<Plain<File>> {
File::open(id.file_path()).await.map(Plain).ok() File::open(id.file_path()).await.map(Plain).ok()
} }
#[delete("/<id>")]
async fn delete(id: PasteId<'_>) -> Option<()> {
fs::remove_file(id.file_path()).await.ok()
}
#[get("/")] #[get("/")]
fn index() -> &'static str { fn index() -> &'static str {
" "
@ -50,8 +55,8 @@ fn index() -> &'static str {
} }
#[launch] #[launch]
fn rocket() -> rocket::Rocket { fn rocket() -> _ {
rocket::ignite() rocket::ignite()
.manage(Absolute::parse(HOST).expect("valid host")) .manage(Absolute::parse(HOST).expect("valid host"))
.mount("/", routes![index, upload, retrieve]) .mount("/", routes![index, upload, delete, retrieve])
} }

View File

@ -1,6 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use rocket::http::uri::{self, FromUriParam};
use rocket::request::FromParam; use rocket::request::FromParam;
use rand::{self, Rng}; use rand::{self, Rng};
@ -44,3 +45,11 @@ impl<'a> FromParam<'a> for PasteId<'a> {
} }
} }
} }
impl<'a> FromUriParam<uri::Path, &'a str> for PasteId<'_> {
type Target = PasteId<'a>;
fn from_uri_param(param: &'a str) -> Self::Target {
PasteId(param.into())
}
}

View File

@ -1,4 +1,4 @@
use super::{rocket, index}; use super::{rocket, index, PasteId};
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType}; use rocket::http::{Status, ContentType};
@ -11,23 +11,31 @@ fn check_index() {
let client = Client::tracked(rocket()).unwrap(); let client = Client::tracked(rocket()).unwrap();
// Ensure the index returns what we expect. // Ensure the index returns what we expect.
let response = client.get("/").dispatch(); let response = client.get(uri!(super::index)).dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Plain)); assert_eq!(response.content_type(), Some(ContentType::Plain));
assert_eq!(response.into_string(), Some(index().into())) assert_eq!(response.into_string(), Some(index().into()))
} }
fn upload_paste(client: &Client, body: &str) -> String { fn upload_paste(client: &Client, body: &str) -> String {
let response = client.post("/").body(body).dispatch(); let response = client.post(uri!(super::upload)).body(body).dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Plain)); assert_eq!(response.content_type(), Some(ContentType::Plain));
extract_id(&response.into_string().unwrap()).unwrap() extract_id(&response.into_string().unwrap()).unwrap()
} }
fn download_paste(client: &Client, id: &str) -> String { fn download_paste(client: &Client, id: &str) -> Option<String> {
let response = client.get(format!("/{}", id)).dispatch(); let response = client.get(uri!(super::retrieve: id)).dispatch();
if response.status().class().is_success() {
Some(response.into_string().unwrap())
} else {
None
}
}
fn delete_paste(client: &Client, id: &str) {
let response = client.delete(uri!(super::delete: id)).dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
response.into_string().unwrap()
} }
#[test] #[test]
@ -37,23 +45,23 @@ fn pasting() {
// Do a trivial upload, just to make sure it works. // Do a trivial upload, just to make sure it works.
let body_1 = "Hello, world!"; let body_1 = "Hello, world!";
let id_1 = upload_paste(&client, body_1); let id_1 = upload_paste(&client, body_1);
assert_eq!(download_paste(&client, &id_1), body_1); assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
// Make sure we can keep getting that paste. // Make sure we can keep getting that paste.
assert_eq!(download_paste(&client, &id_1), body_1); assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
assert_eq!(download_paste(&client, &id_1), body_1); assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
assert_eq!(download_paste(&client, &id_1), body_1); assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
// Upload some unicode. // Upload some unicode.
let body_2 = "こんにちは"; let body_2 = "こんにちは";
let id_2 = upload_paste(&client, body_2); let id_2 = upload_paste(&client, body_2);
assert_eq!(download_paste(&client, &id_2), body_2); assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
// Make sure we can get both pastes. // Make sure we can get both pastes.
assert_eq!(download_paste(&client, &id_1), body_1); assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
assert_eq!(download_paste(&client, &id_2), body_2); assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
assert_eq!(download_paste(&client, &id_1), body_1); assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
assert_eq!(download_paste(&client, &id_2), body_2); assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
// Now a longer upload. // Now a longer upload.
let body_3 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed let body_3 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed
@ -63,8 +71,19 @@ fn pasting() {
in voluptate velit esse cillum dolore eu fugiat nulla pariatur. in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum."; officia deserunt mollit anim id est laborum.";
let id_3 = upload_paste(&client, body_3); let id_3 = upload_paste(&client, body_3);
assert_eq!(download_paste(&client, &id_3), body_3); assert_eq!(download_paste(&client, &id_3).unwrap(), body_3);
assert_eq!(download_paste(&client, &id_1), body_1); assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
assert_eq!(download_paste(&client, &id_2), body_2); assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
// Delete everything we uploaded.
delete_paste(&client, &id_1);
assert!(download_paste(&client, &id_1).is_none());
delete_paste(&client, &id_2);
assert!(download_paste(&client, &id_2).is_none());
delete_paste(&client, &id_3);
assert!(download_paste(&client, &id_3).is_none());
} }

View File

@ -1,9 +0,0 @@
[package]
name = "query_params"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }

View File

@ -1,36 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use rocket::form::Strict;
#[derive(FromForm)]
struct Person {
/// Use the `form` attribute to expect an invalid Rust identifier in the HTTP form.
#[field(name = "first-name")]
name: String,
age: Option<u8>
}
#[get("/hello?<person..>")]
fn hello(person: Option<Strict<Person>>) -> String {
if let Some(person) = person {
if let Some(age) = person.age {
format!("Hello, {} year old named {}!", age, person.name)
} else {
format!("Hello {}!", person.name)
}
} else {
"We're gonna need a name, and only a name.".into()
}
}
#[get("/hello?age=20&<person..>")]
fn hello_20(person: Person) -> String {
format!("20 years old? Hi, {}!", person.name)
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![hello, hello_20])
}

View File

@ -1,78 +0,0 @@
use super::rocket;
use rocket::local::blocking::Client;
use rocket::http::Status;
macro_rules! run_test {
($query:expr, |$response:ident| $body:expr) => ({
let client = Client::tracked(rocket()).unwrap();
#[allow(unused_mut)]
let mut $response = client.get(format!("/hello{}", $query)).dispatch();
$body
})
}
#[test]
fn age_and_name_params() {
run_test!("?age=10&first-name=john", |response| {
assert_eq!(response.into_string(),
Some("Hello, 10 year old named john!".into()));
});
run_test!("?age=20&first-name=john", |response| {
assert_eq!(response.into_string(),
Some("20 years old? Hi, john!".into()));
});
}
#[test]
fn age_param_only() {
run_test!("?age=10", |response| {
assert_eq!(response.into_string(),
Some("We're gonna need a name, and only a name.".into()));
});
run_test!("?age=20", |response| {
assert_eq!(response.into_string(),
Some("We're gonna need a name, and only a name.".into()));
});
}
#[test]
fn name_param_only() {
run_test!("?first-name=John", |response| {
assert_eq!(response.into_string(), Some("Hello John!".into()));
});
}
#[test]
fn no_params() {
run_test!("", |response| {
assert_eq!(response.into_string(),
Some("We're gonna need a name, and only a name.".into()));
});
run_test!("?", |response| {
assert_eq!(response.into_string(),
Some("We're gonna need a name, and only a name.".into()));
});
}
#[test]
fn extra_params() {
run_test!("?age=20&first-name=Bob&extra", |response| {
assert_eq!(response.into_string(),
Some("20 years old? Hi, Bob!".into()));
});
run_test!("?age=30&first-name=Bob&extra", |response| {
assert_eq!(response.into_string(),
Some("We're gonna need a name, and only a name.".into()));
});
}
#[test]
fn wrong_path() {
run_test!("/other?age=20&first-name=Bob", |response| {
assert_eq!(response.status(), Status::NotFound);
});
}

View File

@ -1,9 +0,0 @@
[package]
name = "ranking"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }

View File

@ -1,18 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
#[get("/hello/<name>/<age>")]
fn hello(name: String, age: i8) -> String {
format!("Hello, {} year old named {}!", age, name)
}
#[get("/hello/<name>/<age>", rank = 2)]
fn hi(name: String, age: &str) -> String {
format!("Hi {}! Your age ({}) is kind of funky.", name, age)
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![hi, hello])
}

View File

@ -1,30 +0,0 @@
use rocket::local::blocking::Client;
fn test(uri: String, expected: String) {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get(&uri).dispatch();
assert_eq!(response.into_string(), Some(expected));
}
#[test]
fn test_hello() {
for &(name, age) in &[("Mike", 22), ("Michael", 80), ("A", 0), ("a", 127)] {
test(format!("/hello/{}/{}", name, age),
format!("Hello, {} year old named {}!", age, name));
}
}
#[test]
fn test_failing_hello_hi() {
// Invalid integers.
for &(name, age) in &[("Mike", 1000), ("Michael", 128), ("A", -800), ("a", -200)] {
test(format!("/hello/{}/{}", name, age),
format!("Hi {}! Your age ({}) is kind of funky.", name, age));
}
// Non-integers.
for &(name, age) in &[("Mike", "!"), ("Michael", "hi"), ("A", "blah"), ("a", "0-1")] {
test(format!("/hello/{}/{}", name, age),
format!("Hi {}! Your age ({}) is kind of funky.", name, age));
}
}

View File

@ -1,10 +0,0 @@
[package]
name = "raw_sqlite"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }
rusqlite = "0.23"

View File

@ -1,45 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use std::sync::Mutex;
use rocket::{Rocket, State, response::Debug};
use rusqlite::{Connection, Error, types::ToSql};
type DbConn = Mutex<Connection>;
fn init_database(conn: &Connection) {
conn.execute("CREATE TABLE entries (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)", &[] as &[&dyn ToSql])
.expect("create entries table");
conn.execute("INSERT INTO entries (id, name) VALUES ($1, $2)",
&[&0 as &dyn ToSql, &"Rocketeer"])
.expect("insert single entry into entries table");
}
#[get("/")]
fn hello(db_conn: State<'_, DbConn>) -> Result<String, Debug<Error>> {
db_conn.lock()
.expect("db connection lock")
.query_row("SELECT name FROM entries WHERE id = 0",
&[] as &[&dyn ToSql], |row| { row.get(0) })
.map_err(Debug)
}
#[launch]
fn rocket() -> Rocket {
// Open a new in-memory SQLite database.
let conn = Connection::open_in_memory().expect("in memory db");
// Initialize the `entries` table in the in-memory database.
init_database(&conn);
// Have Rocket manage the database pool.
rocket::ignite()
.manage(Mutex::new(conn))
.mount("/", routes![hello])
}

View File

@ -1,9 +0,0 @@
use super::rocket;
use rocket::local::blocking::Client;
#[test]
fn hello() {
let client = Client::tracked(rocket()).unwrap();
let response = client.get("/").dispatch();
assert_eq!(response.into_string(), Some("Rocketeer".into()));
}

View File

@ -1,9 +0,0 @@
[package]
name = "raw_upload"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }

View File

@ -1,23 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use std::{io, env};
use rocket::data::{Capped, TempFile};
#[post("/upload", data = "<file>")]
async fn upload(mut file: Capped<TempFile<'_>>) -> io::Result<String> {
file.persist_to(env::temp_dir().join("upload.txt")).await?;
Ok(format!("{} bytes at {}", file.n.written, file.path().unwrap().display()))
}
#[get("/")]
fn index() -> &'static str {
"Upload your text files by POSTing them to /upload.\n\
Try `curl --data-binary @file.txt http://127.0.0.1:8000/upload`."
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![index, upload])
}

View File

@ -1,38 +0,0 @@
use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType};
use std::env;
use std::io::Read;
use std::fs::{self, File};
const UPLOAD_CONTENTS: &str = "Hey! I'm going to be uploaded. :D Yay!";
#[test]
fn test_index() {
let client = Client::tracked(super::rocket()).unwrap();
let res = client.get("/").dispatch();
assert_eq!(res.into_string(), Some(super::index().to_string()));
}
#[test]
fn test_raw_upload() {
// Delete the upload file before we begin.
let upload_file = env::temp_dir().join("upload.txt");
let _ = fs::remove_file(&upload_file);
// Do the upload. Make sure we get the expected results.
let client = Client::tracked(super::rocket()).unwrap();
let res = client.post("/upload")
.header(ContentType::Plain)
.body(UPLOAD_CONTENTS)
.dispatch();
assert_eq!(res.status(), Status::Ok);
assert!(res.into_string().unwrap().contains(&UPLOAD_CONTENTS.len().to_string()));
// Ensure we find the body in the /tmp/upload.txt file.
let mut file_contents = String::new();
let mut file = File::open(&upload_file).expect("open upload.txt file");
file.read_to_string(&mut file_contents).expect("read upload.txt");
assert_eq!(&file_contents, UPLOAD_CONTENTS);
}

View File

@ -1,9 +0,0 @@
[package]
name = "redirect"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }

View File

@ -1,20 +0,0 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use rocket::response::Redirect;
#[get("/")]
fn root() -> Redirect {
Redirect::to(uri!(login))
}
#[get("/login")]
fn login() -> &'static str {
"Hi! Please log in before continuing."
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![root, login])
}

View File

@ -1,30 +0,0 @@
use rocket::local::blocking::Client;
use rocket::http::Status;
fn client() -> Client {
let rocket = rocket::ignite().mount("/", routes![super::root, super::login]);
Client::tracked(rocket).unwrap()
}
#[test]
fn test_root() {
let client = client();
let response = client.get("/").dispatch();
assert!(response.body().is_none());
assert_eq!(response.status(), Status::SeeOther);
for h in response.headers().iter() {
match h.name.as_str() {
"Location" => assert_eq!(h.value, "/login"),
"Content-Length" => assert_eq!(h.value.parse::<i32>().unwrap(), 0),
_ => { /* let these through */ }
}
}
}
#[test]
fn test_login() {
let client = client();
let r = client.get("/login").dispatch();
assert_eq!(r.into_string(), Some("Hi! Please log in before continuing.".into()));
}

View File

@ -1,9 +0,0 @@
[package]
name = "request_guard"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }

View File

@ -1,55 +0,0 @@
#[macro_use] extern crate rocket;
use rocket::request::{self, Request, FromRequest};
use rocket::outcome::Outcome::*;
#[derive(Debug)]
struct HeaderCount(usize);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for HeaderCount {
type Error = std::convert::Infallible;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
Success(HeaderCount(request.headers().len()))
}
}
#[get("/")]
fn header_count(header_count: HeaderCount) -> String {
format!("Your request contained {} headers!", header_count.0)
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![header_count])
}
#[cfg(test)]
mod test {
use rocket::local::blocking::Client;
use rocket::http::Header;
fn test_header_count<'h>(headers: Vec<Header<'static>>) {
let client = Client::tracked(super::rocket()).unwrap();
let mut req = client.get("/");
for header in headers.iter().cloned() {
req.add_header(header);
}
let response = req.dispatch();
let expect = format!("Your request contained {} headers!", headers.len());
assert_eq!(response.into_string(), Some(expect));
}
#[test]
fn test_n_headers() {
for i in 0..50 {
let headers = (0..i)
.map(|n| Header::new(n.to_string(), n.to_string()))
.collect();
test_header_count(headers);
}
}
}

View File

@ -1,9 +0,0 @@
[package]
name = "request_local_state"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }

View File

@ -1,20 +0,0 @@
use std::sync::atomic::Ordering;
use super::{rocket, Atomics};
use rocket::local::blocking::Client;
#[test]
fn test() {
let client = Client::tracked(rocket()).unwrap();
client.get("/sync").dispatch();
let atomics = client.rocket().state::<Atomics>().unwrap();
assert_eq!(atomics.uncached.load(Ordering::Relaxed), 2);
assert_eq!(atomics.cached.load(Ordering::Relaxed), 1);
client.get("/async").dispatch();
let atomics = client.rocket().state::<Atomics>().unwrap();
assert_eq!(atomics.uncached.load(Ordering::Relaxed), 4);
assert_eq!(atomics.cached.load(Ordering::Relaxed), 2);
}

View File

@ -1,5 +1,5 @@
[package] [package]
name = "hello_person" name = "responders"
version = "0.0.0" version = "0.0.0"
workspace = "../" workspace = "../"
edition = "2018" edition = "2018"
@ -7,3 +7,4 @@ publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib" }
parking_lot = "0.11"

View File

@ -0,0 +1,162 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
/***************************** `Stream` Responder *****************************/
use std::{io, env};
use rocket::tokio::fs::{self, File};
use rocket::tokio::io::{repeat, AsyncRead, AsyncReadExt};
use rocket::response::{content, Stream};
use rocket::data::{Capped, TempFile};
// Upload your `big_file.dat` by POSTing it to /upload.
// try `curl --data-binary @file.txt http://127.0.0.1:8000/stream/file`
const FILENAME: &str = "big_file.dat";
#[get("/stream/a")]
fn many_as() -> content::Plain<Stream<impl AsyncRead>> {
content::Plain(Stream::from(repeat('a' as u8).take(25000)))
}
#[get("/stream/file")]
async fn file() -> Option<Stream<File>> {
// NOTE: Rocket _always_ streams data from an `AsyncRead`, even when
// `Stream` isn't used. By using `Stream`, however, the data is sent using
// chunked-encoding in HTTP 1.1. DATA frames are sent in HTTP/2.
File::open(env::temp_dir().join(FILENAME)).await.map(Stream::from).ok()
}
#[post("/stream/file", data = "<file>")]
async fn upload(mut file: Capped<TempFile<'_>>) -> io::Result<String> {
file.persist_to(env::temp_dir().join(FILENAME)).await?;
Ok(format!("{} bytes at {}", file.n.written, file.path().unwrap().display()))
}
#[delete("/stream/file")]
async fn delete() -> Option<()> {
fs::remove_file(env::temp_dir().join(FILENAME)).await.ok()
}
/***************************** `Redirect` Responder ***************************/
use rocket::response::Redirect;
#[get("/redir")]
fn redir_root() -> Redirect {
Redirect::to(uri!(redir_login))
}
#[get("/redir/login")]
fn redir_login() -> &'static str {
"Hi! Please log in before continuing."
}
#[get("/redir/<name>")]
fn maybe_redir(name: &str) -> Result<&'static str, Redirect> {
match name {
"Sergio" => Ok("Hello, Sergio!"),
_ => Err(Redirect::to(uri!(redir_login))),
}
}
/***************************** `content` Responders ***************************/
use rocket::Request;
// NOTE: This example explicitly uses the `Json` type from `response::content`
// for demonstration purposes. In a real application, _always_ prefer to use
// `rocket_contrib::json::Json` instead!
// 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. Because the client can use non-specific media types like
// `*/*` in `Accept`, these first two routes would collide without `rank`.
#[get("/content", format = "xml", rank = 1)]
fn xml() -> content::Xml<&'static str> {
content::Xml("<payload>I'm here</payload>")
}
#[get("/content", format = "json", rank = 2)]
fn json() -> content::Json<&'static str> {
content::Json(r#"{ "payload": "I'm here" }"#)
}
#[catch(404)]
fn not_found(request: &Request<'_>) -> content::Html<String> {
let html = match request.format() {
Some(ref mt) if !(mt.is_xml() || mt.is_html()) => {
format!("<p>'{}' requests are not supported.</p>", mt)
}
_ => format!("<p>Sorry, '{}' is an invalid path! Try \
/hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
request.uri())
};
content::Html(html)
}
/******************************* `Either` Responder ***************************/
use rocket::Either;
use rocket::response::content::{Json, MsgPack};
use rocket::http::uncased::AsUncased;
// NOTE: In a real application, we'd use `Json` and `MsgPack` from
// `rocket_contrib`, which perform automatic serialization of responses and
// automatically set the `Content-Type`.
#[get("/content/<kind>")]
fn json_or_msgpack(kind: &str) -> Either<Json<&'static str>, MsgPack<&'static [u8]>> {
if kind.as_uncased() == "msgpack" {
Either::Right(MsgPack(&[162, 104, 105]))
} else {
Either::Left(Json("\"hi\""))
}
}
/******************************* Custom Responder *****************************/
use std::borrow::Cow;
use rocket::response::NamedFile;
use rocket::response::content::Html;
#[derive(Responder)]
enum StoredData {
File(Option<NamedFile>),
String(Cow<'static, str>),
Bytes(Vec<u8>),
#[response(status = 401)]
NotAuthorized(Html<&'static str>),
}
#[derive(FromFormField, UriDisplayQuery)]
enum Kind {
File,
String,
Bytes
}
#[get("/custom?<kind>")]
async fn custom(kind: Option<Kind>) -> StoredData {
match kind {
Some(Kind::File) => {
let path = env::temp_dir().join(FILENAME);
StoredData::File(NamedFile::open(path).await.ok())
},
Some(Kind::String) => StoredData::String("Hey, I'm some data.".into()),
Some(Kind::Bytes) => StoredData::Bytes(vec![72, 105]),
None => StoredData::NotAuthorized(Html("No no no!"))
}
}
#[launch]
fn rocket() -> _ {
rocket::ignite()
.mount("/", routes![many_as, file, upload, delete])
.mount("/", routes![redir_root, redir_login, maybe_redir])
.mount("/", routes![xml, json, json_or_msgpack])
.mount("/", routes![custom])
.register("/", catchers![not_found])
}

View File

@ -0,0 +1,148 @@
use rocket::local::blocking::Client;
use rocket::http::Status;
// We use a lock to synchronize between tests so FS operations don't race.
static FS_LOCK: parking_lot::Mutex<()> = parking_lot::const_mutex(());
/***************************** `Stream` Responder *****************************/
#[test]
fn test_many_as() {
let client = Client::tracked(super::rocket()).unwrap();
let res = client.get(uri!(super::many_as)).dispatch();
// Check that we have exactly 25,000 'a's.
let bytes = res.into_bytes().unwrap();
assert_eq!(bytes.len(), 25000);
assert!(bytes.iter().all(|b| *b == b'a'));
}
#[test]
fn test_file() {
const CONTENTS: &str = "big_file contents...not so big here";
// Take the lock so we exclusively access the FS.
let _lock = FS_LOCK.lock();
// Create the 'big_file'
let client = Client::tracked(super::rocket()).unwrap();
let response = client.post(uri!(super::upload)).body(CONTENTS).dispatch();
assert_eq!(response.status(), Status::Ok);
assert!(response.into_string().unwrap().contains(&CONTENTS.len().to_string()));
// Get the big file contents, hopefully.
let res = client.get(uri!(super::file)).dispatch();
assert_eq!(res.into_string(), Some(CONTENTS.into()));
// Delete it.
let response = client.delete(uri!(super::delete)).dispatch();
assert_eq!(response.status(), Status::Ok);
}
/***************************** `Redirect` Responder ***************************/
#[test]
fn test_redir_root() {
let client = Client::tracked(super::rocket()).unwrap();
let response = client.get(uri!(super::redir_root)).dispatch();
assert!(response.body().is_none());
assert_eq!(response.status(), Status::SeeOther);
for h in response.headers().iter() {
match h.name.as_str() {
"Location" => assert_eq!(h.value.as_ref(), &uri!(super::redir_login)),
"Content-Length" => assert_eq!(h.value.parse::<i32>().unwrap(), 0),
_ => { /* let these through */ }
}
}
}
#[test]
fn test_login() {
let client = Client::tracked(super::rocket()).unwrap();
let r = client.get(uri!(super::redir_login)).dispatch();
assert_eq!(r.into_string().unwrap(), "Hi! Please log in before continuing.");
for name in &["Bob", "Charley", "Joe Roger"] {
let r = client.get(uri!(super::maybe_redir: name)).dispatch();
assert_eq!(r.status(), Status::SeeOther);
}
let r = client.get(uri!(super::maybe_redir: "Sergio")).dispatch();
assert_eq!(r.status(), Status::Ok);
assert_eq!(r.into_string().unwrap(), "Hello, Sergio!");
}
/***************************** `content` Responders ***************************/
use rocket::http::{Accept, ContentType};
#[test]
fn test_xml() {
let client = Client::tracked(super::rocket()).unwrap();
let r = client.get(uri!(super::xml)).header(Accept::XML).dispatch();
assert_eq!(r.content_type().unwrap(), ContentType::XML);
assert_eq!(r.into_string().unwrap(), "<payload>I'm here</payload>");
// Purposefully use the "xml" URL to illustrate `format` handling.
let r = client.get(uri!(super::xml)).header(Accept::JSON).dispatch();
assert_eq!(r.content_type().unwrap(), ContentType::JSON);
assert_eq!(r.into_string().unwrap(), r#"{ "payload": "I'm here" }"#);
let r = client.get(uri!(super::xml)).header(Accept::CSV).dispatch();
assert_eq!(r.status(), Status::NotFound);
assert!(r.into_string().unwrap().contains("not supported"));
let r = client.get("/content/i/dont/exist").header(Accept::HTML).dispatch();
assert_eq!(r.content_type().unwrap(), ContentType::HTML);
assert!(r.into_string().unwrap().contains("invalid path"));
}
/******************************* `Either` Responder ***************************/
#[test]
fn test_either() {
let client = Client::tracked(super::rocket()).unwrap();
let r = client.get(uri!(super::json_or_msgpack: "json")).dispatch();
assert_eq!(r.content_type().unwrap(), ContentType::JSON);
assert_eq!(r.into_string().unwrap(), "\"hi\"");
let r = client.get(uri!(super::json_or_msgpack: "msgpack")).dispatch();
assert_eq!(r.content_type().unwrap(), ContentType::MsgPack);
assert_eq!(r.into_bytes().unwrap(), &[162, 104, 105]);
}
/******************************** Custom Responder ****************************/
use super::Kind;
#[test]
fn test_custom() {
let client = Client::tracked(super::rocket()).unwrap();
let r = client.get(uri!(super::custom: Some(Kind::String))).dispatch();
assert_eq!(r.into_string().unwrap(), "Hey, I'm some data.");
let r = client.get(uri!(super::custom: Some(Kind::Bytes))).dispatch();
assert_eq!(r.into_string().unwrap(), "Hi");
let r = client.get(uri!(super::custom: None as Option<Kind>)).dispatch();
assert_eq!(r.status(), Status::Unauthorized);
assert_eq!(r.content_type().unwrap(), ContentType::HTML);
assert_eq!(r.into_string().unwrap(), "No no no!");
// Take the lock so we exclusively access the FS.
let _lock = FS_LOCK.lock();
// Create the 'big_file'.
const CONTENTS: &str = "custom file contents!";
let response = client.post(uri!(super::upload)).body(CONTENTS).dispatch();
assert_eq!(response.status(), Status::Ok);
// Fetch it using `custom`.
let r = client.get(uri!(super::custom: Some(Kind::File))).dispatch();
assert_eq!(r.into_string(), Some(CONTENTS.into()));
// Delete it.
let r = client.delete(uri!(super::delete)).dispatch();
assert_eq!(r.status(), Status::Ok);
}

View File

@ -1,5 +1,5 @@
[package] [package]
name = "msgpack" name = "serialization"
version = "0.0.0" version = "0.0.0"
workspace = "../" workspace = "../"
edition = "2018" edition = "2018"
@ -7,9 +7,9 @@ publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] } serde = "1"
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
path = "../../contrib/lib" path = "../../contrib/lib"
default-features = false default-features = false
features = ["msgpack"] features = ["json", "msgpack"]

View File

@ -0,0 +1,65 @@
use std::borrow::Cow;
use rocket::State;
use rocket::tokio::sync::Mutex;
use rocket_contrib::json::{Json, JsonValue, json};
use serde::{Serialize, Deserialize};
// The type to represent the ID of a message.
type Id = usize;
// We're going to store all of the messages here. No need for a DB.
type MessageList = Mutex<Vec<String>>;
type Messages<'r> = State<'r, MessageList>;
#[derive(Serialize, Deserialize)]
struct Message<'r> {
id: Option<Id>,
message: Cow<'r, str>
}
#[post("/", format = "json", data = "<message>")]
async fn new(message: Json<Message<'_>>, list: Messages<'_>) -> JsonValue {
let mut list = list.lock().await;
let id = list.len();
list.push(message.message.to_string());
json!({ "status": "ok", "id": id })
}
#[put("/<id>", format = "json", data = "<message>")]
async fn update(id: Id, message: Json<Message<'_>>, list: Messages<'_>) -> Option<JsonValue> {
match list.lock().await.get_mut(id) {
Some(existing) => {
*existing = message.message.to_string();
Some(json!({ "status": "ok" }))
}
None => None
}
}
#[get("/<id>", format = "json")]
async fn get<'r>(id: Id, list: Messages<'r>) -> Option<Json<Message<'r>>> {
let list = list.lock().await;
Some(Json(Message {
id: Some(id),
message: list.get(id)?.to_string().into(),
}))
}
#[catch(404)]
fn not_found() -> JsonValue {
json!({
"status": "error",
"reason": "Resource was not found."
})
}
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_launch("JSON", |rocket| async {
rocket.mount("/json", routes![new, update, get])
.register("/json", catchers![not_found])
.manage(MessageList::new(vec![]))
})
}

View File

@ -0,0 +1,13 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
mod json;
mod msgpack;
#[launch]
fn rocket() -> _ {
rocket::ignite()
.attach(json::stage())
.attach(msgpack::stage())
}

View File

@ -1,7 +1,3 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
use rocket_contrib::msgpack::MsgPack; use rocket_contrib::msgpack::MsgPack;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -14,15 +10,16 @@ struct Message<'r> {
#[get("/<id>", format = "msgpack")] #[get("/<id>", format = "msgpack")]
fn get(id: usize) -> MsgPack<Message<'static>> { fn get(id: usize) -> MsgPack<Message<'static>> {
MsgPack(Message { id: id, contents: "Hello, world!", }) MsgPack(Message { id, contents: "Hello, world!", })
} }
#[post("/", data = "<data>", format = "msgpack")] #[post("/", data = "<data>", format = "msgpack")]
fn create(data: MsgPack<Message<'_>>) -> String { fn echo<'r>(data: MsgPack<Message<'r>>) -> &'r str {
data.contents.to_string() data.contents
} }
#[launch] pub fn stage() -> rocket::fairing::AdHoc {
fn rocket() -> rocket::Rocket { rocket::fairing::AdHoc::on_launch("MessagePack", |rocket| async {
rocket::ignite().mount("/message", routes![get, create]) rocket.mount("/msgpack", routes![echo, get])
})
} }

View File

@ -0,0 +1,110 @@
use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType, Accept};
#[test]
fn json_bad_get_put() {
let client = Client::tracked(super::rocket()).unwrap();
// Try to get a message with an ID that doesn't exist.
let res = client.get("/json/99").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::NotFound);
let body = res.into_string().unwrap();
assert!(body.contains("error"));
assert!(body.contains("Resource was not found."));
// Try to get a message with an invalid ID.
let res = client.get("/json/hi").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::NotFound);
assert!(res.into_string().unwrap().contains("error"));
// Try to put a message without a proper body.
let res = client.put("/json/80").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::BadRequest);
// Try to put a message with a semantically invalid body.
let res = client.put("/json/0")
.header(ContentType::JSON)
.body(r#"{ "dogs?": "love'em!" }"#)
.dispatch();
assert_eq!(res.status(), Status::UnprocessableEntity);
// Try to put a message for an ID that doesn't exist.
let res = client.put("/json/80")
.header(ContentType::JSON)
.body(r#"{ "message": "Bye bye, world!" }"#)
.dispatch();
assert_eq!(res.status(), Status::NotFound);
}
#[test]
fn json_post_get_put_get() {
let client = Client::tracked(super::rocket()).unwrap();
// Create/read/update/read a few messages.
for id in 0..10 {
let uri = format!("/json/{}", id);
let message = format!("Hello, JSON {}!", id);
// Check that a message with doesn't exist.
let res = client.get(&uri).header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::NotFound);
// Add a new message. This should be ID 0.
let res = client.post("/json")
.header(ContentType::JSON)
.body(format!(r#"{{ "message": "{}" }}"#, message))
.dispatch();
assert_eq!(res.status(), Status::Ok);
// Check that the message exists with the correct contents.
let res = client.get(&uri).header(Accept::JSON).dispatch();
assert_eq!(res.status(), Status::Ok);
let body = res.into_string().unwrap();
assert!(body.contains(&message));
// Change the message contents.
let res = client.put(&uri)
.header(ContentType::JSON)
.body(r#"{ "message": "Bye bye, world!" }"#)
.dispatch();
assert_eq!(res.status(), Status::Ok);
// Check that the message exists with the updated contents.
let res = client.get(&uri).header(Accept::JSON).dispatch();
assert_eq!(res.status(), Status::Ok);
let body = res.into_string().unwrap();
assert!(!body.contains(&message));
assert!(body.contains("Bye bye, world!"));
}
}
#[test]
fn msgpack_get() {
let client = Client::tracked(super::rocket()).unwrap();
let res = client.get("/msgpack/1").header(ContentType::MsgPack).dispatch();
assert_eq!(res.status(), Status::Ok);
assert_eq!(res.content_type(), Some(ContentType::MsgPack));
// Check that the message is `[1, "Hello, world!"]`
assert_eq!(&res.into_bytes().unwrap(), &[146, 1, 173, 72, 101, 108, 108,
111, 44, 32, 119, 111, 114, 108, 100, 33]);
}
#[test]
fn msgpack_post() {
// Dispatch request with a message of `[2, "Goodbye, world!"]`.
let client = Client::tracked(super::rocket()).unwrap();
let res = client.post("/msgpack")
.header(ContentType::MsgPack)
.body(&[146, 2, 175, 71, 111, 111, 100, 98, 121, 101, 44, 32, 119, 111,
114, 108, 100, 33])
.dispatch();
assert_eq!(res.status(), Status::Ok);
assert_eq!(res.into_string(), Some("Goodbye, world!".into()));
}

View File

@ -1,14 +0,0 @@
[package]
name = "session"
version = "0.0.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib", features = ["secrets"] }
[dependencies.rocket_contrib]
path = "../../contrib/lib"
default-features = false
features = ["handlebars_templates"]

View File

@ -1,80 +0,0 @@
use super::rocket;
use rocket::local::blocking::{Client, LocalResponse};
use rocket::http::{Status, Cookie, ContentType};
fn user_id_cookie(response: &LocalResponse<'_>) -> Option<Cookie<'static>> {
let cookie = response.headers()
.get("Set-Cookie")
.filter(|v| v.starts_with("user_id"))
.nth(0)
.and_then(|val| Cookie::parse_encoded(val).ok());
cookie.map(|c| c.into_owned())
}
fn login(client: &Client, user: &str, pass: &str) -> Option<Cookie<'static>> {
let response = client.post("/login")
.header(ContentType::Form)
.body(format!("username={}&password={}", user, pass))
.dispatch();
user_id_cookie(&response)
}
#[test]
fn redirect_on_index() {
let client = Client::tracked(rocket()).unwrap();
let response = client.get("/").dispatch();
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get_one("Location"), Some("/login"));
}
#[test]
fn can_login() {
let client = Client::tracked(rocket()).unwrap();
let response = client.get("/login").dispatch();
assert_eq!(response.status(), Status::Ok);
let body = response.into_string().unwrap();
assert!(body.contains("Please login to continue."));
}
#[test]
fn login_fails() {
let client = Client::tracked(rocket()).unwrap();
assert!(login(&client, "Seergio", "password").is_none());
assert!(login(&client, "Sergio", "idontknow").is_none());
}
#[test]
fn login_logout_succeeds() {
let client = Client::tracked(rocket()).unwrap();
let login_cookie = login(&client, "Sergio", "password").expect("logged in");
// Ensure we're logged in.
let response = client.get("/").cookie(login_cookie.clone()).dispatch();
assert_eq!(response.status(), Status::Ok);
let body = response.into_string().unwrap();
assert!(body.contains("Logged in with user ID 1"));
// One more.
let response = client.get("/login").cookie(login_cookie.clone()).dispatch();
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get_one("Location"), Some("/"));
// Logout.
let response = client.post("/logout").cookie(login_cookie).dispatch();
let cookie = user_id_cookie(&response).expect("logout cookie");
assert!(cookie.value().is_empty());
// The user should be redirected back to the login page.
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get_one("Location"), Some("/login"));
// The page should show the success message, and no errors.
let response = client.get("/login").dispatch();
assert_eq!(response.status(), Status::Ok);
let body = response.into_string().unwrap();
assert!(body.contains("Successfully logged out."));
assert!(!body.contains("Error"));
}

View File

@ -7,3 +7,4 @@ publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib" }
flume = "0.10"

View File

@ -2,29 +2,14 @@
#[cfg(test)] mod tests; #[cfg(test)] mod tests;
use std::sync::atomic::{AtomicUsize, Ordering}; mod request_local;
mod managed_hit_count;
use rocket::State; mod managed_queue;
use rocket::response::content;
struct HitCount(AtomicUsize);
#[get("/")]
fn index(hit_count: State<'_, HitCount>) -> content::Html<String> {
hit_count.0.fetch_add(1, Ordering::Relaxed);
let msg = "Your visit has been recorded!";
let count = format!("Visits: {}", count(hit_count));
content::Html(format!("{}<br /><br />{}", msg, count))
}
#[get("/count")]
fn count(hit_count: State<'_, HitCount>) -> String {
hit_count.0.load(Ordering::Relaxed).to_string()
}
#[launch] #[launch]
fn rocket() -> rocket::Rocket { fn rocket() -> _ {
rocket::ignite() rocket::ignite()
.mount("/", routes![index, count]) .attach(request_local::stage())
.manage(HitCount(AtomicUsize::new(0))) .attach(managed_hit_count::stage())
.attach(managed_queue::stage())
} }

View File

@ -0,0 +1,20 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use rocket::State;
use rocket::response::content;
use rocket::fairing::AdHoc;
struct HitCount(AtomicUsize);
#[get("/")]
fn index(hit_count: State<'_, HitCount>) -> content::Html<String> {
let count = hit_count.0.fetch_add(1, Ordering::Relaxed) + 1;
content::Html(format!("Your visit is recorded!<br /><br />Visits: {}", count))
}
pub fn stage() -> AdHoc {
AdHoc::on_launch("Managed Hit Count", |rocket| async {
rocket.mount("/count", routes![index])
.manage(HitCount(AtomicUsize::new(0)))
})
}

View File

@ -0,0 +1,25 @@
use rocket::State;
use rocket::fairing::AdHoc;
use rocket::http::Status;
struct Tx(flume::Sender<String>);
struct Rx(flume::Receiver<String>);
#[put("/push?<event>")]
fn push(event: String, tx: State<'_, Tx>) -> Result<(), Status> {
tx.0.try_send(event).map_err(|_| Status::ServiceUnavailable)
}
#[get("/pop")]
fn pop(rx: State<'_, Rx>) -> Option<String> {
rx.0.try_recv().ok()
}
pub fn stage() -> AdHoc {
AdHoc::on_launch("Managed Queue", |rocket| async {
let (tx, rx) = flume::bounded(32);
rocket.mount("/queue", routes![push, pop])
.manage(Tx(tx))
.manage(Rx(rx))
})
}

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