mirror of https://github.com/rwf2/Rocket.git
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:
parent
cfd5af38fe
commit
50c9e88cf9
|
@ -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.");
|
||||||
|
}
|
|
@ -1,35 +1,22 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"cookies",
|
|
||||||
"errors",
|
|
||||||
"forms",
|
|
||||||
"hello_person",
|
|
||||||
"query_params",
|
|
||||||
"hello_world",
|
|
||||||
"manual_routes",
|
|
||||||
"optional_redirect",
|
|
||||||
"redirect",
|
|
||||||
"static_files",
|
|
||||||
"todo",
|
|
||||||
"content_types",
|
|
||||||
"ranking",
|
|
||||||
"testing",
|
|
||||||
"request_local_state",
|
|
||||||
"request_guard",
|
|
||||||
"stream",
|
|
||||||
"json",
|
|
||||||
"msgpack",
|
|
||||||
"handlebars_templates",
|
|
||||||
"tera_templates",
|
|
||||||
"config",
|
"config",
|
||||||
"raw_upload",
|
"cookies",
|
||||||
"pastebin",
|
"databases",
|
||||||
"state",
|
"error-handling",
|
||||||
"managed_queue",
|
|
||||||
"uuid",
|
|
||||||
"session",
|
|
||||||
"raw_sqlite",
|
|
||||||
"tls",
|
|
||||||
"fairings",
|
"fairings",
|
||||||
"hello_2018",
|
"forms",
|
||||||
|
"hello",
|
||||||
|
"manual-routing",
|
||||||
|
"responders",
|
||||||
|
"serialization",
|
||||||
|
"state",
|
||||||
|
"static-files",
|
||||||
|
"templating",
|
||||||
|
"testing",
|
||||||
|
"tls",
|
||||||
|
"uuid",
|
||||||
|
|
||||||
|
"pastebin",
|
||||||
|
"todo",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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`.
|
|
@ -7,3 +7,4 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { path = "../../core/lib", features = ["secrets"] }
|
rocket = { path = "../../core/lib", features = ["secrets"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
# Except for the secret key, none of these are actually needed; Rocket has sane
|
# Except for the secret key, none of these are actually needed; Rocket has sane
|
||||||
# defaults. We show all of them here explicitly for demonstrative purposes.
|
# defaults. We show all of them here explicitly for demonstrative purposes.
|
||||||
|
|
||||||
[global.limits]
|
[default.limits]
|
||||||
forms = "64 kB"
|
forms = "64 kB"
|
||||||
json = "1 MiB"
|
json = "1 MiB"
|
||||||
msgpack = "2 MiB"
|
msgpack = "2 MiB"
|
||||||
"file/jpg" = "5 MiB"
|
"file/jpg" = "5 MiB"
|
||||||
|
|
||||||
|
[default]
|
||||||
|
key = "a default app-key"
|
||||||
|
extra = false
|
||||||
|
|
||||||
[debug]
|
[debug]
|
||||||
address = "127.0.0.1"
|
address = "127.0.0.1"
|
||||||
port = 8000
|
port = 8000
|
||||||
workers = 1
|
workers = 1
|
||||||
keep_alive = 0
|
keep_alive = 0
|
||||||
log_level = "normal"
|
log_level = "normal"
|
||||||
hi = "Hello!" # this is an unused extra; maybe application specific?
|
|
||||||
is_extra = true # this is an unused extra; maybe application specific?
|
|
||||||
|
|
||||||
[release]
|
[release]
|
||||||
address = "127.0.0.1"
|
address = "127.0.0.1"
|
||||||
|
@ -24,3 +26,5 @@ keep_alive = 5
|
||||||
log_level = "critical"
|
log_level = "critical"
|
||||||
# don't use this key! generate your own and keep it private!
|
# don't use this key! generate your own and keep it private!
|
||||||
secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk="
|
secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk="
|
||||||
|
key = "a release app-key"
|
||||||
|
extra = false
|
||||||
|
|
|
@ -1,14 +1,29 @@
|
||||||
|
#[macro_use] extern crate rocket;
|
||||||
|
|
||||||
#[cfg(test)] mod tests;
|
#[cfg(test)] mod tests;
|
||||||
|
|
||||||
|
use rocket::{State, Config};
|
||||||
use rocket::fairing::AdHoc;
|
use rocket::fairing::AdHoc;
|
||||||
|
|
||||||
// This example's illustration is the Rocket.toml file. Running this server will
|
use serde::Deserialize;
|
||||||
// print the config, however.
|
|
||||||
#[rocket::launch]
|
#[derive(Debug, Deserialize)]
|
||||||
fn rocket() -> rocket::Rocket {
|
struct AppConfig {
|
||||||
rocket::ignite()
|
key: String,
|
||||||
.attach(AdHoc::on_liftoff("Config Reader", |rocket| Box::pin(async move {
|
port: u16
|
||||||
let value = rocket.figment().find_value("").unwrap();
|
}
|
||||||
println!("{:#?}", value);
|
|
||||||
})))
|
#[get("/")]
|
||||||
|
fn read_config(rocket_config: &Config, app_config: State<'_, AppConfig>) -> String {
|
||||||
|
format!("{:#?}\n{:#?}", app_config, rocket_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See Rocket.toml file. Running this server will print the config. Try running
|
||||||
|
// with `ROCKET_PROFILE=release` manually by setting the environment variable
|
||||||
|
// and automatically by compiling with `--release`.
|
||||||
|
#[launch]
|
||||||
|
fn rocket() -> _ {
|
||||||
|
rocket::ignite()
|
||||||
|
.mount("/", routes![read_config])
|
||||||
|
.attach(AdHoc::config::<AppConfig>())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
|
@ -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/<name>/<age> instead.</p>",
|
|
||||||
request.uri())
|
|
||||||
};
|
|
||||||
|
|
||||||
Html(html)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[launch]
|
|
||||||
fn rocket() -> rocket::Rocket {
|
|
||||||
rocket::ignite()
|
|
||||||
.mount("/hello", routes![get_hello, post_hello])
|
|
||||||
.register("/", catchers![not_found])
|
|
||||||
}
|
|
|
@ -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/<name>/<age> instead.</p>";
|
|
||||||
test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string());
|
|
||||||
}
|
|
|
@ -6,7 +6,7 @@ edition = "2018"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { path = "../../core/lib" }
|
rocket = { path = "../../core/lib", features = ["secrets"] }
|
||||||
|
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_contrib]
|
||||||
path = "../../contrib/lib"
|
path = "../../contrib/lib"
|
||||||
|
|
|
@ -1,33 +1,23 @@
|
||||||
#[macro_use] extern crate rocket;
|
#[macro_use] extern crate rocket;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)] mod tests;
|
||||||
mod tests;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
mod session;
|
||||||
|
mod message;
|
||||||
|
|
||||||
use rocket::form::Form;
|
use rocket::response::content::Html;
|
||||||
use rocket::response::Redirect;
|
|
||||||
use rocket::http::{Cookie, CookieJar};
|
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
|
|
||||||
#[post("/submit", data = "<message>")]
|
|
||||||
fn submit(cookies: &CookieJar<'_>, message: Form<String>) -> Redirect {
|
|
||||||
cookies.add(Cookie::new("message", message.into_inner()));
|
|
||||||
Redirect::to("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
fn index(cookies: &CookieJar<'_>) -> Template {
|
fn index() -> Html<&'static str> {
|
||||||
let cookie = cookies.get("message");
|
Html(r#"<a href="message">Set a Message</a> or <a href="session">Use Sessions</a>."#)
|
||||||
let mut context = HashMap::new();
|
|
||||||
if let Some(ref cookie) = cookie {
|
|
||||||
context.insert("message", cookie.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
Template::render("index", &context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> rocket::Rocket {
|
fn rocket() -> _ {
|
||||||
rocket::ignite().mount("/", routes![submit, index]).attach(Template::fairing())
|
rocket::ignite()
|
||||||
|
.attach(Template::fairing())
|
||||||
|
.mount("/", routes![index])
|
||||||
|
.mount("/message", message::routes())
|
||||||
|
.mount("/session", session::routes())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
|
@ -1,7 +1,3 @@
|
||||||
#[macro_use] extern crate rocket;
|
|
||||||
|
|
||||||
#[cfg(test)] mod tests;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use rocket::outcome::IntoOutcome;
|
use rocket::outcome::IntoOutcome;
|
||||||
|
@ -13,9 +9,9 @@ use rocket::form::Form;
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
struct Login {
|
struct Login<'r> {
|
||||||
username: String,
|
username: &'r str,
|
||||||
password: String
|
password: &'r str
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -29,13 +25,42 @@ impl<'r> FromRequest<'r> for User {
|
||||||
request.cookies()
|
request.cookies()
|
||||||
.get_private("user_id")
|
.get_private("user_id")
|
||||||
.and_then(|cookie| cookie.value().parse().ok())
|
.and_then(|cookie| cookie.value().parse().ok())
|
||||||
.map(|id| User(id))
|
.map(User)
|
||||||
.or_forward(())
|
.or_forward(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! session_uri {
|
||||||
|
($($t:tt)*) => (rocket::uri!("/session", $crate::session:: $($t)*))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use session_uri as uri;
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
fn index(user: User) -> Template {
|
||||||
|
let mut context = HashMap::new();
|
||||||
|
context.insert("user_id", user.0);
|
||||||
|
Template::render("session", &context)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/", rank = 2)]
|
||||||
|
fn no_auth_index() -> Redirect {
|
||||||
|
Redirect::to(uri!(login_page))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/login")]
|
||||||
|
fn login(_user: User) -> Redirect {
|
||||||
|
Redirect::to(uri!(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/login", rank = 2)]
|
||||||
|
fn login_page(flash: Option<FlashMessage<'_>>) -> Template {
|
||||||
|
Template::render("login", &flash)
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/login", data = "<login>")]
|
#[post("/login", data = "<login>")]
|
||||||
fn login(cookies: &CookieJar<'_>, login: Form<Login>) -> Result<Redirect, Flash<Redirect>> {
|
fn post_login(cookies: &CookieJar<'_>, login: Form<Login<'_>>) -> Result<Redirect, Flash<Redirect>> {
|
||||||
if login.username == "Sergio" && login.password == "password" {
|
if login.username == "Sergio" && login.password == "password" {
|
||||||
cookies.add_private(Cookie::new("user_id", 1.to_string()));
|
cookies.add_private(Cookie::new("user_id", 1.to_string()));
|
||||||
Ok(Redirect::to(uri!(index)))
|
Ok(Redirect::to(uri!(index)))
|
||||||
|
@ -50,39 +75,6 @@ fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
|
||||||
Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.")
|
Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/login")]
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
fn login_user(_user: User) -> Redirect {
|
routes![index, no_auth_index, login, login_page, post_login, logout]
|
||||||
Redirect::to(uri!(index))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/login", rank = 2)]
|
|
||||||
fn login_page(flash: Option<FlashMessage<'_>>) -> Template {
|
|
||||||
let mut context = HashMap::new();
|
|
||||||
if let Some(ref msg) = flash {
|
|
||||||
context.insert("flash", msg.msg());
|
|
||||||
if msg.name() == "error" {
|
|
||||||
context.insert("flash_type", "Error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Template::render("login", &context)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
fn user_index(user: User) -> Template {
|
|
||||||
let mut context = HashMap::new();
|
|
||||||
context.insert("user_id", user.0);
|
|
||||||
Template::render("index", &context)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/", rank = 2)]
|
|
||||||
fn index() -> Redirect {
|
|
||||||
Redirect::to(uri!(login_page))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[launch]
|
|
||||||
fn rocket() -> rocket::Rocket {
|
|
||||||
rocket::ignite()
|
|
||||||
.attach(Template::fairing())
|
|
||||||
.mount("/", routes![index, user_index, login, logout, login_user, login_page])
|
|
||||||
}
|
}
|
|
@ -1,14 +1,89 @@
|
||||||
use std::collections::HashMap;
|
use super::{rocket, session, message};
|
||||||
|
use rocket::local::blocking::{Client, LocalResponse};
|
||||||
|
use rocket::http::{Status, Cookie, ContentType};
|
||||||
|
|
||||||
use super::rocket;
|
fn user_id_cookie(response: &LocalResponse<'_>) -> Option<Cookie<'static>> {
|
||||||
use rocket::local::blocking::Client;
|
let cookie = response.headers()
|
||||||
use rocket::http::*;
|
.get("Set-Cookie")
|
||||||
use rocket_contrib::templates::Template;
|
.filter(|v| v.starts_with("user_id"))
|
||||||
|
.nth(0)
|
||||||
|
.and_then(|val| Cookie::parse_encoded(val).ok());
|
||||||
|
|
||||||
|
cookie.map(|c| c.into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(client: &Client, user: &str, pass: &str) -> Option<Cookie<'static>> {
|
||||||
|
let response = client.post(session::uri!(login))
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.body(format!("username={}&password={}", user, pass))
|
||||||
|
.dispatch();
|
||||||
|
|
||||||
|
user_id_cookie(&response)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_submit() {
|
fn redirect_logged_out_session() {
|
||||||
let client = Client::tracked(rocket()).unwrap();
|
let client = Client::tracked(rocket()).unwrap();
|
||||||
let response = client.post("/submit")
|
let response = client.get(session::uri!(index)).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::SeeOther);
|
||||||
|
assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(login));
|
||||||
|
|
||||||
|
let response = client.get(session::uri!(login_page)).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let body = response.into_string().unwrap();
|
||||||
|
assert!(body.contains("Please login to continue."));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_fails() {
|
||||||
|
let client = Client::tracked(rocket()).unwrap();
|
||||||
|
assert!(login(&client, "Seergio", "password").is_none());
|
||||||
|
assert!(login(&client, "Sergio", "idontknow").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_logout_succeeds() {
|
||||||
|
let client = Client::tracked(rocket()).unwrap();
|
||||||
|
let login_cookie = login(&client, "Sergio", "password").expect("logged in");
|
||||||
|
|
||||||
|
// Ensure we're logged in.
|
||||||
|
let response = client.get(session::uri!(index)).cookie(login_cookie.clone()).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let body = response.into_string().unwrap();
|
||||||
|
assert!(body.contains("Logged in with user ID 1"));
|
||||||
|
|
||||||
|
// One more.
|
||||||
|
let response = client.get(session::uri!(login)).cookie(login_cookie.clone()).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::SeeOther);
|
||||||
|
assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(index));
|
||||||
|
|
||||||
|
// Logout.
|
||||||
|
let response = client.post(session::uri!(logout)).cookie(login_cookie).dispatch();
|
||||||
|
let cookie = user_id_cookie(&response).expect("logout cookie");
|
||||||
|
assert!(cookie.value().is_empty());
|
||||||
|
|
||||||
|
// The user should be redirected back to the login page.
|
||||||
|
assert_eq!(response.status(), Status::SeeOther);
|
||||||
|
assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(login));
|
||||||
|
|
||||||
|
// The page should show the success message, and no errors.
|
||||||
|
let response = client.get(session::uri!(login)).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let body = response.into_string().unwrap();
|
||||||
|
assert!(body.contains("success: Successfully logged out."));
|
||||||
|
assert!(!body.contains("Error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message() {
|
||||||
|
let client = Client::tracked(rocket()).unwrap();
|
||||||
|
|
||||||
|
// Check that there's no message initially.
|
||||||
|
let response = client.get(message::uri!(index)).dispatch();
|
||||||
|
assert!(response.into_string().unwrap().contains("No message yet."));
|
||||||
|
|
||||||
|
// Now set a message; we should get a cookie back.
|
||||||
|
let response = client.post(message::uri!(submit))
|
||||||
.header(ContentType::Form)
|
.header(ContentType::Form)
|
||||||
.body("message=Hello from Rocket!")
|
.body("message=Hello from Rocket!")
|
||||||
.dispatch();
|
.dispatch();
|
||||||
|
@ -16,35 +91,10 @@ fn test_submit() {
|
||||||
let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect();
|
let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect();
|
||||||
assert_eq!(cookie_headers.len(), 1);
|
assert_eq!(cookie_headers.len(), 1);
|
||||||
assert!(cookie_headers[0].starts_with("message=Hello%20from%20Rocket!"));
|
assert!(cookie_headers[0].starts_with("message=Hello%20from%20Rocket!"));
|
||||||
|
assert_eq!(response.headers().get_one("Location").unwrap(), &message::uri!(index));
|
||||||
let location_headers: Vec<_> = response.headers().get("Location").collect();
|
|
||||||
assert_eq!(location_headers, vec!["/".to_string()]);
|
|
||||||
assert_eq!(response.status(), Status::SeeOther);
|
assert_eq!(response.status(), Status::SeeOther);
|
||||||
}
|
|
||||||
|
// Check that the message is reflected.
|
||||||
fn test_body(optional_cookie: Option<Cookie<'static>>, expected_body: String) {
|
let response = client.get(message::uri!(index)).dispatch();
|
||||||
// Attach a cookie if one is given.
|
assert!(response.into_string().unwrap().contains("Hello from Rocket!"));
|
||||||
let client = Client::tracked(rocket()).unwrap();
|
|
||||||
let response = match optional_cookie {
|
|
||||||
Some(cookie) => client.get("/").cookie(cookie).dispatch(),
|
|
||||||
None => client.get("/").dispatch(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
|
||||||
assert_eq!(response.into_string(), Some(expected_body));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_index() {
|
|
||||||
let client = Client::tracked(rocket()).unwrap();
|
|
||||||
|
|
||||||
// Render the template with an empty context.
|
|
||||||
let mut context: HashMap<&str, &str> = HashMap::new();
|
|
||||||
let template = Template::show(client.rocket(), "index", &context).unwrap();
|
|
||||||
test_body(None, template);
|
|
||||||
|
|
||||||
// Render the template with a context that contains the message.
|
|
||||||
context.insert("message", "Hello from Rocket!");
|
|
||||||
let template = Template::show(client.rocket(), "index", &context).unwrap();
|
|
||||||
test_body(Some(Cookie::new("message", "Hello from Rocket!")), template);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,18 @@
|
||||||
|
|
||||||
<p>Please login to continue.</p>
|
<p>Please login to continue.</p>
|
||||||
|
|
||||||
{{#if flash}}
|
{{#if message}}
|
||||||
<p>{{#if flash_type}}{{flash_type}}: {{/if}}{{ flash }}</p>
|
<p>{{#if kind}}{{kind}}: {{/if}}{{ message }}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<form action="/login" method="post" accept-charset="utf-8">
|
<form action="/session/login" method="post" accept-charset="utf-8">
|
||||||
<label for="username">username</label>
|
<label for="username">username</label>
|
||||||
<input type="text" name="username" id="username" value="" />
|
<input type="text" name="username" id="username" value="" />
|
||||||
<label for="password">password</label>
|
<label for="password">password</label>
|
||||||
<input type="password" name="password" id="password" value="" />
|
<input type="password" name="password" id="password" value="" />
|
||||||
<p><input type="submit" value="login"></p>
|
<p><input type="submit" value="login"></p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<a href="/">Home</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -3,20 +3,22 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<title>Rocket: Cookie Examples</title>
|
<title>Rocket: Cookie Message</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Rocket Cookie Examples</h1>
|
<h1>Rocket Cookie Message</h1>
|
||||||
{{#if message }}
|
{{#if message }}
|
||||||
<p>{{message}}</p>
|
<p>{{message}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>No message yet.</p>
|
<p>No message yet.</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<form action="/submit" method="post" accept-charset="utf-8">
|
<form action="/message" method="post" accept-charset="utf-8">
|
||||||
<textarea placeholder="Your message here..."
|
<textarea placeholder="Your message here..."
|
||||||
name="message" rows="10" cols="50"></textarea>
|
name="message" rows="10" cols="50"></textarea>
|
||||||
<p><input type="submit" value="Set Cookie"></p>
|
<p><input type="submit" value="Set Cookie"></p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<a href="/">Home</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -8,8 +8,10 @@
|
||||||
<body>
|
<body>
|
||||||
<h1>Rocket Session Example</h1>
|
<h1>Rocket Session Example</h1>
|
||||||
<p>Logged in with user ID {{ user_id }}.</p>
|
<p>Logged in with user ID {{ user_id }}.</p>
|
||||||
<form action="/logout" method="post" accept-charset="utf-8">
|
<form action="/session/logout" method="post" accept-charset="utf-8">
|
||||||
<input type="submit" name="logout" id="logout" value="logout" />
|
<input type="submit" name="logout" id="logout" value="logout" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<a href="/">Home</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -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"]
|
|
@ -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"
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE posts;
|
|
@ -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
|
||||||
|
);
|
|
@ -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
|
||||||
|
);
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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])
|
||||||
|
})
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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])
|
||||||
|
})
|
||||||
|
}
|
|
@ -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])
|
||||||
|
})
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "hello_world"
|
name = "error-handling"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
workspace = "../"
|
workspace = "../"
|
||||||
edition = "2018"
|
edition = "2018"
|
|
@ -58,11 +58,11 @@ fn token(token: State<'_, Token>) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> rocket::Rocket {
|
fn rocket() -> _ {
|
||||||
rocket::ignite()
|
rocket::ignite()
|
||||||
.mount("/", routes![hello, token])
|
.mount("/", routes![hello, token])
|
||||||
.attach(Counter::default())
|
.attach(Counter::default())
|
||||||
.attach(AdHoc::on_launch("Token State", |rocket| async {
|
.attach(AdHoc::try_on_launch("Token State", |rocket| async {
|
||||||
println!("Adding token managed state...");
|
println!("Adding token managed state...");
|
||||||
match rocket.figment().extract_inner("token") {
|
match rocket.figment().extract_inner("token") {
|
||||||
Ok(value) => Ok(rocket.manage(Token(value))),
|
Ok(value) => Ok(rocket.manage(Token(value))),
|
||||||
|
|
|
@ -81,7 +81,7 @@ fn submit<'r>(form: Form<Contextual<'r, Submit<'r>>>) -> (Status, Template) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> rocket::Rocket {
|
fn rocket() -> _ {
|
||||||
rocket::ignite()
|
rocket::ignite()
|
||||||
.mount("/", routes![index, submit])
|
.mount("/", routes![index, submit])
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
|
|
|
@ -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"]
|
|
|
@ -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(¶m.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));
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
<footer>
|
|
||||||
<p>This is a footer partial.</p>
|
|
||||||
</footer>
|
|
|
@ -1 +0,0 @@
|
||||||
<a href="/hello/Unknown">Hello</a> | <a href="/about">About</a>
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "errors"
|
name = "hello"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
workspace = "../"
|
workspace = "../"
|
||||||
edition = "2018"
|
edition = "2018"
|
|
@ -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])
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "hello_2018"
|
|
||||||
version = "0.0.0"
|
|
||||||
workspace = "../"
|
|
||||||
edition = "2018"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { path = "../../core/lib" }
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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()));
|
|
||||||
}
|
|
|
@ -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"]
|
|
|
@ -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()))
|
|
||||||
}
|
|
|
@ -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!"));
|
|
||||||
}
|
|
|
@ -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" }
|
|
|
@ -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()))
|
|
||||||
}
|
|
|
@ -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()));
|
|
||||||
}
|
|
|
@ -93,7 +93,7 @@ impl Handler for CustomHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::launch]
|
#[rocket::launch]
|
||||||
fn rocket() -> rocket::Rocket {
|
fn rocket() -> _ {
|
||||||
let always_forward = Route::ranked(1, Get, "/", forward);
|
let always_forward = Route::ranked(1, Get, "/", forward);
|
||||||
let hello = Route::ranked(2, Get, "/", hi);
|
let hello = Route::ranked(2, Get, "/", hi);
|
||||||
|
|
|
@ -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()));
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "optional_redirect"
|
|
||||||
version = "0.0.0"
|
|
||||||
workspace = "../"
|
|
||||||
edition = "2018"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { path = "../../core/lib" }
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
|
@ -9,7 +9,7 @@ use rocket::State;
|
||||||
use rocket::data::{Data, ToByteUnit};
|
use rocket::data::{Data, ToByteUnit};
|
||||||
use rocket::http::uri::Absolute;
|
use rocket::http::uri::Absolute;
|
||||||
use rocket::response::content::Plain;
|
use rocket::response::content::Plain;
|
||||||
use rocket::tokio::fs::File;
|
use rocket::tokio::fs::{self, File};
|
||||||
|
|
||||||
use crate::paste_id::PasteId;
|
use crate::paste_id::PasteId;
|
||||||
|
|
||||||
|
@ -31,6 +31,11 @@ async fn retrieve(id: PasteId<'_>) -> Option<Plain<File>> {
|
||||||
File::open(id.file_path()).await.map(Plain).ok()
|
File::open(id.file_path()).await.map(Plain).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[delete("/<id>")]
|
||||||
|
async fn delete(id: PasteId<'_>) -> Option<()> {
|
||||||
|
fs::remove_file(id.file_path()).await.ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
fn index() -> &'static str {
|
fn index() -> &'static str {
|
||||||
"
|
"
|
||||||
|
@ -50,8 +55,8 @@ fn index() -> &'static str {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> rocket::Rocket {
|
fn rocket() -> _ {
|
||||||
rocket::ignite()
|
rocket::ignite()
|
||||||
.manage(Absolute::parse(HOST).expect("valid host"))
|
.manage(Absolute::parse(HOST).expect("valid host"))
|
||||||
.mount("/", routes![index, upload, retrieve])
|
.mount("/", routes![index, upload, delete, retrieve])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use rocket::http::uri::{self, FromUriParam};
|
||||||
use rocket::request::FromParam;
|
use rocket::request::FromParam;
|
||||||
use rand::{self, Rng};
|
use rand::{self, Rng};
|
||||||
|
|
||||||
|
@ -44,3 +45,11 @@ impl<'a> FromParam<'a> for PasteId<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> FromUriParam<uri::Path, &'a str> for PasteId<'_> {
|
||||||
|
type Target = PasteId<'a>;
|
||||||
|
|
||||||
|
fn from_uri_param(param: &'a str) -> Self::Target {
|
||||||
|
PasteId(param.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{rocket, index};
|
use super::{rocket, index, PasteId};
|
||||||
use rocket::local::blocking::Client;
|
use rocket::local::blocking::Client;
|
||||||
use rocket::http::{Status, ContentType};
|
use rocket::http::{Status, ContentType};
|
||||||
|
|
||||||
|
@ -11,23 +11,31 @@ fn check_index() {
|
||||||
let client = Client::tracked(rocket()).unwrap();
|
let client = Client::tracked(rocket()).unwrap();
|
||||||
|
|
||||||
// Ensure the index returns what we expect.
|
// Ensure the index returns what we expect.
|
||||||
let response = client.get("/").dispatch();
|
let response = client.get(uri!(super::index)).dispatch();
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
assert_eq!(response.content_type(), Some(ContentType::Plain));
|
assert_eq!(response.content_type(), Some(ContentType::Plain));
|
||||||
assert_eq!(response.into_string(), Some(index().into()))
|
assert_eq!(response.into_string(), Some(index().into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upload_paste(client: &Client, body: &str) -> String {
|
fn upload_paste(client: &Client, body: &str) -> String {
|
||||||
let response = client.post("/").body(body).dispatch();
|
let response = client.post(uri!(super::upload)).body(body).dispatch();
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
assert_eq!(response.content_type(), Some(ContentType::Plain));
|
assert_eq!(response.content_type(), Some(ContentType::Plain));
|
||||||
extract_id(&response.into_string().unwrap()).unwrap()
|
extract_id(&response.into_string().unwrap()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_paste(client: &Client, id: &str) -> String {
|
fn download_paste(client: &Client, id: &str) -> Option<String> {
|
||||||
let response = client.get(format!("/{}", id)).dispatch();
|
let response = client.get(uri!(super::retrieve: id)).dispatch();
|
||||||
|
if response.status().class().is_success() {
|
||||||
|
Some(response.into_string().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_paste(client: &Client, id: &str) {
|
||||||
|
let response = client.delete(uri!(super::delete: id)).dispatch();
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
response.into_string().unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -37,23 +45,23 @@ fn pasting() {
|
||||||
// Do a trivial upload, just to make sure it works.
|
// Do a trivial upload, just to make sure it works.
|
||||||
let body_1 = "Hello, world!";
|
let body_1 = "Hello, world!";
|
||||||
let id_1 = upload_paste(&client, body_1);
|
let id_1 = upload_paste(&client, body_1);
|
||||||
assert_eq!(download_paste(&client, &id_1), body_1);
|
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
|
||||||
|
|
||||||
// Make sure we can keep getting that paste.
|
// Make sure we can keep getting that paste.
|
||||||
assert_eq!(download_paste(&client, &id_1), body_1);
|
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
|
||||||
assert_eq!(download_paste(&client, &id_1), body_1);
|
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
|
||||||
assert_eq!(download_paste(&client, &id_1), body_1);
|
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
|
||||||
|
|
||||||
// Upload some unicode.
|
// Upload some unicode.
|
||||||
let body_2 = "こんにちは";
|
let body_2 = "こんにちは";
|
||||||
let id_2 = upload_paste(&client, body_2);
|
let id_2 = upload_paste(&client, body_2);
|
||||||
assert_eq!(download_paste(&client, &id_2), body_2);
|
assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
|
||||||
|
|
||||||
// Make sure we can get both pastes.
|
// Make sure we can get both pastes.
|
||||||
assert_eq!(download_paste(&client, &id_1), body_1);
|
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
|
||||||
assert_eq!(download_paste(&client, &id_2), body_2);
|
assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
|
||||||
assert_eq!(download_paste(&client, &id_1), body_1);
|
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
|
||||||
assert_eq!(download_paste(&client, &id_2), body_2);
|
assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
|
||||||
|
|
||||||
// Now a longer upload.
|
// Now a longer upload.
|
||||||
let body_3 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed
|
let body_3 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed
|
||||||
|
@ -63,8 +71,19 @@ fn pasting() {
|
||||||
in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
officia deserunt mollit anim id est laborum.";
|
officia deserunt mollit anim id est laborum.";
|
||||||
|
|
||||||
let id_3 = upload_paste(&client, body_3);
|
let id_3 = upload_paste(&client, body_3);
|
||||||
assert_eq!(download_paste(&client, &id_3), body_3);
|
assert_eq!(download_paste(&client, &id_3).unwrap(), body_3);
|
||||||
assert_eq!(download_paste(&client, &id_1), body_1);
|
assert_eq!(download_paste(&client, &id_1).unwrap(), body_1);
|
||||||
assert_eq!(download_paste(&client, &id_2), body_2);
|
assert_eq!(download_paste(&client, &id_2).unwrap(), body_2);
|
||||||
|
|
||||||
|
// Delete everything we uploaded.
|
||||||
|
delete_paste(&client, &id_1);
|
||||||
|
assert!(download_paste(&client, &id_1).is_none());
|
||||||
|
|
||||||
|
delete_paste(&client, &id_2);
|
||||||
|
assert!(download_paste(&client, &id_2).is_none());
|
||||||
|
|
||||||
|
delete_paste(&client, &id_3);
|
||||||
|
assert!(download_paste(&client, &id_3).is_none());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "query_params"
|
|
||||||
version = "0.0.0"
|
|
||||||
workspace = "../"
|
|
||||||
edition = "2018"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { path = "../../core/lib" }
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "ranking"
|
|
||||||
version = "0.0.0"
|
|
||||||
workspace = "../"
|
|
||||||
edition = "2018"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { path = "../../core/lib" }
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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()));
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "raw_upload"
|
|
||||||
version = "0.0.0"
|
|
||||||
workspace = "../"
|
|
||||||
edition = "2018"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { path = "../../core/lib" }
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "redirect"
|
|
||||||
version = "0.0.0"
|
|
||||||
workspace = "../"
|
|
||||||
edition = "2018"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { path = "../../core/lib" }
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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()));
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "request_guard"
|
|
||||||
version = "0.0.0"
|
|
||||||
workspace = "../"
|
|
||||||
edition = "2018"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { path = "../../core/lib" }
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "request_local_state"
|
|
||||||
version = "0.0.0"
|
|
||||||
workspace = "../"
|
|
||||||
edition = "2018"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { path = "../../core/lib" }
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "hello_person"
|
name = "responders"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
workspace = "../"
|
workspace = "../"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
@ -7,3 +7,4 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { path = "../../core/lib" }
|
rocket = { path = "../../core/lib" }
|
||||||
|
parking_lot = "0.11"
|
|
@ -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/<name>/<age> 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])
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "msgpack"
|
name = "serialization"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
workspace = "../"
|
workspace = "../"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
@ -7,9 +7,9 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { path = "../../core/lib" }
|
rocket = { path = "../../core/lib" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = "1"
|
||||||
|
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_contrib]
|
||||||
path = "../../contrib/lib"
|
path = "../../contrib/lib"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["msgpack"]
|
features = ["json", "msgpack"]
|
|
@ -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![]))
|
||||||
|
})
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -1,7 +1,3 @@
|
||||||
#[macro_use] extern crate rocket;
|
|
||||||
|
|
||||||
#[cfg(test)] mod tests;
|
|
||||||
|
|
||||||
use rocket_contrib::msgpack::MsgPack;
|
use rocket_contrib::msgpack::MsgPack;
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
@ -14,15 +10,16 @@ struct Message<'r> {
|
||||||
|
|
||||||
#[get("/<id>", format = "msgpack")]
|
#[get("/<id>", format = "msgpack")]
|
||||||
fn get(id: usize) -> MsgPack<Message<'static>> {
|
fn get(id: usize) -> MsgPack<Message<'static>> {
|
||||||
MsgPack(Message { id: id, contents: "Hello, world!", })
|
MsgPack(Message { id, contents: "Hello, world!", })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/", data = "<data>", format = "msgpack")]
|
#[post("/", data = "<data>", format = "msgpack")]
|
||||||
fn create(data: MsgPack<Message<'_>>) -> String {
|
fn echo<'r>(data: MsgPack<Message<'r>>) -> &'r str {
|
||||||
data.contents.to_string()
|
data.contents
|
||||||
}
|
}
|
||||||
|
|
||||||
#[launch]
|
pub fn stage() -> rocket::fairing::AdHoc {
|
||||||
fn rocket() -> rocket::Rocket {
|
rocket::fairing::AdHoc::on_launch("MessagePack", |rocket| async {
|
||||||
rocket::ignite().mount("/message", routes![get, create])
|
rocket.mount("/msgpack", routes![echo, get])
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -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()));
|
||||||
|
}
|
|
@ -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"]
|
|
|
@ -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"));
|
|
||||||
}
|
|
|
@ -7,3 +7,4 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { path = "../../core/lib" }
|
rocket = { path = "../../core/lib" }
|
||||||
|
flume = "0.10"
|
||||||
|
|
|
@ -2,29 +2,14 @@
|
||||||
|
|
||||||
#[cfg(test)] mod tests;
|
#[cfg(test)] mod tests;
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
mod request_local;
|
||||||
|
mod managed_hit_count;
|
||||||
use rocket::State;
|
mod managed_queue;
|
||||||
use rocket::response::content;
|
|
||||||
|
|
||||||
struct HitCount(AtomicUsize);
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
fn index(hit_count: State<'_, HitCount>) -> content::Html<String> {
|
|
||||||
hit_count.0.fetch_add(1, Ordering::Relaxed);
|
|
||||||
let msg = "Your visit has been recorded!";
|
|
||||||
let count = format!("Visits: {}", count(hit_count));
|
|
||||||
content::Html(format!("{}<br /><br />{}", msg, count))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/count")]
|
|
||||||
fn count(hit_count: State<'_, HitCount>) -> String {
|
|
||||||
hit_count.0.load(Ordering::Relaxed).to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> rocket::Rocket {
|
fn rocket() -> _ {
|
||||||
rocket::ignite()
|
rocket::ignite()
|
||||||
.mount("/", routes![index, count])
|
.attach(request_local::stage())
|
||||||
.manage(HitCount(AtomicUsize::new(0)))
|
.attach(managed_hit_count::stage())
|
||||||
|
.attach(managed_queue::stage())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)))
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
Loading…
Reference in New Issue