Rocket/core/codegen/tests/route.rs
Sergio Benitez 78e2f8a3c9 Revamp codegen, fixing inconscpicuous bugs.
This commit completely revamps the way that codegen handles route URI
"parameters". The changes are largely internal. In summary, codegen code
is better organized, better written, and less subject to error.

There are three breaking changes:
  * `path` is now `uri` in `route` attribute: `#[route(GET, path = "..")]`
    becomes `#[route(GET, uri = "..")]`.
  * the order of execution for path and query guards relative to
    each-other is now unspecified
  * URI normalization now normalizes the query part as well.

Several error messages were improved. A couple of bugs were fixed:
  * Prior to this commit, Rocket would optimistically try to parse every
    segment of a URI as an ident, in case one was needed in the future.
    A bug in rustc results in codegen "panicking" if the segment
    couldn't _lex_ as an ident. This panic didn't manifest until far
    after expansion, unfortunately. This wasn't a problem before as we
    only allowed ident-like segments (ASCII), but now that we allow any
    UTF-8, the bug surfaced. This was fixed by never attempting to parse
    non-idents as idents.
  * Prior to this commit, it was impossible to generate typed URIs for
    paths that ignored path parameters via the recently added syntax
    `<_>`: the macro would panic. This was fixed by, well, handling
    these ignored parameters.

Some minor additions:
  * Added `RawStr::find()`, expanding its `Pattern`-based API.
  * Added an internal mechanism to dynamically determine if a `UriPart`
    is `Path` or `Query`.
2021-03-04 02:01:25 -08:00

299 lines
8.7 KiB
Rust

// Rocket sometimes generates mangled identifiers that activate the
// non_snake_case lint. We deny the lint in this test to ensure that
// code generation uses #[allow(non_snake_case)] in the appropriate places.
#![deny(non_snake_case)]
#[macro_use] extern crate rocket;
use std::path::PathBuf;
use rocket::request::Request;
use rocket::http::ext::Normalize;
use rocket::local::blocking::Client;
use rocket::data::{self, Data, FromData};
use rocket::http::{Status, RawStr, ContentType};
// Use all of the code generation available at once.
#[derive(FromForm, UriDisplayQuery)]
struct Inner<'r> {
field: &'r str
}
struct Simple(String);
#[async_trait]
impl<'r> FromData<'r> for Simple {
type Error = std::io::Error;
async fn from_data(req: &'r Request<'_>, data: Data) -> data::Outcome<Self, Self::Error> {
String::from_data(req, data).await.map(Simple)
}
}
#[post(
"/<a>/<name>/name/<path..>?sky=blue&<sky>&<query..>",
format = "json",
data = "<simple>",
rank = 138
)]
fn post1(
sky: usize,
name: &str,
a: String,
query: Inner<'_>,
path: PathBuf,
simple: Simple,
) -> String {
let string = format!("{}, {}, {}, {}, {}, {}",
sky, name, a, query.field, path.normalized_str(), simple.0);
let uri = uri!(post1: a, name, path, sky, query);
format!("({}) ({})", string, uri.to_string())
}
#[route(
POST,
uri = "/<a>/<name>/name/<path..>?sky=blue&<sky>&<query..>",
format = "json",
data = "<simple>",
rank = 138
)]
fn post2(
sky: usize,
name: &str,
a: String,
query: Inner<'_>,
path: PathBuf,
simple: Simple,
) -> String {
let string = format!("{}, {}, {}, {}, {}, {}",
sky, name, a, query.field, path.normalized_str(), simple.0);
let uri = uri!(post2: a, name, path, sky, query);
format!("({}) ({})", string, uri.to_string())
}
#[allow(dead_code)]
#[post("/<_unused_param>?<_unused_query>", data="<_unused_data>")]
fn test_unused_params(_unused_param: String, _unused_query: String, _unused_data: Data) {
}
#[test]
fn test_full_route() {
let rocket = rocket::ignite()
.mount("/1", routes![post1])
.mount("/2", routes![post2]);
let client = Client::tracked(rocket).unwrap();
let a = RawStr::new("A%20A");
let name = RawStr::new("Bob%20McDonald");
let path = "this/path/here";
let sky = 777;
let query = "field=inside";
let simple = "data internals";
let path_part = format!("/{}/{}/name/{}", a, name, path);
let query_part = format!("?sky={}&sky=blue&{}", sky, query);
let uri = format!("{}{}", path_part, query_part);
let expected_uri = format!("{}?sky=blue&sky={}&{}", path_part, sky, query);
let response = client.post(&uri).body(simple).dispatch();
assert_eq!(response.status(), Status::NotFound);
let response = client.post(format!("/1{}", uri)).body(simple).dispatch();
assert_eq!(response.status(), Status::NotFound);
let response = client
.post(format!("/1{}", uri))
.header(ContentType::JSON)
.body(simple)
.dispatch();
assert_eq!(response.into_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})",
sky, name.percent_decode().unwrap(), "A A", "inside", path, simple, expected_uri));
let response = client.post(format!("/2{}", uri)).body(simple).dispatch();
assert_eq!(response.status(), Status::NotFound);
let response = client
.post(format!("/2{}", uri))
.header(ContentType::JSON)
.body(simple)
.dispatch();
assert_eq!(response.into_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})",
sky, name.percent_decode().unwrap(), "A A", "inside", path, simple, expected_uri));
}
mod scopes {
mod other {
#[get("/world")]
pub fn world() -> &'static str {
"Hello, world!"
}
}
#[get("/hello")]
pub fn hello() -> &'static str {
"Hello, outside world!"
}
use other::world;
fn _rocket() -> rocket::Rocket {
rocket::ignite().mount("/", rocket::routes![hello, world])
}
}
use rocket::form::Contextual;
#[derive(Default, Debug, PartialEq, FromForm)]
struct Filtered<'r> {
bird: Option<&'r str>,
color: Option<&'r str>,
cat: Option<&'r str>,
rest: Option<&'r str>,
}
#[get("/?bird=1&color=blue&<bird>&<color>&cat=bob&<rest..>")]
fn filtered_raw_query(bird: usize, color: &str, rest: Contextual<'_, Filtered<'_>>) -> String {
assert_ne!(bird, 1);
assert_ne!(color, "blue");
assert_eq!(rest.value.unwrap(), Filtered::default());
format!("{} - {}", bird, color)
}
#[test]
fn test_filtered_raw_query() {
let rocket = rocket::ignite().mount("/", routes![filtered_raw_query]);
let client = Client::untracked(rocket).unwrap();
#[track_caller]
fn run(client: &Client, birds: &[&str], colors: &[&str], cats: &[&str]) -> (Status, String) {
let join = |slice: &[&str], name: &str| slice.iter()
.map(|v| format!("{}={}", name, v))
.collect::<Vec<_>>()
.join("&");
let q = format!("{}&{}&{}",
join(birds, "bird"),
join(colors, "color"),
join(cats, "cat"));
let response = client.get(format!("/?{}", q)).dispatch();
let status = response.status();
let body = response.into_string().unwrap();
(status, body)
}
let birds = &["2", "3"];
let colors = &["red", "blue", "green"];
let cats = &["bob", "bob"];
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
let birds = &["2", "1", "3"];
let colors = &["red", "green"];
let cats = &["bob", "bob"];
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
let birds = &["2", "1", "3"];
let colors = &["red", "blue", "green"];
let cats = &[];
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
let birds = &["2", "1", "3"];
let colors = &["red", "blue", "green"];
let cats = &["bob", "bob"];
assert_eq!(run(&client, birds, colors, cats).1, "2 - red");
let birds = &["1", "2", "1", "3"];
let colors = &["blue", "red", "blue", "green"];
let cats = &["bob"];
assert_eq!(run(&client, birds, colors, cats).1, "2 - red");
let birds = &["5", "1"];
let colors = &["blue", "orange", "red", "blue", "green"];
let cats = &["bob"];
assert_eq!(run(&client, birds, colors, cats).1, "5 - orange");
}
#[derive(Debug, PartialEq, FromForm)]
struct Dog<'r> {
name: &'r str,
age: usize
}
#[derive(Debug, PartialEq, FromForm)]
struct Q<'r> {
dog: Dog<'r>
}
#[get("/?<color>&color=red&<q..>")]
fn query_collection(color: Vec<&str>, q: Q<'_>) -> String {
format!("{} - {} - {}", color.join("&"), q.dog.name, q.dog.age)
}
#[get("/?<color>&color=red&<dog>")]
fn query_collection_2(color: Vec<&str>, dog: Dog<'_>) -> String {
format!("{} - {} - {}", color.join("&"), dog.name, dog.age)
}
#[test]
fn test_query_collection() {
#[track_caller]
fn run(client: &Client, colors: &[&str], dog: &[&str]) -> (Status, String) {
let join = |slice: &[&str], prefix: &str| slice.iter()
.map(|v| format!("{}{}", prefix, v))
.collect::<Vec<_>>()
.join("&");
let q = format!("{}&{}", join(colors, "color="), join(dog, "dog."));
let response = client.get(format!("/?{}", q)).dispatch();
(response.status(), response.into_string().unwrap())
}
fn run_tests(rocket: rocket::Rocket) {
let client = Client::untracked(rocket).unwrap();
let colors = &["blue", "green"];
let dog = &["name=Fido", "age=10"];
assert_eq!(run(&client, colors, dog).0, Status::NotFound);
let colors = &["red"];
let dog = &["name=Fido"];
assert_eq!(run(&client, colors, dog).0, Status::NotFound);
let colors = &["red"];
let dog = &["name=Fido", "age=2"];
assert_eq!(run(&client, colors, dog).1, " - Fido - 2");
let colors = &["red", "blue", "green"];
let dog = &["name=Fido", "age=10"];
assert_eq!(run(&client, colors, dog).1, "blue&green - Fido - 10");
let colors = &["red", "blue", "green"];
let dog = &["name=Fido", "age=10", "toy=yes"];
assert_eq!(run(&client, colors, dog).1, "blue&green - Fido - 10");
let colors = &["blue", "red", "blue"];
let dog = &["name=Fido", "age=10"];
assert_eq!(run(&client, colors, dog).1, "blue&blue - Fido - 10");
let colors = &["blue", "green", "red", "blue"];
let dog = &["name=Max+Fido", "age=10"];
assert_eq!(run(&client, colors, dog).1, "blue&green&blue - Max Fido - 10");
}
let rocket = rocket::ignite().mount("/", routes![query_collection]);
run_tests(rocket);
let rocket = rocket::ignite().mount("/", routes![query_collection_2]);
run_tests(rocket);
}