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]
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",
"raw_upload",
"pastebin",
"state",
"managed_queue",
"uuid",
"session",
"raw_sqlite",
"tls",
"cookies",
"databases",
"error-handling",
"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]
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
# defaults. We show all of them here explicitly for demonstrative purposes.
[global.limits]
[default.limits]
forms = "64 kB"
json = "1 MiB"
msgpack = "2 MiB"
"file/jpg" = "5 MiB"
[default]
key = "a default app-key"
extra = false
[debug]
address = "127.0.0.1"
port = 8000
workers = 1
keep_alive = 0
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]
address = "127.0.0.1"
@ -24,3 +26,5 @@ keep_alive = 5
log_level = "critical"
# don't use this key! generate your own and keep it private!
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;
use rocket::{State, Config};
use rocket::fairing::AdHoc;
// This example's illustration is the Rocket.toml file. Running this server will
// print the config, however.
#[rocket::launch]
fn rocket() -> rocket::Rocket {
rocket::ignite()
.attach(AdHoc::on_liftoff("Config Reader", |rocket| Box::pin(async move {
let value = rocket.figment().find_value("").unwrap();
println!("{:#?}", value);
})))
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AppConfig {
key: String,
port: u16
}
#[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
[dependencies]
rocket = { path = "../../core/lib" }
rocket = { path = "../../core/lib", features = ["secrets"] }
[dependencies.rocket_contrib]
path = "../../contrib/lib"

View File

@ -1,33 +1,23 @@
#[macro_use] extern crate rocket;
#[cfg(test)]
mod tests;
#[cfg(test)] mod tests;
use std::collections::HashMap;
mod session;
mod message;
use rocket::form::Form;
use rocket::response::Redirect;
use rocket::http::{Cookie, CookieJar};
use rocket::response::content::Html;
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("/")]
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("index", &context)
fn index() -> Html<&'static str> {
Html(r#"<a href="message">Set a Message</a> or <a href="session">Use Sessions</a>."#)
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![submit, index]).attach(Template::fairing())
fn rocket() -> _ {
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 rocket::outcome::IntoOutcome;
@ -13,9 +9,9 @@ use rocket::form::Form;
use rocket_contrib::templates::Template;
#[derive(FromForm)]
struct Login {
username: String,
password: String
struct Login<'r> {
username: &'r str,
password: &'r str
}
#[derive(Debug)]
@ -29,13 +25,42 @@ impl<'r> FromRequest<'r> for User {
request.cookies()
.get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok())
.map(|id| User(id))
.map(User)
.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>")]
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" {
cookies.add_private(Cookie::new("user_id", 1.to_string()));
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.")
}
#[get("/login")]
fn login_user(_user: User) -> Redirect {
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])
pub fn routes() -> Vec<rocket::Route> {
routes![index, no_auth_index, login, login_page, post_login, logout]
}

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;
use rocket::local::blocking::Client;
use rocket::http::*;
use rocket_contrib::templates::Template;
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(session::uri!(login))
.header(ContentType::Form)
.body(format!("username={}&password={}", user, pass))
.dispatch();
user_id_cookie(&response)
}
#[test]
fn test_submit() {
fn redirect_logged_out_session() {
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)
.body("message=Hello from Rocket!")
.dispatch();
@ -16,35 +91,10 @@ fn test_submit() {
let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect();
assert_eq!(cookie_headers.len(), 1);
assert!(cookie_headers[0].starts_with("message=Hello%20from%20Rocket!"));
let location_headers: Vec<_> = response.headers().get("Location").collect();
assert_eq!(location_headers, vec!["/".to_string()]);
assert_eq!(response.headers().get_one("Location").unwrap(), &message::uri!(index));
assert_eq!(response.status(), Status::SeeOther);
}
fn test_body(optional_cookie: Option<Cookie<'static>>, expected_body: String) {
// Attach a cookie if one is given.
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);
// Check that the message is reflected.
let response = client.get(message::uri!(index)).dispatch();
assert!(response.into_string().unwrap().contains("Hello from Rocket!"));
}

View File

@ -10,16 +10,18 @@
<p>Please login to continue.</p>
{{#if flash}}
<p>{{#if flash_type}}{{flash_type}}: {{/if}}{{ flash }}</p>
{{#if message}}
<p>{{#if kind}}{{kind}}: {{/if}}{{ message }}</p>
{{/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>
<input type="text" name="username" id="username" value="" />
<label for="password">password</label>
<input type="password" name="password" id="password" value="" />
<p><input type="submit" value="login"></p>
</form>
<a href="/">Home</a>
</body>
</html>

View File

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

View File

@ -8,8 +8,10 @@
<body>
<h1>Rocket Session Example</h1>
<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" />
</form>
<a href="/">Home</a>
</body>
</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]
name = "hello_world"
name = "error-handling"
version = "0.0.0"
workspace = "../"
edition = "2018"

View File

@ -58,11 +58,11 @@ fn token(token: State<'_, Token>) -> String {
}
#[launch]
fn rocket() -> rocket::Rocket {
fn rocket() -> _ {
rocket::ignite()
.mount("/", routes![hello, token])
.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...");
match rocket.figment().extract_inner("token") {
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]
fn rocket() -> rocket::Rocket {
fn rocket() -> _ {
rocket::ignite()
.mount("/", routes![index, submit])
.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]
name = "errors"
name = "hello"
version = "0.0.0"
workspace = "../"
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]
fn rocket() -> rocket::Rocket {
fn rocket() -> _ {
let always_forward = Route::ranked(1, Get, "/", forward);
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::http::uri::Absolute;
use rocket::response::content::Plain;
use rocket::tokio::fs::File;
use rocket::tokio::fs::{self, File};
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()
}
#[delete("/<id>")]
async fn delete(id: PasteId<'_>) -> Option<()> {
fs::remove_file(id.file_path()).await.ok()
}
#[get("/")]
fn index() -> &'static str {
"
@ -50,8 +55,8 @@ fn index() -> &'static str {
}
#[launch]
fn rocket() -> rocket::Rocket {
fn rocket() -> _ {
rocket::ignite()
.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::path::{Path, PathBuf};
use rocket::http::uri::{self, FromUriParam};
use rocket::request::FromParam;
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::http::{Status, ContentType};
@ -11,23 +11,31 @@ fn check_index() {
let client = Client::tracked(rocket()).unwrap();
// 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.content_type(), Some(ContentType::Plain));
assert_eq!(response.into_string(), Some(index().into()))
}
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.content_type(), Some(ContentType::Plain));
extract_id(&response.into_string().unwrap()).unwrap()
}
fn download_paste(client: &Client, id: &str) -> String {
let response = client.get(format!("/{}", id)).dispatch();
fn download_paste(client: &Client, id: &str) -> Option<String> {
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);
response.into_string().unwrap()
}
#[test]
@ -37,23 +45,23 @@ fn pasting() {
// Do a trivial upload, just to make sure it works.
let body_1 = "Hello, world!";
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.
assert_eq!(download_paste(&client, &id_1), body_1);
assert_eq!(download_paste(&client, &id_1), 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).unwrap(), body_1);
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
// Upload some unicode.
let 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.
assert_eq!(download_paste(&client, &id_1), body_1);
assert_eq!(download_paste(&client, &id_2), body_2);
assert_eq!(download_paste(&client, &id_1), body_1);
assert_eq!(download_paste(&client, &id_2), body_2);
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
// Now a longer upload.
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.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.";
let id_3 = upload_paste(&client, body_3);
assert_eq!(download_paste(&client, &id_3), body_3);
assert_eq!(download_paste(&client, &id_1), body_1);
assert_eq!(download_paste(&client, &id_2), body_2);
assert_eq!(download_paste(&client, &id_3).unwrap(), body_3);
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
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]
name = "hello_person"
name = "responders"
version = "0.0.0"
workspace = "../"
edition = "2018"
@ -7,3 +7,4 @@ publish = false
[dependencies]
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]
name = "msgpack"
name = "serialization"
version = "0.0.0"
workspace = "../"
edition = "2018"
@ -7,9 +7,9 @@ publish = false
[dependencies]
rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] }
serde = "1"
[dependencies.rocket_contrib]
path = "../../contrib/lib"
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 serde::{Serialize, Deserialize};
@ -14,15 +10,16 @@ struct Message<'r> {
#[get("/<id>", format = "msgpack")]
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")]
fn create(data: MsgPack<Message<'_>>) -> String {
data.contents.to_string()
fn echo<'r>(data: MsgPack<Message<'r>>) -> &'r str {
data.contents
}
#[launch]
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/message", routes![get, create])
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_launch("MessagePack", |rocket| async {
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]
rocket = { path = "../../core/lib" }
flume = "0.10"

View File

@ -2,29 +2,14 @@
#[cfg(test)] mod tests;
use std::sync::atomic::{AtomicUsize, Ordering};
use rocket::State;
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()
}
mod request_local;
mod managed_hit_count;
mod managed_queue;
#[launch]
fn rocket() -> rocket::Rocket {
fn rocket() -> _ {
rocket::ignite()
.mount("/", routes![index, count])
.manage(HitCount(AtomicUsize::new(0)))
.attach(request_local::stage())
.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