diff --git a/core/lib/tests/scoped-uri.rs b/core/lib/tests/scoped-uri.rs new file mode 100644 index 00000000..50971d94 --- /dev/null +++ b/core/lib/tests/scoped-uri.rs @@ -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("/")] +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."); +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 4802a251..318216c8 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -1,35 +1,22 @@ [workspace] members = [ - "cookies", - "errors", - "forms", - "hello_person", - "query_params", - "hello_world", - "manual_routes", - "optional_redirect", - "redirect", - "static_files", - "todo", - "content_types", - "ranking", - "testing", - "request_local_state", - "request_guard", - "stream", - "json", - "msgpack", - "handlebars_templates", - "tera_templates", "config", - "raw_upload", - "pastebin", - "state", - "managed_queue", - "uuid", - "session", - "raw_sqlite", - "tls", + "cookies", + "databases", + "error-handling", "fairings", - "hello_2018", + "forms", + "hello", + "manual-routing", + "responders", + "serialization", + "state", + "static-files", + "templating", + "testing", + "tls", + "uuid", + + "pastebin", + "todo", ] diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..b7e25d13 --- /dev/null +++ b/examples/README.md @@ -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`. diff --git a/examples/config/Cargo.toml b/examples/config/Cargo.toml index 20e6ad1b..8aee719a 100644 --- a/examples/config/Cargo.toml +++ b/examples/config/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] rocket = { path = "../../core/lib", features = ["secrets"] } +serde = { version = "1", features = ["derive"] } diff --git a/examples/config/Rocket.toml b/examples/config/Rocket.toml index f21bff8c..d38cb5ea 100644 --- a/examples/config/Rocket.toml +++ b/examples/config/Rocket.toml @@ -1,20 +1,22 @@ # Except for the secret key, none of these are actually needed; Rocket has sane # defaults. We show all of them here explicitly for demonstrative purposes. -[global.limits] +[default.limits] forms = "64 kB" json = "1 MiB" msgpack = "2 MiB" "file/jpg" = "5 MiB" +[default] +key = "a default app-key" +extra = false + [debug] address = "127.0.0.1" port = 8000 workers = 1 keep_alive = 0 log_level = "normal" -hi = "Hello!" # this is an unused extra; maybe application specific? -is_extra = true # this is an unused extra; maybe application specific? [release] address = "127.0.0.1" @@ -24,3 +26,5 @@ keep_alive = 5 log_level = "critical" # don't use this key! generate your own and keep it private! secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" +key = "a release app-key" +extra = false diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs index 4fcde6f2..38ec4d83 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -1,14 +1,29 @@ +#[macro_use] extern crate rocket; + #[cfg(test)] mod tests; +use rocket::{State, Config}; use rocket::fairing::AdHoc; -// This example's illustration is the Rocket.toml file. Running this server will -// print the config, however. -#[rocket::launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .attach(AdHoc::on_liftoff("Config Reader", |rocket| Box::pin(async move { - let value = rocket.figment().find_value("").unwrap(); - println!("{:#?}", value); - }))) +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct AppConfig { + key: String, + port: u16 +} + +#[get("/")] +fn read_config(rocket_config: &Config, app_config: State<'_, AppConfig>) -> String { + format!("{:#?}\n{:#?}", app_config, rocket_config) +} + +// See Rocket.toml file. Running this server will print the config. Try running +// with `ROCKET_PROFILE=release` manually by setting the environment variable +// and automatically by compiling with `--release`. +#[launch] +fn rocket() -> _ { + rocket::ignite() + .mount("/", routes![read_config]) + .attach(AdHoc::config::()) } diff --git a/examples/content_types/Cargo.toml b/examples/content_types/Cargo.toml deleted file mode 100644 index cc63dc2c..00000000 --- a/examples/content_types/Cargo.toml +++ /dev/null @@ -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" diff --git a/examples/content_types/src/main.rs b/examples/content_types/src/main.rs deleted file mode 100644 index 44d2e889..00000000 --- a/examples/content_types/src/main.rs +++ /dev/null @@ -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("//", format = "json")] -fn get_hello(name: String, age: u8) -> io::Result> { - // 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("/", format = "plain", data = "")] -async fn post_hello(age: u8, name_data: Data) -> io::Result> { - 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 { - let html = match request.format() { - Some(ref mt) if !mt.is_json() && !mt.is_plain() => { - format!("

'{}' requests are not supported.

", mt) - } - _ => format!("

Sorry, '{}' is an invalid path! Try \ - /hello/<name>/<age> instead.

", - request.uri()) - }; - - Html(html) -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .mount("/hello", routes![get_hello, post_hello]) - .register("/", catchers![not_found]) -} diff --git a/examples/content_types/src/tests.rs b/examples/content_types/src/tests.rs deleted file mode 100644 index bb9b2a9e..00000000 --- a/examples/content_types/src/tests.rs +++ /dev/null @@ -1,41 +0,0 @@ -use super::Person; -use rocket::http::{Accept, ContentType, Header, MediaType, Method, Status}; -use rocket::local::blocking::Client; - -fn test(method: Method, uri: &str, header: H, status: Status, body: String) - where H: Into> -{ - 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!("

'{}' requests are not supported.

", 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 = "

Sorry, '/unknown' is an invalid path! Try \ - /hello/<name>/<age> instead.

"; - test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string()); -} diff --git a/examples/cookies/Cargo.toml b/examples/cookies/Cargo.toml index 664ae6b4..b474ff7d 100644 --- a/examples/cookies/Cargo.toml +++ b/examples/cookies/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -rocket = { path = "../../core/lib" } +rocket = { path = "../../core/lib", features = ["secrets"] } [dependencies.rocket_contrib] path = "../../contrib/lib" diff --git a/examples/session/Rocket.toml b/examples/cookies/Rocket.toml similarity index 100% rename from examples/session/Rocket.toml rename to examples/cookies/Rocket.toml diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs index 10f0c10f..61aafaf4 100644 --- a/examples/cookies/src/main.rs +++ b/examples/cookies/src/main.rs @@ -1,33 +1,23 @@ #[macro_use] extern crate rocket; -#[cfg(test)] -mod tests; +#[cfg(test)] mod tests; -use std::collections::HashMap; +mod session; +mod message; -use rocket::form::Form; -use rocket::response::Redirect; -use rocket::http::{Cookie, CookieJar}; +use rocket::response::content::Html; use rocket_contrib::templates::Template; -#[post("/submit", data = "")] -fn submit(cookies: &CookieJar<'_>, message: Form) -> Redirect { - cookies.add(Cookie::new("message", message.into_inner())); - Redirect::to("/") -} - #[get("/")] -fn index(cookies: &CookieJar<'_>) -> Template { - let cookie = cookies.get("message"); - let mut context = HashMap::new(); - if let Some(ref cookie) = cookie { - context.insert("message", cookie.value()); - } - - Template::render("index", &context) +fn index() -> Html<&'static str> { + Html(r#"Set a Message or Use Sessions."#) } #[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/", routes![submit, index]).attach(Template::fairing()) +fn rocket() -> _ { + rocket::ignite() + .attach(Template::fairing()) + .mount("/", routes![index]) + .mount("/message", message::routes()) + .mount("/session", session::routes()) } diff --git a/examples/cookies/src/message.rs b/examples/cookies/src/message.rs new file mode 100644 index 00000000..a492f1a8 --- /dev/null +++ b/examples/cookies/src/message.rs @@ -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 = "")] +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 { + routes![submit, index] +} diff --git a/examples/session/src/main.rs b/examples/cookies/src/session.rs similarity index 64% rename from examples/session/src/main.rs rename to examples/cookies/src/session.rs index b5bed24e..ff83d385 100644 --- a/examples/session/src/main.rs +++ b/examples/cookies/src/session.rs @@ -1,7 +1,3 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - use std::collections::HashMap; use rocket::outcome::IntoOutcome; @@ -13,9 +9,9 @@ use rocket::form::Form; use rocket_contrib::templates::Template; #[derive(FromForm)] -struct Login { - username: String, - password: String +struct Login<'r> { + username: &'r str, + password: &'r str } #[derive(Debug)] @@ -29,13 +25,42 @@ impl<'r> FromRequest<'r> for User { request.cookies() .get_private("user_id") .and_then(|cookie| cookie.value().parse().ok()) - .map(|id| User(id)) + .map(User) .or_forward(()) } } +#[macro_export] +macro_rules! session_uri { + ($($t:tt)*) => (rocket::uri!("/session", $crate::session:: $($t)*)) +} + +pub use session_uri as uri; + +#[get("/")] +fn index(user: User) -> Template { + let mut context = HashMap::new(); + context.insert("user_id", user.0); + Template::render("session", &context) +} + +#[get("/", rank = 2)] +fn no_auth_index() -> Redirect { + Redirect::to(uri!(login_page)) +} + +#[get("/login")] +fn login(_user: User) -> Redirect { + Redirect::to(uri!(index)) +} + +#[get("/login", rank = 2)] +fn login_page(flash: Option>) -> Template { + Template::render("login", &flash) +} + #[post("/login", data = "")] -fn login(cookies: &CookieJar<'_>, login: Form) -> Result> { +fn post_login(cookies: &CookieJar<'_>, login: Form>) -> Result> { if login.username == "Sergio" && login.password == "password" { cookies.add_private(Cookie::new("user_id", 1.to_string())); Ok(Redirect::to(uri!(index))) @@ -50,39 +75,6 @@ fn logout(cookies: &CookieJar<'_>) -> Flash { Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.") } -#[get("/login")] -fn login_user(_user: User) -> Redirect { - Redirect::to(uri!(index)) -} - -#[get("/login", rank = 2)] -fn login_page(flash: Option>) -> Template { - let mut context = HashMap::new(); - if let Some(ref msg) = flash { - context.insert("flash", msg.msg()); - if msg.name() == "error" { - context.insert("flash_type", "Error"); - } - } - - Template::render("login", &context) -} - -#[get("/")] -fn user_index(user: User) -> Template { - let mut context = HashMap::new(); - context.insert("user_id", user.0); - Template::render("index", &context) -} - -#[get("/", rank = 2)] -fn index() -> Redirect { - Redirect::to(uri!(login_page)) -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .attach(Template::fairing()) - .mount("/", routes![index, user_index, login, logout, login_user, login_page]) +pub fn routes() -> Vec { + routes![index, no_auth_index, login, login_page, post_login, logout] } diff --git a/examples/cookies/src/tests.rs b/examples/cookies/src/tests.rs index 53d36276..b6c3e047 100644 --- a/examples/cookies/src/tests.rs +++ b/examples/cookies/src/tests.rs @@ -1,14 +1,89 @@ -use std::collections::HashMap; +use super::{rocket, session, message}; +use rocket::local::blocking::{Client, LocalResponse}; +use rocket::http::{Status, Cookie, ContentType}; -use super::rocket; -use rocket::local::blocking::Client; -use rocket::http::*; -use rocket_contrib::templates::Template; +fn user_id_cookie(response: &LocalResponse<'_>) -> Option> { + 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> { + let response = client.post(session::uri!(login)) + .header(ContentType::Form) + .body(format!("username={}&password={}", user, pass)) + .dispatch(); + + user_id_cookie(&response) +} #[test] -fn test_submit() { +fn redirect_logged_out_session() { let client = Client::tracked(rocket()).unwrap(); - let response = client.post("/submit") + let response = client.get(session::uri!(index)).dispatch(); + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(login)); + + let response = client.get(session::uri!(login_page)).dispatch(); + assert_eq!(response.status(), Status::Ok); + let body = response.into_string().unwrap(); + assert!(body.contains("Please login to continue.")); +} + +#[test] +fn login_fails() { + let client = Client::tracked(rocket()).unwrap(); + assert!(login(&client, "Seergio", "password").is_none()); + assert!(login(&client, "Sergio", "idontknow").is_none()); +} + +#[test] +fn login_logout_succeeds() { + let client = Client::tracked(rocket()).unwrap(); + let login_cookie = login(&client, "Sergio", "password").expect("logged in"); + + // Ensure we're logged in. + let response = client.get(session::uri!(index)).cookie(login_cookie.clone()).dispatch(); + assert_eq!(response.status(), Status::Ok); + let body = response.into_string().unwrap(); + assert!(body.contains("Logged in with user ID 1")); + + // One more. + let response = client.get(session::uri!(login)).cookie(login_cookie.clone()).dispatch(); + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(index)); + + // Logout. + let response = client.post(session::uri!(logout)).cookie(login_cookie).dispatch(); + let cookie = user_id_cookie(&response).expect("logout cookie"); + assert!(cookie.value().is_empty()); + + // The user should be redirected back to the login page. + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get_one("Location").unwrap(), &session::uri!(login)); + + // The page should show the success message, and no errors. + let response = client.get(session::uri!(login)).dispatch(); + assert_eq!(response.status(), Status::Ok); + let body = response.into_string().unwrap(); + assert!(body.contains("success: Successfully logged out.")); + assert!(!body.contains("Error")); +} + +#[test] +fn test_message() { + let client = Client::tracked(rocket()).unwrap(); + + // Check that there's no message initially. + let response = client.get(message::uri!(index)).dispatch(); + assert!(response.into_string().unwrap().contains("No message yet.")); + + // Now set a message; we should get a cookie back. + let response = client.post(message::uri!(submit)) .header(ContentType::Form) .body("message=Hello from Rocket!") .dispatch(); @@ -16,35 +91,10 @@ fn test_submit() { let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect(); assert_eq!(cookie_headers.len(), 1); assert!(cookie_headers[0].starts_with("message=Hello%20from%20Rocket!")); - - let location_headers: Vec<_> = response.headers().get("Location").collect(); - assert_eq!(location_headers, vec!["/".to_string()]); + assert_eq!(response.headers().get_one("Location").unwrap(), &message::uri!(index)); assert_eq!(response.status(), Status::SeeOther); -} - -fn test_body(optional_cookie: Option>, expected_body: String) { - // Attach a cookie if one is given. - let client = Client::tracked(rocket()).unwrap(); - let response = match optional_cookie { - Some(cookie) => client.get("/").cookie(cookie).dispatch(), - None => client.get("/").dispatch(), - }; - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string(), Some(expected_body)); -} - -#[test] -fn test_index() { - let client = Client::tracked(rocket()).unwrap(); - - // Render the template with an empty context. - let mut context: HashMap<&str, &str> = HashMap::new(); - let template = Template::show(client.rocket(), "index", &context).unwrap(); - test_body(None, template); - - // Render the template with a context that contains the message. - context.insert("message", "Hello from Rocket!"); - let template = Template::show(client.rocket(), "index", &context).unwrap(); - test_body(Some(Cookie::new("message", "Hello from Rocket!")), template); + + // Check that the message is reflected. + let response = client.get(message::uri!(index)).dispatch(); + assert!(response.into_string().unwrap().contains("Hello from Rocket!")); } diff --git a/examples/session/templates/login.html.hbs b/examples/cookies/templates/login.html.hbs similarity index 76% rename from examples/session/templates/login.html.hbs rename to examples/cookies/templates/login.html.hbs index 46ae01d1..10f75b63 100644 --- a/examples/session/templates/login.html.hbs +++ b/examples/cookies/templates/login.html.hbs @@ -10,16 +10,18 @@

Please login to continue.

- {{#if flash}} -

{{#if flash_type}}{{flash_type}}: {{/if}}{{ flash }}

+ {{#if message}} +

{{#if kind}}{{kind}}: {{/if}}{{ message }}

{{/if}} -
+

+ + Home diff --git a/examples/cookies/templates/index.html.hbs b/examples/cookies/templates/message.html.hbs similarity index 71% rename from examples/cookies/templates/index.html.hbs rename to examples/cookies/templates/message.html.hbs index de4be354..7dcec374 100644 --- a/examples/cookies/templates/index.html.hbs +++ b/examples/cookies/templates/message.html.hbs @@ -3,20 +3,22 @@ - Rocket: Cookie Examples + Rocket: Cookie Message -

Rocket Cookie Examples

+

Rocket Cookie Message

{{#if message }}

{{message}}

{{else}}

No message yet.

{{/if}} -
+

+ + Home diff --git a/examples/session/templates/index.html.hbs b/examples/cookies/templates/session.html.hbs similarity index 78% rename from examples/session/templates/index.html.hbs rename to examples/cookies/templates/session.html.hbs index f1c84d0b..8afb5073 100644 --- a/examples/session/templates/index.html.hbs +++ b/examples/cookies/templates/session.html.hbs @@ -8,8 +8,10 @@

Rocket Session Example

Logged in with user ID {{ user_id }}.

-
+
+ + Home diff --git a/examples/databases/Cargo.toml b/examples/databases/Cargo.toml new file mode 100644 index 00000000..93e6e497 --- /dev/null +++ b/examples/databases/Cargo.toml @@ -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"] diff --git a/examples/databases/Rocket.toml b/examples/databases/Rocket.toml new file mode 100644 index 00000000..12acc8ec --- /dev/null +++ b/examples/databases/Rocket.toml @@ -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" diff --git a/examples/databases/db/diesel/migrations/.gitkeep b/examples/databases/db/diesel/migrations/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/databases/db/diesel/migrations/20210329150332_create_posts_table/down.sql b/examples/databases/db/diesel/migrations/20210329150332_create_posts_table/down.sql new file mode 100644 index 00000000..1651d895 --- /dev/null +++ b/examples/databases/db/diesel/migrations/20210329150332_create_posts_table/down.sql @@ -0,0 +1 @@ +DROP TABLE posts; diff --git a/examples/databases/db/diesel/migrations/20210329150332_create_posts_table/up.sql b/examples/databases/db/diesel/migrations/20210329150332_create_posts_table/up.sql new file mode 100644 index 00000000..11941923 --- /dev/null +++ b/examples/databases/db/diesel/migrations/20210329150332_create_posts_table/up.sql @@ -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 +); diff --git a/examples/databases/db/sqlx/migrations/20210331024424_create-posts-table.sql b/examples/databases/db/sqlx/migrations/20210331024424_create-posts-table.sql new file mode 100644 index 00000000..11941923 --- /dev/null +++ b/examples/databases/db/sqlx/migrations/20210331024424_create-posts-table.sql @@ -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 +); diff --git a/examples/databases/sqlx-data.json b/examples/databases/sqlx-data.json new file mode 100644 index 00000000..9dbd4945 --- /dev/null +++ b/examples/databases/sqlx-data.json @@ -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": [] + } + } +} \ No newline at end of file diff --git a/examples/databases/src/diesel_sqlite.rs b/examples/databases/src/diesel_sqlite.rs new file mode 100644 index 00000000..75e78fb3 --- /dev/null +++ b/examples/databases/src/diesel_sqlite.rs @@ -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> = std::result::Result; + +#[derive(Debug, Clone, Deserialize, Serialize, Queryable, Insertable)] +#[table_name="posts"] +struct Post { + #[serde(skip_deserializing)] + id: Option, + title: String, + text: String, + #[serde(skip_deserializing)] + published: bool, +} + +table! { + posts (id) { + id -> Nullable, + title -> Text, + text -> Text, + published -> Bool, + } +} + +#[post("/", data = "")] +async fn create(db: Db, post: Json) -> Result>> { + 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>>> { + let ids: Vec> = db.run(move |conn| { + posts::table + .select(posts::id) + .load(conn) + }).await?; + + Ok(Json(ids)) +} + +#[get("/")] +async fn read(db: Db, id: i32) -> Option> { + db.run(move |conn| { + posts::table + .filter(posts::id.eq(id)) + .first(conn) + }).await.map(Json).ok() +} + +#[delete("/")] +async fn delete(db: Db, id: i32) -> Result> { + 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]) + }) +} diff --git a/examples/databases/src/main.rs b/examples/databases/src/main.rs new file mode 100644 index 00000000..05eee959 --- /dev/null +++ b/examples/databases/src/main.rs @@ -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()) +} diff --git a/examples/databases/src/rusqlite.rs b/examples/databases/src/rusqlite.rs new file mode 100644 index 00000000..9968972d --- /dev/null +++ b/examples/databases/src/rusqlite.rs @@ -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, + title: String, + text: String, +} + +type Result> = std::result::Result; + +#[post("/", data = "")] +async fn create(db: Db, post: Json) -> Result>> { + 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>> { + let ids = db.run(|conn| { + conn.prepare("SELECT id FROM posts")? + .query_map(params![], |row| row.get(0))? + .collect::, _>>() + }).await?; + + Ok(Json(ids)) +} + +#[get("/")] +async fn read(db: Db, id: i64) -> Option> { + 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("/")] +async fn delete(db: Db, id: i64) -> Result> { + 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]) + }) +} diff --git a/examples/databases/src/sqlx.rs b/examples/databases/src/sqlx.rs new file mode 100644 index 00000000..c1badb85 --- /dev/null +++ b/examples/databases/src/sqlx.rs @@ -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> = std::result::Result; + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct Post { + #[serde(skip_deserializing, skip_serializing_if = "Option::is_none")] + id: Option, + title: String, + text: String, +} + +#[post("/", data = "")] +async fn create(db: State<'_, Db>, post: Json) -> Result>> { + // 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>> { + let ids = sqlx::query!("SELECT id FROM posts") + .fetch(&*db) + .map_ok(|record| record.id) + .try_collect::>() + .await?; + + Ok(Json(ids)) +} + +#[get("/")] +async fn read(db: State<'_, Db>, id: i64) -> Option> { + 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("/")] +async fn delete(db: State<'_, Db>, id: i64) -> Result> { + 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 { + 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]) + }) +} diff --git a/examples/databases/src/tests.rs b/examples/databases/src/tests.rs new file mode 100644 index 00000000..8ca60636 --- /dev/null +++ b/examples/databases/src/tests.rs @@ -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(self) -> Option; +} + +trait LocalRequestExt { + fn json(self, value: &T) -> Self; +} + +impl LocalResponseExt for LocalResponse<'_> { + fn into_json(self) -> Option { + serde_json::from_reader(self).ok() + } +} + +impl LocalRequestExt for LocalRequest<'_> { + fn json(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::>(), 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::(); + assert_eq!(response.unwrap(), post); + + // Ensure the index shows one more post. + let list = client.get(base).dispatch().into_json::>().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::().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::>().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::>().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()) +} diff --git a/examples/hello_world/Cargo.toml b/examples/error-handling/Cargo.toml similarity index 84% rename from examples/hello_world/Cargo.toml rename to examples/error-handling/Cargo.toml index ead674ed..6075b1f0 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/error-handling/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "hello_world" +name = "error-handling" version = "0.0.0" workspace = "../" edition = "2018" diff --git a/examples/errors/src/main.rs b/examples/error-handling/src/main.rs similarity index 100% rename from examples/errors/src/main.rs rename to examples/error-handling/src/main.rs diff --git a/examples/errors/src/tests.rs b/examples/error-handling/src/tests.rs similarity index 100% rename from examples/errors/src/tests.rs rename to examples/error-handling/src/tests.rs diff --git a/examples/fairings/src/main.rs b/examples/fairings/src/main.rs index b284cccf..fc471068 100644 --- a/examples/fairings/src/main.rs +++ b/examples/fairings/src/main.rs @@ -58,11 +58,11 @@ fn token(token: State<'_, Token>) -> String { } #[launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { rocket::ignite() .mount("/", routes![hello, token]) .attach(Counter::default()) - .attach(AdHoc::on_launch("Token State", |rocket| async { + .attach(AdHoc::try_on_launch("Token State", |rocket| async { println!("Adding token managed state..."); match rocket.figment().extract_inner("token") { Ok(value) => Ok(rocket.manage(Token(value))), diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index fff5a604..ca31ea7f 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -81,7 +81,7 @@ fn submit<'r>(form: Form>>) -> (Status, Template) { } #[launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { rocket::ignite() .mount("/", routes![index, submit]) .attach(Template::fairing()) diff --git a/examples/handlebars_templates/Cargo.toml b/examples/handlebars_templates/Cargo.toml deleted file mode 100644 index ab3ee8cb..00000000 --- a/examples/handlebars_templates/Cargo.toml +++ /dev/null @@ -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"] diff --git a/examples/handlebars_templates/src/main.rs b/examples/handlebars_templates/src/main.rs deleted file mode 100644 index a2421f5f..00000000 --- a/examples/handlebars_templates/src/main.rs +++ /dev/null @@ -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, - 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/")] -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("")?; - out.write(¶m.value().render())?; - out.write("")?; - } - - 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)); - })) -} diff --git a/examples/handlebars_templates/src/tests.rs b/examples/handlebars_templates/src/tests.rs deleted file mode 100644 index 998a364a..00000000 --- a/examples/handlebars_templates/src/tests.rs +++ /dev/null @@ -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/ 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)); - }); -} diff --git a/examples/handlebars_templates/templates/footer.hbs b/examples/handlebars_templates/templates/footer.hbs deleted file mode 100644 index b303fbd0..00000000 --- a/examples/handlebars_templates/templates/footer.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
-

This is a footer partial.

-
diff --git a/examples/handlebars_templates/templates/nav.hbs b/examples/handlebars_templates/templates/nav.hbs deleted file mode 100644 index de74e6d2..00000000 --- a/examples/handlebars_templates/templates/nav.hbs +++ /dev/null @@ -1 +0,0 @@ -Hello | About diff --git a/examples/errors/Cargo.toml b/examples/hello/Cargo.toml similarity index 89% rename from examples/errors/Cargo.toml rename to examples/hello/Cargo.toml index dde9a009..9d2442a4 100644 --- a/examples/errors/Cargo.toml +++ b/examples/hello/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "errors" +name = "hello" version = "0.0.0" workspace = "../" edition = "2018" diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs new file mode 100644 index 00000000..4ad8418a --- /dev/null +++ b/examples/hello/src/main.rs @@ -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("//")] +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("/?&")] +fn hello(lang: Option, 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]) +} diff --git a/examples/hello/src/tests.rs b/examples/hello/src/tests.rs new file mode 100644 index 00000000..fd5b628d --- /dev/null +++ b/examples/hello/src/tests.rs @@ -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); + } + } +} diff --git a/examples/hello_2018/Cargo.toml b/examples/hello_2018/Cargo.toml deleted file mode 100644 index 7c09ed32..00000000 --- a/examples/hello_2018/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "hello_2018" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/hello_2018/src/main.rs b/examples/hello_2018/src/main.rs deleted file mode 100644 index 51651475..00000000 --- a/examples/hello_2018/src/main.rs +++ /dev/null @@ -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" -} diff --git a/examples/hello_2018/src/tests.rs b/examples/hello_2018/src/tests.rs deleted file mode 100644 index 38ca2adf..00000000 --- a/examples/hello_2018/src/tests.rs +++ /dev/null @@ -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("/")] - 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."); - } -} diff --git a/examples/hello_person/src/main.rs b/examples/hello_person/src/main.rs deleted file mode 100644 index 72307c08..00000000 --- a/examples/hello_person/src/main.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - -#[get("/hello//")] -fn hello(name: String, age: u8) -> String { - format!("Hello, {} year old named {}!", age, name) -} - -#[get("/hello/")] -fn hi(name: &str) -> &str { - name -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/", routes![hello, hi]) -} diff --git a/examples/hello_person/src/tests.rs b/examples/hello_person/src/tests.rs deleted file mode 100644 index c223ed20..00000000 --- a/examples/hello_person/src/tests.rs +++ /dev/null @@ -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()); - } -} diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs deleted file mode 100644 index 203605d0..00000000 --- a/examples/hello_world/src/main.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - -#[get("/?")] -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]) -} diff --git a/examples/hello_world/src/tests.rs b/examples/hello_world/src/tests.rs deleted file mode 100644 index 39d67b58..00000000 --- a/examples/hello_world/src/tests.rs +++ /dev/null @@ -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())); -} diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml deleted file mode 100644 index a6047eca..00000000 --- a/examples/json/Cargo.toml +++ /dev/null @@ -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"] diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs deleted file mode 100644 index 72e849c4..00000000 --- a/examples/json/src/main.rs +++ /dev/null @@ -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>>; - -#[derive(Serialize, Deserialize)] -struct Message<'r> { - id: Option, - contents: Cow<'r, str> -} - -#[post("/", format = "json", data = "")] -async fn new(id: Id, message: Json>, 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("/", format = "json", data = "")] -async fn update(id: Id, message: Json>, map: MessageMap<'_>) -> Option { - 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("/", format = "json")] -async fn get<'r>(id: Id, map: MessageMap<'r>) -> Option>> { - let hashmap = map.lock().await; - let contents = hashmap.get(&id)?.clone(); - Some(Json(Message { - id: Some(id), - contents: contents.into() - })) -} - -#[get("/echo", data = "")] -fn echo<'r>(msg: Json>) -> 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::::new())) -} diff --git a/examples/json/src/tests.rs b/examples/json/src/tests.rs deleted file mode 100644 index 40b874b4..00000000 --- a/examples/json/src/tests.rs +++ /dev/null @@ -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!")); -} diff --git a/examples/managed_queue/Cargo.toml b/examples/managed_queue/Cargo.toml deleted file mode 100644 index 42d97d19..00000000 --- a/examples/managed_queue/Cargo.toml +++ /dev/null @@ -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" } diff --git a/examples/managed_queue/src/main.rs b/examples/managed_queue/src/main.rs deleted file mode 100644 index 9fe476ef..00000000 --- a/examples/managed_queue/src/main.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - -use rocket::State; -use crossbeam::queue::SegQueue; - -struct LogChannel(SegQueue); - -#[put("/push?")] -fn push(event: String, queue: State<'_, LogChannel>) { - queue.0.push(event); -} - -#[get("/pop")] -fn pop(queue: State<'_, LogChannel>) -> Option { - queue.0.pop().ok() -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .mount("/", routes![push, pop]) - .manage(LogChannel(SegQueue::new())) -} diff --git a/examples/managed_queue/src/tests.rs b/examples/managed_queue/src/tests.rs deleted file mode 100644 index a00c8a2f..00000000 --- a/examples/managed_queue/src/tests.rs +++ /dev/null @@ -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())); -} diff --git a/examples/manual_routes/Cargo.toml b/examples/manual-routing/Cargo.toml similarity index 100% rename from examples/manual_routes/Cargo.toml rename to examples/manual-routing/Cargo.toml diff --git a/examples/manual_routes/Rocket.toml b/examples/manual-routing/Rocket.toml similarity index 100% rename from examples/manual_routes/Rocket.toml rename to examples/manual-routing/Rocket.toml diff --git a/examples/manual_routes/src/main.rs b/examples/manual-routing/src/main.rs similarity index 99% rename from examples/manual_routes/src/main.rs rename to examples/manual-routing/src/main.rs index d24cdce1..db5f0ee7 100644 --- a/examples/manual_routes/src/main.rs +++ b/examples/manual-routing/src/main.rs @@ -93,7 +93,7 @@ impl Handler for CustomHandler { } #[rocket::launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { let always_forward = Route::ranked(1, Get, "/", forward); let hello = Route::ranked(2, Get, "/", hi); diff --git a/examples/manual_routes/src/tests.rs b/examples/manual-routing/src/tests.rs similarity index 100% rename from examples/manual_routes/src/tests.rs rename to examples/manual-routing/src/tests.rs diff --git a/examples/msgpack/src/tests.rs b/examples/msgpack/src/tests.rs deleted file mode 100644 index f33660ac..00000000 --- a/examples/msgpack/src/tests.rs +++ /dev/null @@ -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())); -} diff --git a/examples/optional_redirect/Cargo.toml b/examples/optional_redirect/Cargo.toml deleted file mode 100644 index 737591e4..00000000 --- a/examples/optional_redirect/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "optional_redirect" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/optional_redirect/src/main.rs b/examples/optional_redirect/src/main.rs deleted file mode 100644 index 7e33f498..00000000 --- a/examples/optional_redirect/src/main.rs +++ /dev/null @@ -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/")] -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]) -} diff --git a/examples/optional_redirect/src/tests.rs b/examples/optional_redirect/src/tests.rs deleted file mode 100644 index 21137bba..00000000 --- a/examples/optional_redirect/src/tests.rs +++ /dev/null @@ -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"); -} diff --git a/examples/pastebin/src/main.rs b/examples/pastebin/src/main.rs index 5835139c..d1caaa3c 100644 --- a/examples/pastebin/src/main.rs +++ b/examples/pastebin/src/main.rs @@ -9,7 +9,7 @@ use rocket::State; use rocket::data::{Data, ToByteUnit}; use rocket::http::uri::Absolute; use rocket::response::content::Plain; -use rocket::tokio::fs::File; +use rocket::tokio::fs::{self, File}; use crate::paste_id::PasteId; @@ -31,6 +31,11 @@ async fn retrieve(id: PasteId<'_>) -> Option> { File::open(id.file_path()).await.map(Plain).ok() } +#[delete("/")] +async fn delete(id: PasteId<'_>) -> Option<()> { + fs::remove_file(id.file_path()).await.ok() +} + #[get("/")] fn index() -> &'static str { " @@ -50,8 +55,8 @@ fn index() -> &'static str { } #[launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { rocket::ignite() .manage(Absolute::parse(HOST).expect("valid host")) - .mount("/", routes![index, upload, retrieve]) + .mount("/", routes![index, upload, delete, retrieve]) } diff --git a/examples/pastebin/src/paste_id.rs b/examples/pastebin/src/paste_id.rs index 25c9fc02..1eee006f 100644 --- a/examples/pastebin/src/paste_id.rs +++ b/examples/pastebin/src/paste_id.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::path::{Path, PathBuf}; +use rocket::http::uri::{self, FromUriParam}; use rocket::request::FromParam; use rand::{self, Rng}; @@ -44,3 +45,11 @@ impl<'a> FromParam<'a> for PasteId<'a> { } } } + +impl<'a> FromUriParam for PasteId<'_> { + type Target = PasteId<'a>; + + fn from_uri_param(param: &'a str) -> Self::Target { + PasteId(param.into()) + } +} diff --git a/examples/pastebin/src/tests.rs b/examples/pastebin/src/tests.rs index 6bfe6a85..bf77ce6f 100644 --- a/examples/pastebin/src/tests.rs +++ b/examples/pastebin/src/tests.rs @@ -1,4 +1,4 @@ -use super::{rocket, index}; +use super::{rocket, index, PasteId}; use rocket::local::blocking::Client; use rocket::http::{Status, ContentType}; @@ -11,23 +11,31 @@ fn check_index() { let client = Client::tracked(rocket()).unwrap(); // Ensure the index returns what we expect. - let response = client.get("/").dispatch(); + let response = client.get(uri!(super::index)).dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::Plain)); assert_eq!(response.into_string(), Some(index().into())) } fn upload_paste(client: &Client, body: &str) -> String { - let response = client.post("/").body(body).dispatch(); + let response = client.post(uri!(super::upload)).body(body).dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::Plain)); extract_id(&response.into_string().unwrap()).unwrap() } -fn download_paste(client: &Client, id: &str) -> String { - let response = client.get(format!("/{}", id)).dispatch(); +fn download_paste(client: &Client, id: &str) -> Option { + let response = client.get(uri!(super::retrieve: id)).dispatch(); + if response.status().class().is_success() { + Some(response.into_string().unwrap()) + } else { + None + } +} + +fn delete_paste(client: &Client, id: &str) { + let response = client.delete(uri!(super::delete: id)).dispatch(); assert_eq!(response.status(), Status::Ok); - response.into_string().unwrap() } #[test] @@ -37,23 +45,23 @@ fn pasting() { // Do a trivial upload, just to make sure it works. let body_1 = "Hello, world!"; let id_1 = upload_paste(&client, body_1); - assert_eq!(download_paste(&client, &id_1), body_1); + assert_eq!(download_paste(&client, &id_1).unwrap(), body_1); // Make sure we can keep getting that paste. - assert_eq!(download_paste(&client, &id_1), body_1); - assert_eq!(download_paste(&client, &id_1), body_1); - assert_eq!(download_paste(&client, &id_1), body_1); + assert_eq!(download_paste(&client, &id_1).unwrap(), body_1); + assert_eq!(download_paste(&client, &id_1).unwrap(), body_1); + assert_eq!(download_paste(&client, &id_1).unwrap(), body_1); // Upload some unicode. let body_2 = "こんにちは"; let id_2 = upload_paste(&client, body_2); - assert_eq!(download_paste(&client, &id_2), body_2); + assert_eq!(download_paste(&client, &id_2).unwrap(), body_2); // Make sure we can get both pastes. - assert_eq!(download_paste(&client, &id_1), body_1); - assert_eq!(download_paste(&client, &id_2), body_2); - assert_eq!(download_paste(&client, &id_1), body_1); - assert_eq!(download_paste(&client, &id_2), body_2); + assert_eq!(download_paste(&client, &id_1).unwrap(), body_1); + assert_eq!(download_paste(&client, &id_2).unwrap(), body_2); + assert_eq!(download_paste(&client, &id_1).unwrap(), body_1); + assert_eq!(download_paste(&client, &id_2).unwrap(), body_2); // Now a longer upload. let body_3 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed @@ -63,8 +71,19 @@ fn pasting() { in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + let id_3 = upload_paste(&client, body_3); - assert_eq!(download_paste(&client, &id_3), body_3); - assert_eq!(download_paste(&client, &id_1), body_1); - assert_eq!(download_paste(&client, &id_2), body_2); + assert_eq!(download_paste(&client, &id_3).unwrap(), body_3); + assert_eq!(download_paste(&client, &id_1).unwrap(), body_1); + assert_eq!(download_paste(&client, &id_2).unwrap(), body_2); + + // Delete everything we uploaded. + delete_paste(&client, &id_1); + assert!(download_paste(&client, &id_1).is_none()); + + delete_paste(&client, &id_2); + assert!(download_paste(&client, &id_2).is_none()); + + delete_paste(&client, &id_3); + assert!(download_paste(&client, &id_3).is_none()); } diff --git a/examples/query_params/Cargo.toml b/examples/query_params/Cargo.toml deleted file mode 100644 index 41c99351..00000000 --- a/examples/query_params/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "query_params" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/query_params/src/main.rs b/examples/query_params/src/main.rs deleted file mode 100644 index ec1a282c..00000000 --- a/examples/query_params/src/main.rs +++ /dev/null @@ -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 -} - -#[get("/hello?")] -fn hello(person: Option>) -> 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&")] -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]) -} diff --git a/examples/query_params/src/tests.rs b/examples/query_params/src/tests.rs deleted file mode 100644 index 0ad92d6e..00000000 --- a/examples/query_params/src/tests.rs +++ /dev/null @@ -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); - }); -} diff --git a/examples/ranking/Cargo.toml b/examples/ranking/Cargo.toml deleted file mode 100644 index 286a1edc..00000000 --- a/examples/ranking/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "ranking" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/ranking/src/main.rs b/examples/ranking/src/main.rs deleted file mode 100644 index 82f1aa53..00000000 --- a/examples/ranking/src/main.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - -#[get("/hello//")] -fn hello(name: String, age: i8) -> String { - format!("Hello, {} year old named {}!", age, name) -} - -#[get("/hello//", 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]) -} diff --git a/examples/ranking/src/tests.rs b/examples/ranking/src/tests.rs deleted file mode 100644 index 6f6f3dc7..00000000 --- a/examples/ranking/src/tests.rs +++ /dev/null @@ -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)); - } -} diff --git a/examples/raw_sqlite/Cargo.toml b/examples/raw_sqlite/Cargo.toml deleted file mode 100644 index b10c9e26..00000000 --- a/examples/raw_sqlite/Cargo.toml +++ /dev/null @@ -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" diff --git a/examples/raw_sqlite/src/main.rs b/examples/raw_sqlite/src/main.rs deleted file mode 100644 index 7c7e6c9d..00000000 --- a/examples/raw_sqlite/src/main.rs +++ /dev/null @@ -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; - -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> { - 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]) -} diff --git a/examples/raw_sqlite/src/tests.rs b/examples/raw_sqlite/src/tests.rs deleted file mode 100644 index df334da7..00000000 --- a/examples/raw_sqlite/src/tests.rs +++ /dev/null @@ -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())); -} diff --git a/examples/raw_upload/Cargo.toml b/examples/raw_upload/Cargo.toml deleted file mode 100644 index 17bd5bba..00000000 --- a/examples/raw_upload/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "raw_upload" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/raw_upload/src/main.rs b/examples/raw_upload/src/main.rs deleted file mode 100644 index 49ffd6f4..00000000 --- a/examples/raw_upload/src/main.rs +++ /dev/null @@ -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 = "")] -async fn upload(mut file: Capped>) -> io::Result { - 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]) -} diff --git a/examples/raw_upload/src/tests.rs b/examples/raw_upload/src/tests.rs deleted file mode 100644 index b3757609..00000000 --- a/examples/raw_upload/src/tests.rs +++ /dev/null @@ -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); -} diff --git a/examples/redirect/Cargo.toml b/examples/redirect/Cargo.toml deleted file mode 100644 index 5cd3f4eb..00000000 --- a/examples/redirect/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "redirect" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/redirect/src/main.rs b/examples/redirect/src/main.rs deleted file mode 100644 index 4f3ff885..00000000 --- a/examples/redirect/src/main.rs +++ /dev/null @@ -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]) -} diff --git a/examples/redirect/src/tests.rs b/examples/redirect/src/tests.rs deleted file mode 100644 index 5884dd32..00000000 --- a/examples/redirect/src/tests.rs +++ /dev/null @@ -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::().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())); -} diff --git a/examples/request_guard/Cargo.toml b/examples/request_guard/Cargo.toml deleted file mode 100644 index 6f7d2eee..00000000 --- a/examples/request_guard/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "request_guard" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/request_guard/src/main.rs b/examples/request_guard/src/main.rs deleted file mode 100644 index cc9cbf28..00000000 --- a/examples/request_guard/src/main.rs +++ /dev/null @@ -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 { - 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>) { - 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); - } - } -} diff --git a/examples/request_local_state/Cargo.toml b/examples/request_local_state/Cargo.toml deleted file mode 100644 index b466ee01..00000000 --- a/examples/request_local_state/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "request_local_state" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/request_local_state/src/tests.rs b/examples/request_local_state/src/tests.rs deleted file mode 100644 index ac03355e..00000000 --- a/examples/request_local_state/src/tests.rs +++ /dev/null @@ -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::().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::().unwrap(); - assert_eq!(atomics.uncached.load(Ordering::Relaxed), 4); - assert_eq!(atomics.cached.load(Ordering::Relaxed), 2); -} diff --git a/examples/hello_person/Cargo.toml b/examples/responders/Cargo.toml similarity index 76% rename from examples/hello_person/Cargo.toml rename to examples/responders/Cargo.toml index 66d9f256..514974c9 100644 --- a/examples/hello_person/Cargo.toml +++ b/examples/responders/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "hello_person" +name = "responders" version = "0.0.0" workspace = "../" edition = "2018" @@ -7,3 +7,4 @@ publish = false [dependencies] rocket = { path = "../../core/lib" } +parking_lot = "0.11" diff --git a/examples/responders/src/main.rs b/examples/responders/src/main.rs new file mode 100644 index 00000000..50d92ee8 --- /dev/null +++ b/examples/responders/src/main.rs @@ -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> { + content::Plain(Stream::from(repeat('a' as u8).take(25000))) +} + +#[get("/stream/file")] +async fn file() -> Option> { + // 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 = "")] +async fn upload(mut file: Capped>) -> io::Result { + 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/")] +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("I'm here") +} + +#[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 { + let html = match request.format() { + Some(ref mt) if !(mt.is_xml() || mt.is_html()) => { + format!("

'{}' requests are not supported.

", mt) + } + _ => format!("

Sorry, '{}' is an invalid path! Try \ + /hello/<name>/<age> instead.

", + 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/")] +fn json_or_msgpack(kind: &str) -> Either, 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), + String(Cow<'static, str>), + Bytes(Vec), + #[response(status = 401)] + NotAuthorized(Html<&'static str>), +} + +#[derive(FromFormField, UriDisplayQuery)] +enum Kind { + File, + String, + Bytes +} + +#[get("/custom?")] +async fn custom(kind: Option) -> 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]) +} diff --git a/examples/responders/src/tests.rs b/examples/responders/src/tests.rs new file mode 100644 index 00000000..17f43bef --- /dev/null +++ b/examples/responders/src/tests.rs @@ -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::().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(), "I'm here"); + + // 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)).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); +} diff --git a/examples/msgpack/Cargo.toml b/examples/serialization/Cargo.toml similarity index 70% rename from examples/msgpack/Cargo.toml rename to examples/serialization/Cargo.toml index 91d689bc..1ebe072f 100644 --- a/examples/msgpack/Cargo.toml +++ b/examples/serialization/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "msgpack" +name = "serialization" version = "0.0.0" workspace = "../" edition = "2018" @@ -7,9 +7,9 @@ publish = false [dependencies] rocket = { path = "../../core/lib" } -serde = { version = "1.0", features = ["derive"] } +serde = "1" [dependencies.rocket_contrib] path = "../../contrib/lib" default-features = false -features = ["msgpack"] +features = ["json", "msgpack"] diff --git a/examples/serialization/src/json.rs b/examples/serialization/src/json.rs new file mode 100644 index 00000000..ac898c51 --- /dev/null +++ b/examples/serialization/src/json.rs @@ -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>; +type Messages<'r> = State<'r, MessageList>; + +#[derive(Serialize, Deserialize)] +struct Message<'r> { + id: Option, + message: Cow<'r, str> +} + +#[post("/", format = "json", data = "")] +async fn new(message: Json>, 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("/", format = "json", data = "")] +async fn update(id: Id, message: Json>, list: Messages<'_>) -> Option { + match list.lock().await.get_mut(id) { + Some(existing) => { + *existing = message.message.to_string(); + Some(json!({ "status": "ok" })) + } + None => None + } +} + +#[get("/", format = "json")] +async fn get<'r>(id: Id, list: Messages<'r>) -> Option>> { + 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![])) + }) +} diff --git a/examples/serialization/src/main.rs b/examples/serialization/src/main.rs new file mode 100644 index 00000000..80fcbdf1 --- /dev/null +++ b/examples/serialization/src/main.rs @@ -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()) +} diff --git a/examples/msgpack/src/main.rs b/examples/serialization/src/msgpack.rs similarity index 50% rename from examples/msgpack/src/main.rs rename to examples/serialization/src/msgpack.rs index e086aa75..cf2be837 100644 --- a/examples/msgpack/src/main.rs +++ b/examples/serialization/src/msgpack.rs @@ -1,7 +1,3 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - use rocket_contrib::msgpack::MsgPack; use serde::{Serialize, Deserialize}; @@ -14,15 +10,16 @@ struct Message<'r> { #[get("/", format = "msgpack")] fn get(id: usize) -> MsgPack> { - MsgPack(Message { id: id, contents: "Hello, world!", }) + MsgPack(Message { id, contents: "Hello, world!", }) } #[post("/", data = "", format = "msgpack")] -fn create(data: MsgPack>) -> String { - data.contents.to_string() +fn echo<'r>(data: MsgPack>) -> &'r str { + data.contents } -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/message", routes![get, create]) +pub fn stage() -> rocket::fairing::AdHoc { + rocket::fairing::AdHoc::on_launch("MessagePack", |rocket| async { + rocket.mount("/msgpack", routes![echo, get]) + }) } diff --git a/examples/serialization/src/tests.rs b/examples/serialization/src/tests.rs new file mode 100644 index 00000000..07ea172d --- /dev/null +++ b/examples/serialization/src/tests.rs @@ -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())); +} diff --git a/examples/session/Cargo.toml b/examples/session/Cargo.toml deleted file mode 100644 index cb00be11..00000000 --- a/examples/session/Cargo.toml +++ /dev/null @@ -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"] diff --git a/examples/session/src/tests.rs b/examples/session/src/tests.rs deleted file mode 100644 index 51ace29f..00000000 --- a/examples/session/src/tests.rs +++ /dev/null @@ -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> { - 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> { - 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")); -} diff --git a/examples/state/Cargo.toml b/examples/state/Cargo.toml index e2d6d515..41117c6e 100644 --- a/examples/state/Cargo.toml +++ b/examples/state/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] rocket = { path = "../../core/lib" } +flume = "0.10" diff --git a/examples/state/src/main.rs b/examples/state/src/main.rs index a0e72ba9..f253b94c 100644 --- a/examples/state/src/main.rs +++ b/examples/state/src/main.rs @@ -2,29 +2,14 @@ #[cfg(test)] mod tests; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use rocket::State; -use rocket::response::content; - -struct HitCount(AtomicUsize); - -#[get("/")] -fn index(hit_count: State<'_, HitCount>) -> content::Html { - 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!("{}

{}", msg, count)) -} - -#[get("/count")] -fn count(hit_count: State<'_, HitCount>) -> String { - hit_count.0.load(Ordering::Relaxed).to_string() -} +mod request_local; +mod managed_hit_count; +mod managed_queue; #[launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { rocket::ignite() - .mount("/", routes![index, count]) - .manage(HitCount(AtomicUsize::new(0))) + .attach(request_local::stage()) + .attach(managed_hit_count::stage()) + .attach(managed_queue::stage()) } diff --git a/examples/state/src/managed_hit_count.rs b/examples/state/src/managed_hit_count.rs new file mode 100644 index 00000000..468269a5 --- /dev/null +++ b/examples/state/src/managed_hit_count.rs @@ -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 { + let count = hit_count.0.fetch_add(1, Ordering::Relaxed) + 1; + content::Html(format!("Your visit is recorded!

Visits: {}", count)) +} + +pub fn stage() -> AdHoc { + AdHoc::on_launch("Managed Hit Count", |rocket| async { + rocket.mount("/count", routes![index]) + .manage(HitCount(AtomicUsize::new(0))) + }) +} diff --git a/examples/state/src/managed_queue.rs b/examples/state/src/managed_queue.rs new file mode 100644 index 00000000..999cd9d7 --- /dev/null +++ b/examples/state/src/managed_queue.rs @@ -0,0 +1,25 @@ +use rocket::State; +use rocket::fairing::AdHoc; +use rocket::http::Status; + +struct Tx(flume::Sender); +struct Rx(flume::Receiver); + +#[put("/push?")] +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 { + 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)) + }) +} diff --git a/examples/request_local_state/src/main.rs b/examples/state/src/request_local.rs similarity index 58% rename from examples/request_local_state/src/main.rs rename to examples/state/src/request_local.rs index 3b5e4bfb..3791caf1 100644 --- a/examples/request_local_state/src/main.rs +++ b/examples/state/src/request_local.rs @@ -1,17 +1,14 @@ -#[macro_use] extern crate rocket; - use std::sync::atomic::{AtomicUsize, Ordering}; use rocket::State; use rocket::outcome::Outcome; use rocket::request::{self, FromRequest, Request}; +use rocket::fairing::AdHoc; -#[cfg(test)] mod tests; - -#[derive(Default)] -struct Atomics { - uncached: AtomicUsize, - cached: AtomicUsize, +#[derive(Default, Debug)] +pub struct Atomics { + pub uncached: AtomicUsize, + pub cached: AtomicUsize, } struct Guard1; @@ -24,9 +21,14 @@ impl<'r> FromRequest<'r> for Guard1 { type Error = (); async fn from_request(req: &'r Request<'_>) -> request::Outcome { + rocket::info_!("-- 1 --"); + let atomics = try_outcome!(req.guard::>().await); atomics.uncached.fetch_add(1, Ordering::Relaxed); - req.local_cache(|| atomics.cached.fetch_add(1, Ordering::Relaxed)); + req.local_cache(|| { + rocket::info_!("1: populating cache!"); + atomics.cached.fetch_add(1, Ordering::Relaxed) + }); Outcome::Success(Guard1) } @@ -37,6 +39,8 @@ impl<'r> FromRequest<'r> for Guard2 { type Error = (); async fn from_request(req: &'r Request<'_>) -> request::Outcome { + rocket::info_!("-- 2 --"); + try_outcome!(req.guard::().await); Outcome::Success(Guard2) } @@ -47,9 +51,12 @@ impl<'r> FromRequest<'r> for Guard3 { type Error = (); async fn from_request(req: &'r Request<'_>) -> request::Outcome { + rocket::info_!("-- 3 --"); + let atomics = try_outcome!(req.guard::>().await); atomics.uncached.fetch_add(1, Ordering::Relaxed); req.local_cache_async(async { + rocket::info_!("3: populating cache!"); atomics.cached.fetch_add(1, Ordering::Relaxed) }).await; @@ -62,24 +69,37 @@ impl<'r> FromRequest<'r> for Guard4 { type Error = (); async fn from_request(req: &'r Request<'_>) -> request::Outcome { + rocket::info_!("-- 4 --"); + try_outcome!(Guard3::from_request(req).await); Outcome::Success(Guard4) } } -#[get("/sync")] -fn r_sync(_g1: Guard1, _g2: Guard2) { - // This exists only to run the request guards. +#[get("/1-2")] +fn one_two(_g1: Guard1, _g2: Guard2, state: State<'_, Atomics>) -> String { + format!("{:#?}", state) } -#[get("/async")] -async fn r_async(_g1: Guard3, _g2: Guard4) { - // This exists only to run the request guards. +#[get("/3-4")] +fn three_four(_g3: Guard3, _g4: Guard4, state: State<'_, Atomics>) -> String { + format!("{:#?}", state) } -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .manage(Atomics::default()) - .mount("/", routes![r_sync, r_async]) +#[get("/1-2-3-4")] +fn all( + _g1: Guard1, + _g2: Guard2, + _g3: Guard3, + _g4: Guard4, + state: State<'_, Atomics> +) -> String { + format!("{:#?}", state) +} + +pub fn stage() -> AdHoc { + AdHoc::on_launch("Request Local State", |rocket| async { + rocket.manage(Atomics::default()) + .mount("/req-local", routes![one_two, three_four, all]) + }) } diff --git a/examples/state/src/tests.rs b/examples/state/src/tests.rs index b7d942b4..0a3debb7 100644 --- a/examples/state/src/tests.rs +++ b/examples/state/src/tests.rs @@ -1,39 +1,20 @@ use rocket::local::blocking::Client; use rocket::http::Status; -fn register_hit(client: &Client) { - let response = client.get("/").dispatch(); - assert_eq!(response.status(), Status::Ok); -} - -fn get_count(client: &Client) -> usize { - let response = client.get("/count").dispatch(); - response.into_string().and_then(|s| s.parse().ok()).unwrap() -} - #[test] fn test_count() { let client = Client::tracked(super::rocket()).unwrap(); - // Count should start at 0. - assert_eq!(get_count(&client), 0); + fn get_count(client: &Client) -> usize { + let response = client.get("/count").dispatch().into_string().unwrap(); + let count = response.split(" ").last().unwrap(); + count.parse().unwrap() + } - for _ in 0..99 { register_hit(&client); } - assert_eq!(get_count(&client), 99); - - register_hit(&client); - assert_eq!(get_count(&client), 100); -} - -#[test] -fn test_raw_state_count() { - use rocket::State; - use super::{count, index}; - - let rocket = super::rocket(); - assert_eq!(count(State::from(&rocket).unwrap()), "0"); - assert!(index(State::from(&rocket).unwrap()).0.contains("Visits: 1")); - assert_eq!(count(State::from(&rocket).unwrap()), "1"); + // Count starts at 0; our hit is the first. + for i in 1..128 { + assert_eq!(get_count(&client), i); + } } // Cargo runs each test in parallel on different threads. We use all of these @@ -47,3 +28,43 @@ fn test_raw_state_count() { #[test] fn test_count_parallel_7() { test_count() } #[test] fn test_count_parallel_8() { test_count() } #[test] fn test_count_parallel_9() { test_count() } + +#[test] +fn test_queue_push_pop() { + let client = Client::tracked(super::rocket()).unwrap(); + + let response = client.put("/queue/push?event=test1").dispatch(); + assert_eq!(response.status(), Status::Ok); + + let response = client.get("/queue/pop").dispatch(); + assert_eq!(response.into_string().unwrap(), "test1"); + + client.put("/queue/push?event=POP!%20...goes+").dispatch(); + client.put("/queue/push?event=the+weasel").dispatch(); + let r1 = client.get("/queue/pop").dispatch().into_string().unwrap(); + let r2 = client.get("/queue/pop").dispatch().into_string().unwrap(); + assert_eq!(r1 + &r2, "POP! ...goes the weasel"); +} + +#[test] +fn test_request_local_state() { + use super::request_local::Atomics; + use std::sync::atomic::Ordering; + + let client = Client::tracked(super::rocket()).unwrap(); + + client.get("/req-local/1-2").dispatch(); + let atomics = client.rocket().state::().unwrap(); + assert_eq!(atomics.uncached.load(Ordering::Relaxed), 2); + assert_eq!(atomics.cached.load(Ordering::Relaxed), 1); + + client.get("/req-local/1-2").dispatch(); + let atomics = client.rocket().state::().unwrap(); + assert_eq!(atomics.uncached.load(Ordering::Relaxed), 4); + assert_eq!(atomics.cached.load(Ordering::Relaxed), 2); + + client.get("/req-local/1-2-3-4").dispatch(); + let atomics = client.rocket().state::().unwrap(); + assert_eq!(atomics.uncached.load(Ordering::Relaxed), 8); + assert_eq!(atomics.cached.load(Ordering::Relaxed), 3); +} diff --git a/examples/static_files/Cargo.toml b/examples/static-files/Cargo.toml similarity index 89% rename from examples/static_files/Cargo.toml rename to examples/static-files/Cargo.toml index d104792f..5e05c0ac 100644 --- a/examples/static_files/Cargo.toml +++ b/examples/static-files/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "static_files" +name = "static-files" version = "0.0.0" workspace = "../" edition = "2018" diff --git a/examples/static_files/src/main.rs b/examples/static-files/src/main.rs similarity index 100% rename from examples/static_files/src/main.rs rename to examples/static-files/src/main.rs diff --git a/examples/static_files/src/tests.rs b/examples/static-files/src/tests.rs similarity index 100% rename from examples/static_files/src/tests.rs rename to examples/static-files/src/tests.rs diff --git a/examples/static_files/static/hidden/hi.txt b/examples/static-files/static/hidden/hi.txt similarity index 100% rename from examples/static_files/static/hidden/hi.txt rename to examples/static-files/static/hidden/hi.txt diff --git a/examples/static_files/static/hidden/index.html b/examples/static-files/static/hidden/index.html similarity index 100% rename from examples/static_files/static/hidden/index.html rename to examples/static-files/static/hidden/index.html diff --git a/examples/static_files/static/index.html b/examples/static-files/static/index.html similarity index 100% rename from examples/static_files/static/index.html rename to examples/static-files/static/index.html diff --git a/examples/static_files/static/rocket-icon.jpg b/examples/static-files/static/rocket-icon.jpg similarity index 100% rename from examples/static_files/static/rocket-icon.jpg rename to examples/static-files/static/rocket-icon.jpg diff --git a/examples/stream/Cargo.toml b/examples/stream/Cargo.toml deleted file mode 100644 index 88d96f05..00000000 --- a/examples/stream/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "stream" -version = "0.0.0" -workspace = "../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } diff --git a/examples/stream/src/main.rs b/examples/stream/src/main.rs deleted file mode 100644 index a0b15432..00000000 --- a/examples/stream/src/main.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - -use rocket::response::{content, Stream}; - -use rocket::tokio::fs::File; -use rocket::tokio::io::{repeat, AsyncRead, AsyncReadExt}; - -// Generate this file using: head -c BYTES /dev/random > big_file.dat -const FILENAME: &str = "big_file.dat"; - -#[get("/")] -fn root() -> content::Plain> { - content::Plain(Stream::from(repeat('a' as u8).take(25000))) -} - -#[get("/big_file")] -async fn file() -> Option> { - File::open(FILENAME).await.map(Stream::from).ok() -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/", routes![root, file]) -} diff --git a/examples/stream/src/tests.rs b/examples/stream/src/tests.rs deleted file mode 100644 index 6fe44846..00000000 --- a/examples/stream/src/tests.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::fs::{self, File}; -use std::io::prelude::*; - -use rocket::local::blocking::Client; - -#[test] -fn test_root() { - let client = Client::tracked(super::rocket()).unwrap(); - let res = client.get("/").dispatch(); - - // Check that we have exactly 25,000 'a'. - let res_str = res.into_string().unwrap(); - assert_eq!(res_str.len(), 25000); - for byte in res_str.as_bytes() { - assert_eq!(*byte, b'a'); - } -} - -#[test] -fn test_file() { - // Create the 'big_file' - const CONTENTS: &str = "big_file contents...not so big here"; - let mut file = File::create(super::FILENAME).expect("create big_file"); - file.write_all(CONTENTS.as_bytes()).expect("write to big_file"); - - // Get the big file contents, hopefully. - let client = Client::tracked(super::rocket()).unwrap(); - let res = client.get("/big_file").dispatch(); - assert_eq!(res.into_string(), Some(CONTENTS.into())); - - // Delete the 'big_file'. - fs::remove_file(super::FILENAME).expect("remove big_file"); -} diff --git a/examples/tera_templates/Cargo.toml b/examples/templating/Cargo.toml similarity index 65% rename from examples/tera_templates/Cargo.toml rename to examples/templating/Cargo.toml index 3126422f..20b83d8f 100644 --- a/examples/tera_templates/Cargo.toml +++ b/examples/templating/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tera_templates" +name = "templating" version = "0.0.0" workspace = "../" edition = "2018" @@ -13,4 +13,5 @@ serde_json = "1.0" [dependencies.rocket_contrib] path = "../../contrib/lib" default-features = false -features = ["tera_templates"] +# in your application, you should enable only the template engine(s) used +features = ["tera_templates", "handlebars_templates"] diff --git a/examples/templating/src/hbs.rs b/examples/templating/src/hbs.rs new file mode 100644 index 00000000..ba94f0ea --- /dev/null +++ b/examples/templating/src/hbs.rs @@ -0,0 +1,65 @@ +use rocket::Request; +use rocket::response::Redirect; +use rocket_contrib::templates::{Template, handlebars}; +use self::handlebars::{Handlebars, JsonRender}; + +#[derive(serde::Serialize)] +struct TemplateContext<'r> { + title: &'r str, + name: Option<&'r str>, + items: Vec<&'r str>, + // This special key tells handlebars which template is the parent. + parent: &'static str, +} + +#[get("/")] +pub fn index() -> Redirect { + Redirect::to(uri!("/hbs", hello: name = "Your Name")) +} + +#[get("/hello/")] +pub fn hello(name: &str) -> Template { + Template::render("hbs/index", &TemplateContext { + title: "Hello", + name: Some(name), + items: vec!["One", "Two", "Three"], + parent: "hbs/layout", + }) +} + +#[get("/about")] +pub fn about() -> Template { + Template::render("hbs/about", &TemplateContext { + title: "About", + name: None, + items: vec!["Some", "Important", "Info"], + parent: "hbs/layout", + }) +} + +#[catch(404)] +pub fn not_found(req: &Request<'_>) -> Template { + let mut map = std::collections::HashMap::new(); + map.insert("path", req.uri().path()); + Template::render("hbs/error/404", &map) +} + +fn wow_helper( + h: &handlebars::Helper<'_, '_>, + _: &handlebars::Handlebars, + _: &handlebars::Context, + _: &mut handlebars::RenderContext<'_, '_>, + out: &mut dyn handlebars::Output +) -> handlebars::HelperResult { + if let Some(param) = h.param(0) { + out.write("")?; + out.write(¶m.value().render())?; + out.write("")?; + } + + Ok(()) +} + +pub fn customize(hbs: &mut Handlebars) { + hbs.register_helper("wow", Box::new(wow_helper)); +} diff --git a/examples/templating/src/main.rs b/examples/templating/src/main.rs new file mode 100644 index 00000000..06b2b58c --- /dev/null +++ b/examples/templating/src/main.rs @@ -0,0 +1,28 @@ +#[macro_use] extern crate rocket; + +mod hbs; +mod tera; + +#[cfg(test)] mod tests; + +use rocket::response::content::Html; +use rocket_contrib::templates::Template; + +#[get("/")] +fn index() -> Html<&'static str> { + Html(r#"See Tera or Handlebars."#) +} + +#[launch] +fn rocket() -> _ { + rocket::ignite() + .mount("/", routes![index]) + .mount("/tera", routes![tera::index, tera::hello]) + .mount("/hbs", routes![hbs::index, hbs::hello, hbs::about]) + .register("/hbs", catchers![hbs::not_found]) + .register("/tera", catchers![tera::not_found]) + .attach(Template::custom(|engines| { + hbs::customize(&mut engines.handlebars); + tera::customize(&mut engines.tera); + })) +} diff --git a/examples/templating/src/tera.rs b/examples/templating/src/tera.rs new file mode 100644 index 00000000..a4271157 --- /dev/null +++ b/examples/templating/src/tera.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use rocket::Request; +use rocket::response::Redirect; +use rocket_contrib::templates::{Template, tera::Tera}; + +#[derive(serde::Serialize)] +struct TemplateContext<'r> { + title: &'r str, + name: &'r str, + items: Vec<&'r str> +} + +#[get("/")] +pub fn index() -> Redirect { + Redirect::to(uri!("/tera", hello: name = "Your Name")) +} + +#[get("/hello/")] +pub fn hello(name: &str) -> Template { + Template::render("tera/index", &TemplateContext { + name, + title: "Hello", + items: vec!["One", "Two", "Three"], + }) +} + +#[catch(404)] +pub fn not_found(req: &Request<'_>) -> Template { + let mut map = HashMap::new(); + map.insert("path", req.uri().path()); + Template::render("tera/error/404", &map) +} + +pub fn customize(_tera: &mut Tera) { + /* register helpers, and so on */ +} diff --git a/examples/templating/src/tests.rs b/examples/templating/src/tests.rs new file mode 100644 index 00000000..50b44be9 --- /dev/null +++ b/examples/templating/src/tests.rs @@ -0,0 +1,85 @@ +use super::rocket; + +use rocket::http::{RawStr, Status, Method::*}; +use rocket::local::blocking::Client; +use rocket_contrib::templates::Template; + +fn test_root(kind: &str) { + // Check that the redirect works. + let client = Client::tracked(rocket()).unwrap(); + for method in &[Get, Head] { + let response = client.req(*method, format!("/{}", kind)).dispatch(); + assert_eq!(response.status(), Status::SeeOther); + assert!(response.body().is_none()); + + let location = response.headers().get_one("Location").unwrap(); + assert_eq!(location, format!("/{}/hello/Your%20Name", kind)); + } + + // Check that other request methods are not accepted (and instead caught). + for method in &[Post, Put, Delete, Options, Trace, Connect, Patch] { + let mut map = std::collections::HashMap::new(); + map.insert("path", format!("/{}", kind)); + let expected = Template::show(client.rocket(), format!("{}/error/404", kind), &map); + + let response = client.req(*method, format!("/{}", kind)).dispatch(); + assert_eq!(response.status(), Status::NotFound); + assert_eq!(response.into_string(), expected); + } +} + +fn test_name(base: &str) { + // Check that the /hello/ route works. + let client = Client::tracked(rocket()).unwrap(); + let response = client.get(format!("/{}/hello/Jack%20Daniels", base)).dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(response.into_string().unwrap().contains("Hi Jack Daniels!")); +} + +fn test_404(base: &str) { + // Check that the error catcher works. + let client = Client::tracked(rocket()).unwrap(); + for bad_path in &["/hello", "/foo/bar", "/404"] { + let path = format!("/{}{}", base, bad_path); + let escaped_path = RawStr::new(&path).html_escape(); + + let response = client.get(&path).dispatch(); + assert_eq!(response.status(), Status::NotFound); + let response = dbg!(response.into_string().unwrap()); + + assert!(response.contains(base)); + assert! { + response.contains(&format!("{} does not exist", path)) + || response.contains(&format!("{} does not exist", escaped_path)) + }; + } +} + +fn test_about(base: &str) { + let client = Client::tracked(rocket()).unwrap(); + let response = client.get(format!("/{}/about", base)).dispatch(); + assert!(response.into_string().unwrap().contains("About - Here's another page!")); +} + +#[test] +fn test_index() { + let client = Client::tracked(rocket()).unwrap(); + let response = client.get("/").dispatch().into_string().unwrap(); + assert!(response.contains("Tera")); + assert!(response.contains("Handlebars")); +} + +#[test] +fn hbs() { + test_root("hbs"); + test_name("hbs"); + test_404("hbs"); + test_about("hbs"); +} + +#[test] +fn tera() { + test_root("tera"); + test_name("tera"); + test_404("tera"); +} diff --git a/examples/handlebars_templates/templates/about.hbs b/examples/templating/templates/hbs/about.hbs similarity index 79% rename from examples/handlebars_templates/templates/about.hbs rename to examples/templating/templates/hbs/about.hbs index 7ba705c2..45c042c2 100644 --- a/examples/handlebars_templates/templates/about.hbs +++ b/examples/templating/templates/hbs/about.hbs @@ -1,7 +1,7 @@ {{#*inline "page"}}
-

Here's another page!

+

About - Here's another page!

    {{#each items}}
  • {{ this }}
  • diff --git a/examples/handlebars_templates/templates/error/404.hbs b/examples/templating/templates/hbs/error/404.hbs similarity index 86% rename from examples/handlebars_templates/templates/error/404.hbs rename to examples/templating/templates/hbs/error/404.hbs index d1c0b363..81fbc477 100644 --- a/examples/handlebars_templates/templates/error/404.hbs +++ b/examples/templating/templates/hbs/error/404.hbs @@ -2,7 +2,7 @@ - 404 + 404 - hbs

    404: Hey! There's nothing here.

    diff --git a/examples/templating/templates/hbs/footer.hbs b/examples/templating/templates/hbs/footer.hbs new file mode 100644 index 00000000..98266dbe --- /dev/null +++ b/examples/templating/templates/hbs/footer.hbs @@ -0,0 +1,3 @@ + diff --git a/examples/handlebars_templates/templates/index.hbs b/examples/templating/templates/hbs/index.hbs similarity index 79% rename from examples/handlebars_templates/templates/index.hbs rename to examples/templating/templates/hbs/index.hbs index 9d4f5985..f5b778b8 100644 --- a/examples/handlebars_templates/templates/index.hbs +++ b/examples/templating/templates/hbs/index.hbs @@ -11,7 +11,7 @@
-

Try going to /hello/YourName.

+

Try going to /hbs/hello/Your Name.

Also, check {{ wow "this" }} (custom helper) out!

diff --git a/examples/handlebars_templates/templates/layout.hbs b/examples/templating/templates/hbs/layout.hbs similarity index 77% rename from examples/handlebars_templates/templates/layout.hbs rename to examples/templating/templates/hbs/layout.hbs index a3707108..6d00b44c 100644 --- a/examples/handlebars_templates/templates/layout.hbs +++ b/examples/templating/templates/hbs/layout.hbs @@ -4,8 +4,8 @@ Rocket Example - {{ title }} - {{> nav}} + {{> hbs/nav}} {{~> page}} - {{> footer}} + {{> hbs/footer}} diff --git a/examples/templating/templates/hbs/nav.hbs b/examples/templating/templates/hbs/nav.hbs new file mode 100644 index 00000000..1872c4bd --- /dev/null +++ b/examples/templating/templates/hbs/nav.hbs @@ -0,0 +1 @@ +Hello | About diff --git a/examples/tera_templates/templates/base.html.tera b/examples/templating/templates/tera/base.html.tera similarity index 61% rename from examples/tera_templates/templates/base.html.tera rename to examples/templating/templates/tera/base.html.tera index 822ecb1b..8f3db199 100644 --- a/examples/tera_templates/templates/base.html.tera +++ b/examples/templating/templates/tera/base.html.tera @@ -2,9 +2,12 @@ - Tera Demo + Tera Demo - {{ title }} {% block content %}{% endblock content %} + diff --git a/examples/tera_templates/templates/error/404.html.tera b/examples/templating/templates/tera/error/404.html.tera similarity index 86% rename from examples/tera_templates/templates/error/404.html.tera rename to examples/templating/templates/tera/error/404.html.tera index d1c0b363..748f1754 100644 --- a/examples/tera_templates/templates/error/404.html.tera +++ b/examples/templating/templates/tera/error/404.html.tera @@ -2,7 +2,7 @@ - 404 + 404 - tera

404: Hey! There's nothing here.

diff --git a/examples/tera_templates/templates/index.html.tera b/examples/templating/templates/tera/index.html.tera similarity index 56% rename from examples/tera_templates/templates/index.html.tera rename to examples/templating/templates/tera/index.html.tera index d0c46826..a43b5d03 100644 --- a/examples/tera_templates/templates/index.html.tera +++ b/examples/templating/templates/tera/index.html.tera @@ -1,7 +1,7 @@ -{% extends "base" %} +{% extends "tera/base" %} {% block content %} -

Hi {{name}}

+

Hi {{ name }}!

Here are your items:

    {% for s in items %} @@ -9,5 +9,5 @@ {% endfor %}
-

Try going to /hello/YourName

+

Try going to /tera/hello/Your Name

{% endblock content %} diff --git a/examples/tera_templates/Rocket.toml b/examples/tera_templates/Rocket.toml deleted file mode 100644 index fcb67324..00000000 --- a/examples/tera_templates/Rocket.toml +++ /dev/null @@ -1,2 +0,0 @@ -[global] -template_dir = "templates/" diff --git a/examples/tera_templates/src/main.rs b/examples/tera_templates/src/main.rs deleted file mode 100644 index 0ca25378..00000000 --- a/examples/tera_templates/src/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - -use std::collections::HashMap; - -use rocket::Request; -use rocket::response::Redirect; -use rocket_contrib::templates::Template; - -#[derive(serde::Serialize)] -struct TemplateContext { - name: String, - items: Vec<&'static str> -} - -#[get("/")] -fn index() -> Redirect { - Redirect::to(uri!(get: name = "Unknown")) -} - -#[get("/hello/")] -fn get(name: String) -> Template { - let context = TemplateContext { name, items: vec!["One", "Two", "Three"] }; - Template::render("index", &context) -} - -#[catch(404)] -fn not_found(req: &Request<'_>) -> Template { - let mut map = HashMap::new(); - map.insert("path", req.uri().path()); - Template::render("error/404", &map) -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .mount("/", routes![index, get]) - .attach(Template::fairing()) - .register("/", catchers![not_found]) -} diff --git a/examples/tera_templates/src/tests.rs b/examples/tera_templates/src/tests.rs deleted file mode 100644 index 05f386b7..00000000 --- a/examples/tera_templates/src/tests.rs +++ /dev/null @@ -1,67 +0,0 @@ -use super::rocket; -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/ route works. - dispatch!(Get, "/hello/Jack", |client, response| { - let context = super::TemplateContext { - name: "Jack".into(), - items: vec!["One", "Two", "Three"] - }; - - 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)); - }); -} diff --git a/examples/testing/src/async_required.rs b/examples/testing/src/async_required.rs index a9f68fd7..b8fe12ae 100644 --- a/examples/testing/src/async_required.rs +++ b/examples/testing/src/async_required.rs @@ -13,7 +13,7 @@ pub fn rocket() -> rocket::Rocket { rocket::ignite() .mount("/", routes![rendezvous]) .attach(AdHoc::on_launch("Add Channel", |rocket| async { - Ok(rocket.manage(Barrier::new(2))) + rocket.manage(Barrier::new(2)) })) } diff --git a/examples/testing/src/main.rs b/examples/testing/src/main.rs index 1576a7e4..68b87011 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -8,7 +8,7 @@ fn hello() -> &'static str { } #[launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { async_required::rocket().mount("/", routes![hello]) } diff --git a/examples/tls/Rocket.toml b/examples/tls/Rocket.toml index 30791b31..0a1a0827 100644 --- a/examples/tls/Rocket.toml +++ b/examples/tls/Rocket.toml @@ -1,9 +1,10 @@ -# The certificate/private key pairs used here where generated via openssl using -# the 'gen_certs.sh' script located in the `private/` subdirectory. +# The certificate key pairs used here were generated with openssl via the +# 'private/gen_certs.sh' script. # -# The certificates are self-signed. As such, you will need to trust them directly -# for your browser to refer to the connection as secure. You should NEVER use +# These certificates are self-signed. As such, you will need to trust them +# directly for your browser to show connections as secure. You should NEVER use # these certificate/key pairs. They are here for DEMONSTRATION PURPOSES ONLY. + [default.tls] certs = "private/rsa_sha256_cert.pem" key = "private/rsa_sha256_key.pem" diff --git a/examples/tls/src/main.rs b/examples/tls/src/main.rs index 0d210d84..48b5b4b1 100644 --- a/examples/tls/src/main.rs +++ b/examples/tls/src/main.rs @@ -8,6 +8,7 @@ fn hello() -> &'static str { } #[launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { + // See `Rocket.toml` and `Cargo.toml` for TLS configuration. rocket::ignite().mount("/", routes![hello]) } diff --git a/examples/todo/Rocket.toml b/examples/todo/Rocket.toml index cfd6c2d1..48620aa1 100644 --- a/examples/todo/Rocket.toml +++ b/examples/todo/Rocket.toml @@ -1,5 +1,5 @@ -[global] +[default] template_dir = "static" -[global.databases.sqlite_database] +[default.databases.sqlite_database] url = "db/db.sqlite" diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs index 287c9115..f946bbc0 100644 --- a/examples/todo/src/main.rs +++ b/examples/todo/src/main.rs @@ -23,25 +23,25 @@ pub struct DbConn(diesel::SqliteConnection); #[derive(Debug, serde::Serialize)] struct Context { - msg: Option<(String, String)>, + flash: Option<(String, String)>, tasks: Vec } impl Context { pub async fn err(conn: &DbConn, msg: M) -> Context { Context { - msg: Some(("error".into(), msg.to_string())), + flash: Some(("error".into(), msg.to_string())), tasks: Task::all(conn).await.unwrap_or_default() } } - pub async fn raw(conn: &DbConn, msg: Option<(String, String)>) -> Context { + pub async fn raw(conn: &DbConn, flash: Option<(String, String)>) -> Context { match Task::all(conn).await { - Ok(tasks) => Context { msg, tasks }, + Ok(tasks) => Context { flash, tasks }, Err(e) => { error_!("DB Task::all() error: {}", e); Context { - msg: Some(("error".into(), "Fail to access database.".into())), + flash: Some(("error".into(), "Fail to access database.".into())), tasks: vec![] } } @@ -85,25 +85,21 @@ async fn delete(id: i32, conn: DbConn) -> Result, Template> { } #[get("/")] -async fn index(msg: Option>, conn: DbConn) -> Template { - let msg = msg.map(|m| (m.name().to_string(), m.msg().to_string())); - Template::render("index", Context::raw(&conn, msg).await) +async fn index(flash: Option>, conn: DbConn) -> Template { + let flash = flash.map(FlashMessage::into_inner); + Template::render("index", Context::raw(&conn, flash).await) } -async fn run_db_migrations(rocket: Rocket) -> Result { +async fn run_migrations(rocket: Rocket) -> Rocket { // This macro from `diesel_migrations` defines an `embedded_migrations` // module containing a function named `run`. This allows the example to be // run and tested without any outside setup of the database. embed_migrations!(); let conn = DbConn::get_one(&rocket).await.expect("database connection"); - match conn.run(|c| embedded_migrations::run(c)).await { - Ok(()) => Ok(rocket), - Err(e) => { - error!("Failed to run database migrations: {:?}", e); - Err(rocket) - } - } + conn.run(|c| embedded_migrations::run(c)).await.expect("can run migrations"); + + rocket } #[launch] @@ -111,8 +107,8 @@ fn rocket() -> Rocket { rocket::ignite() .attach(DbConn::fairing()) .attach(Template::fairing()) - .attach(AdHoc::on_launch("Database Migrations", run_db_migrations)) - .mount("/", StaticFiles::from(crate_relative!("/static"))) + .attach(AdHoc::on_launch("Run Migrations", run_migrations)) + .mount("/", StaticFiles::from(crate_relative!("static"))) .mount("/", routes![index]) .mount("/todo", routes![new, toggle, delete]) } diff --git a/examples/todo/src/tests.rs b/examples/todo/src/tests.rs index 37d506a9..286f73ae 100644 --- a/examples/todo/src/tests.rs +++ b/examples/todo/src/tests.rs @@ -1,6 +1,5 @@ use super::task::Task; -use parking_lot::Mutex; use rand::{Rng, thread_rng, distributions::Alphanumeric}; use rocket::local::asynchronous::Client; @@ -9,7 +8,7 @@ use rocket::http::{Status, ContentType}; // We use a lock to synchronize between tests so DB operations don't collide. // For now. In the future, we'll have a nice way to run each test in a DB // transaction so we can regain concurrency. -static DB_LOCK: Mutex<()> = parking_lot::const_mutex(()); +static DB_LOCK: parking_lot::Mutex<()> = parking_lot::const_mutex(()); macro_rules! run_test { (|$client:ident, $conn:ident| $block:expr) => ({ @@ -104,7 +103,12 @@ fn test_many_insertions() { for i in 0..ITER { // Issue a request to insert a new task with a random description. - let desc: String = thread_rng().sample_iter(&Alphanumeric).take(12).map(char::from).collect(); + let desc: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(12) + .map(char::from) + .collect(); + client.post("/todo") .header(ContentType::Form) .body(format!("description={}", desc)) diff --git a/examples/uuid/src/main.rs b/examples/uuid/src/main.rs index 253d5b2a..9a6e38fa 100644 --- a/examples/uuid/src/main.rs +++ b/examples/uuid/src/main.rs @@ -24,7 +24,7 @@ fn people(id: Uuid, people: State) -> Result { } #[launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { let mut map = HashMap::new(); map.insert("7f205202-7ba1-4c39-b2fc-3e630722bf9f".parse().unwrap(), "Lacy"); map.insert("4da34121-bc7d-4fc1-aee6-bf8de0795333".parse().unwrap(), "Bob"); diff --git a/site/guide/3-overview.md b/site/guide/3-overview.md index fc29bc32..2f555d1d 100644 --- a/site/guide/3-overview.md +++ b/site/guide/3-overview.md @@ -87,8 +87,7 @@ routing and error handling. anywhere in your application without importing them explicitly. You may instead prefer to import macros explicitly or refer to them with - absolute paths: `use rocket::get;` or `#[rocket::get]`. The [`hello_2018` - example](@example/hello_2018) showcases this alternative. + absolute paths: `use rocket::get;` or `#[rocket::get]`. ## Mounting @@ -194,9 +193,9 @@ as we expected. ! note: This and other examples are on GitHub. - A version of this example's complete crate, ready to `cargo run`, can be found - on [GitHub](@example/hello_world). You can find dozens of other complete - examples, spanning all of Rocket's features, in the [GitHub examples + An expanded version of this example's complete crate, ready to `cargo run`, + can be found on [GitHub](@example/hello). You can find dozens of other + complete examples, spanning all of Rocket's features, in the [GitHub examples directory](@example/). The second approach uses the `#[rocket::main]` route attribute. @@ -222,10 +221,10 @@ async fn main() { `#[rocket::main]` is useful when a handle to the `Future` returned by `launch()` is desired, or when the return value of [`launch()`] is to be inspected. The -[errors example] for instance, inspects the return value. +[error handling example] for instance, inspects the return value. [`launch()`]: @api/rocket/struct.Rocket.html#method.launch -[errors example]: @example/errors +[error handling example]: @example/error-handling ## Futures and Async diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 09d3de04..cc2095b9 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -1860,8 +1860,8 @@ Rocket provides a built-in default catcher. It produces HTML or JSON, depending on the value of the `Accept` header. As such, custom catchers only need to be registered for custom error handling. -The [error catcher example](@example/errors) illustrates catcher use in full, -while the [`Catcher`] API documentation provides further details. +The [error handling example](@example/error-handling) illustrates catcher use in +full, while the [`Catcher`] API documentation provides further details. [`catch`]: @api/rocket/attr.catch.html [`register()`]: @api/rocket/struct.Rocket.html#method.register diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index fbeca0a5..c8cda1e9 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -348,12 +348,12 @@ The `Json` type serializes the structure into JSON, sets the Content-Type to JSON, and emits the serialized data in a fixed-sized body. If serialization fails, a **500 - Internal Server Error** is returned. -The [JSON example on GitHub] provides further illustration. +The [serialization example] provides further illustration. [`Json`]: @api/rocket_contrib/json/struct.Json.html [`Serialize`]: https://docs.serde.rs/serde/trait.Serialize.html [`serde`]: https://docs.serde.rs/serde/ -[JSON example on GitHub]: @example/json +[serialization example]: @example/serialization ## Templates @@ -423,9 +423,8 @@ reloading is disabled. The [`Template`] API documentation contains more information about templates, including how to customize a template engine to add custom helpers and filters. -The [Handlebars templates example](@example/handlebars_templates) is a -fully composed application that makes use of Handlebars templates, while the -[Tera templates example](@example/tera_templates) does the same for Tera. +The [templating example](@example/templating) uses both Tera and Handlebars +templating to implement the same application. [`Template`]: @api/rocket_contrib/templates/struct.Template.html [configurable]: ../configuration/#extras diff --git a/site/guide/6-state.md b/site/guide/6-state.md index a4490795..6b1f5039 100644 --- a/site/guide/6-state.md +++ b/site/guide/6-state.md @@ -325,7 +325,9 @@ async fn get_logs(conn: LogsDbConn, id: usize) -> Logs { syntax. Rocket does not provide an ORM. It is up to you to decide how to model your application's data. -! note + + +! note: Rocket wraps synchronous databases in an `async` API. The database engines supported by `#[database]` are *synchronous*. Normally, using such a database would block the thread of execution. To prevent this, @@ -344,6 +346,10 @@ postgres = { version = "0.15", features = ["with-chrono"] } ``` For more on Rocket's built-in database support, see the -[`rocket_contrib::databases`] module documentation. +[`rocket_contrib::databases`] module documentation. For examples of CRUD-like +"blog" JSON APIs backed by a SQLite database driven by each of `sqlx`, `diesel`, +and `rusqlite` with migrations run automatically for the former two drivers and +`contrib` database support use for the latter two drivers, see the [databases +example](@example/databases). [`rocket_contrib::databases`]: @api/rocket_contrib/databases/index.html