Skip to content

Commit 7f0f7a0

Browse files
committed
Test all the things. Here's a WIP CHANGELOG:
Routing: * Unicode characters are accepted anywhere in route paths. (#998) * Dyanmic query values can (and must) be any `FromForm` type. The `Form` type is no longer useable in any query parameter type. Capped * A new `Capped` type is used to indicate when data has been truncated due to incoming data limits. It allows checking whether data is complete or truncated. `DataStream` methods returns `Capped` types. * Several `Capped<T>` types implement `FromData`, `FromForm`. * HTTP 413 (Payload Too Large) errors are now returned when the data limit is exceeded. (resolves #972) Hierarchical Limits * Data limits are now hierarchical, delimited with `/`. A limit of `a/b/c` falls back to `a/b` then `a` when not set. Temporary Files * A new `TempFile` data and form guard allows streaming data directly to a file which can then be persisted. * A new `temp_dir` config parameter specifies where to store `TempFile`. * The limits `file` and `file/$ext`, where `$ext` is the file extension, determines the data limit for a `TempFile`. Forms Revamp * All form related types now reside in a new `form` module. * Multipart forms are supported. (resolves #106) * Collections are supported in body forms and queries. (resolves #205) * Nested forms and structures are supported. (resolves #313) * Form fields can be ad-hoc validated with `#[field(value = expr)]`. Core: * `&RawStr` no longer implements `FromParam`. * `&str` implements `FromParam`, `FromData`, `FromForm`. * `FromTransformedData` was removed. * `FromData` gained a lifetime for use with request-local data. * All dynamic paramters in a query string must typecheck as `FromForm`. * `FromFormValue` removed in favor of `FromFormField`. * Dyanmic paramters, form values are always percent-decoded. * The default error HTML is more compact. * `&Config` is a request guard. * The `DataStream` interface was entirely revamped. * `State` is only exported via `rocket::State`. * A `request::local_cache!()` macro was added for storing values in request-local cache without consideration for type uniqueness by using a locally generated anonymous type. * `Request::get_param()` is now `Request::param()`. * `Request::get_segments()` is now `Request::segments()`, takes a range. * `Request::get_query_value()` is now `Request::query_value()`, can parse any `FromForm` including sequences. * `std::io::Error` implements `Responder` as `Debug<std::io::Error>`. * `(Status, R)` where `R: Responder` implements `Responder` by setting overriding the `Status` of `R`. * The name of a route is printed first during route matching. * `FlashMessage` now only has one lifetime generic. HTTP: * `RawStr` implements `serde::{Serialize, Deserialize}`. * `RawStr` implements _many_ more methods, in particular, those related to the `Pattern` API. * `RawStr::from_str()` is now `RawStr::new()`. * `RawStr::url_decode()` and `RawStr::url_decode_lossy()` only allocate as necessary, return `Cow`. * `(Status, R)` where `R: Responder` is a responder that overwrites the status of `R` to `Status`. * `Status` implements `Default` with `Status::Ok`. * `Status` implements `PartialEq`, `Eq`, `Hash`, `PartialOrd`, `Ord`. * Authority and origin part of `Absolute` can be modified with new `Absolute::{with,set}_authority()`, `Absolute::{with,set}_origin()` methods. * `Origin::segments()` was removed in favor of methods split into query and path parts and into raw and decoded parts. * The `Segments` iterator is signficantly smarter. Returns `&str`. * `Segments::into_path_buf()` is now `Segments::to_path_buf()`, doesn't consume. * A new `QuerySegments` is the analogous query segment iterator. * Once set, the `expires` field on private cookies is not overwritten. (resolves #1506) * `Origin::path()` and `Origin::query()` return `&RawStr`, not `&str`. Codegen: * Preserve more spans in `uri!` macro. * `FromFormValue` derive removed; `FromFormField` added. * The `form` `FromForm` and `FromFormField` field attribute is now named `field`. `#[form(field = ..)]` is now `#[form(name = ..)]`. Examples: * `form_validation` and `form_kitchen_sink` removed in favor of `forms` * `rocket_contrib::Json` implements `FromForm`. * The `json!` macro is exported as `rocket_contrib::json::json`. * `rocket_contrib::MsgPack` implements `FromForm`. * Added clarifying docs to `StaticFiles`. * The `hello_world` example uses unicode in paths. Internal: * Codegen uses new `exports` module with the following conventions: - Locals starts with `__` and are lowercased. - Rocket modules start with `_` are are lowercased. - Stdlib types start with `_` are are titlecased. - Rocket types are titlecased. * A `header` module was added to `http`, contains header types. * `SAFETY` is used as doc-string keyword for `unsafe` related comments. * The `Uri` parser no longer recognizes Rocket route URIs.
1 parent e1d8a6e commit 7f0f7a0

File tree

29 files changed

+555
-831
lines changed

29 files changed

+555
-831
lines changed

contrib/lib/src/json.rs

+32-5
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,21 @@ pub use serde_json::{json_internal, json_internal_vec};
3434
///
3535
/// ## Receiving JSON
3636
///
37-
/// If you're receiving JSON data, simply add a `data` parameter to your route
38-
/// arguments and ensure the type of the parameter is a `Json<T>`, where `T` is
39-
/// some type you'd like to parse from JSON. `T` must implement [`Deserialize`]
40-
/// from [`serde`]. The data is parsed from the HTTP request body.
37+
/// `Json` is both a data guard and a form guard.
38+
///
39+
/// ### Data Guard
40+
///
41+
/// To parse request body data as JSON , add a `data` route argument with a
42+
/// target type of `Json<T>`, where `T` is some type you'd like to parse from
43+
/// JSON. `T` must implement [`serde::Deserialize`].
4144
///
4245
/// ```rust
4346
/// # #[macro_use] extern crate rocket;
4447
/// # extern crate rocket_contrib;
4548
/// # type User = usize;
4649
/// use rocket_contrib::json::Json;
4750
///
48-
/// #[post("/users", format = "json", data = "<user>")]
51+
/// #[post("/user", format = "json", data = "<user>")]
4952
/// fn new_user(user: Json<User>) {
5053
/// /* ... */
5154
/// }
@@ -56,6 +59,30 @@ pub use serde_json::{json_internal, json_internal_vec};
5659
/// "application/json" as its `Content-Type` header value will not be routed to
5760
/// the handler.
5861
///
62+
/// ### Form Guard
63+
///
64+
/// `Json<T>`, as a form guard, accepts value and data fields and parses the
65+
/// data as a `T`. Simple use `Json<T>`:
66+
///
67+
/// ```rust
68+
/// # #[macro_use] extern crate rocket;
69+
/// # extern crate rocket_contrib;
70+
/// # type Metadata = usize;
71+
/// use rocket::form::{Form, FromForm};
72+
/// use rocket_contrib::json::Json;
73+
///
74+
/// #[derive(FromForm)]
75+
/// struct User<'r> {
76+
/// name: &'r str,
77+
/// metadata: Json<Metadata>
78+
/// }
79+
///
80+
/// #[post("/user", data = "<form>")]
81+
/// fn new_user(form: Form<User<'_>>) {
82+
/// /* ... */
83+
/// }
84+
/// ```
85+
///
5986
/// ## Sending JSON
6087
///
6188
/// If you're responding with JSON data, return a `Json<T>` type, where `T`

contrib/lib/src/msgpack.rs

+31-5
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ pub use rmp_serde::decode::Error;
3434
///
3535
/// ## Receiving MessagePack
3636
///
37-
/// If you're receiving MessagePack data, simply add a `data` parameter to your
38-
/// route arguments and ensure the type of the parameter is a `MsgPack<T>`,
39-
/// where `T` is some type you'd like to parse from MessagePack. `T` must
40-
/// implement [`Deserialize`] from [`serde`]. The data is parsed from the HTTP
41-
/// request body.
37+
/// `MsgPack` is both a data guard and a form guard.
38+
///
39+
/// ### Data Guard
40+
///
41+
/// To parse request body data as MessagePack , add a `data` route argument with
42+
/// a target type of `MsgPack<T>`, where `T` is some type you'd like to parse
43+
/// from JSON. `T` must implement [`serde::Deserialize`].
4244
///
4345
/// ```rust
4446
/// # #[macro_use] extern crate rocket;
@@ -57,6 +59,30 @@ pub use rmp_serde::decode::Error;
5759
/// "application/msgpack" as its first `Content-Type:` header parameter will not
5860
/// be routed to this handler.
5961
///
62+
/// ### Form Guard
63+
///
64+
/// `MsgPack<T>`, as a form guard, accepts value and data fields and parses the
65+
/// data as a `T`. Simple use `MsgPack<T>`:
66+
///
67+
/// ```rust
68+
/// # #[macro_use] extern crate rocket;
69+
/// # extern crate rocket_contrib;
70+
/// # type Metadata = usize;
71+
/// use rocket::form::{Form, FromForm};
72+
/// use rocket_contrib::msgpack::MsgPack;
73+
///
74+
/// #[derive(FromForm)]
75+
/// struct User<'r> {
76+
/// name: &'r str,
77+
/// metadata: MsgPack<Metadata>
78+
/// }
79+
///
80+
/// #[post("/users", data = "<form>")]
81+
/// fn new_user(form: Form<User<'_>>) {
82+
/// /* ... */
83+
/// }
84+
/// ```
85+
///
6086
/// ## Sending MessagePack
6187
///
6288
/// If you're responding with MessagePack data, return a `MsgPack<T>` type,

contrib/lib/src/uuid.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::str::FromStr;
2121
use std::ops::Deref;
2222

2323
use rocket::request::FromParam;
24-
use rocket::form::{self, FromFormField, Errors, ValueField};
24+
use rocket::form::{self, FromFormField, ValueField};
2525

2626
type ParseError = <self::uuid_crate::Uuid as FromStr>::Err;
2727

@@ -110,7 +110,7 @@ impl<'a> FromParam<'a> for Uuid {
110110
}
111111

112112
impl<'v> FromFormField<'v> for Uuid {
113-
fn from_value(field: ValueField<'v>) -> Result<Self, Errors<'v>> {
113+
fn from_value(field: ValueField<'v>) -> form::Result<'v, Self> {
114114
Ok(field.value.parse().map_err(form::error::Error::custom)?)
115115
}
116116
}

core/codegen/src/attribute/route.rs

+20-6
Original file line numberDiff line numberDiff line change
@@ -194,16 +194,27 @@ fn data_expr(ident: &syn::Ident, ty: &syn::Type) -> TokenStream {
194194
}
195195

196196
fn query_exprs(route: &Route) -> Option<TokenStream> {
197-
use devise::ext::Split6;
197+
use devise::ext::{Split2, Split6};
198198

199199
define_spanned_export!(Span::call_site() =>
200200
__req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None
201201
);
202202

203203
let query_segments = route.attribute.path.query.as_ref()?;
204204

205-
// NOTE: We only care about dynamic parameters since the router will only
206-
// send us request where the static parameters match.
205+
// Record all of the static parameters for later filtering.
206+
let (raw_name, raw_value) = query_segments.iter()
207+
.filter(|s| !s.is_dynamic())
208+
.map(|s| {
209+
let name = s.name.name();
210+
match name.find('=') {
211+
Some(i) => (&name[..i], &name[i + 1..]),
212+
None => (name, "")
213+
}
214+
})
215+
.split2();
216+
217+
// Now record all of the dynamic parameters.
207218
let (name, matcher, ident, init_expr, push_expr, finalize_expr) = query_segments.iter()
208219
.filter(|s| s.is_dynamic())
209220
.map(|s| (s, s.name.name(), route.find_input(&s.name).expect("dynamic has input")))
@@ -235,9 +246,12 @@ fn query_exprs(route: &Route) -> Option<TokenStream> {
235246
#(let mut #ident = #init_expr;)*
236247

237248
for _f in #__req.query_fields() {
238-
match _f.name.key_lossy().as_str() {
239-
// FIXME: Need to skip raw so we don't push into trailing.
240-
#(#matcher => #push_expr,)*
249+
let _raw = (_f.name.source().as_str(), _f.value);
250+
let _key = _f.name.key_lossy().as_str();
251+
match (_raw, _key) {
252+
// Skip static parameters so <param..> doesn't see them.
253+
#(((#raw_name, #raw_value), _) => { /* skip */ },)*
254+
#((_, #matcher) => #push_expr,)*
241255
_ => { /* in case we have no trailing, ignore all else */ },
242256
}
243257
}

core/codegen/tests/route-data.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@ use rocket::form::Form;
99
// Test that the data parameters works as expected.
1010

1111
#[derive(FromForm)]
12-
struct Inner {
13-
field: String
12+
struct Inner<'r> {
13+
field: &'r str
1414
}
1515

16-
struct Simple(String);
16+
struct Simple<'r>(&'r str);
1717

1818
#[async_trait]
19-
impl<'r> FromData<'r> for Simple {
19+
impl<'r> FromData<'r> for Simple<'r> {
2020
type Error = std::io::Error;
2121

2222
async fn from_data(req: &'r Request<'_>, data: Data) -> data::Outcome<Self, Self::Error> {
23-
String::from_data(req, data).await.map(Simple)
23+
<&'r str>::from_data(req, data).await.map(Simple)
2424
}
2525
}
2626

2727
#[post("/f", data = "<form>")]
28-
fn form(form: Form<Inner>) -> String { form.into_inner().field }
28+
fn form<'r>(form: Form<Inner<'r>>) -> &'r str { form.into_inner().field }
2929

3030
#[post("/s", data = "<simple>")]
31-
fn simple(simple: Simple) -> String { simple.0 }
31+
fn simple<'r>(simple: Simple<'r>) -> &'r str { simple.0 }
3232

3333
#[test]
3434
fn test_data() {

core/codegen/tests/route.rs

+148
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,151 @@ mod scopes {
148148
rocket::ignite().mount("/", rocket::routes![hello, world])
149149
}
150150
}
151+
152+
use rocket::form::Contextual;
153+
154+
#[derive(Default, Debug, PartialEq, FromForm)]
155+
struct Filtered<'r> {
156+
bird: Option<&'r str>,
157+
color: Option<&'r str>,
158+
cat: Option<&'r str>,
159+
rest: Option<&'r str>,
160+
}
161+
162+
#[get("/?bird=1&color=blue&<bird>&<color>&cat=bob&<rest..>")]
163+
fn filtered_raw_query(bird: usize, color: &str, rest: Contextual<'_, Filtered<'_>>) -> String {
164+
assert_ne!(bird, 1);
165+
assert_ne!(color, "blue");
166+
assert_eq!(rest.value.unwrap(), Filtered::default());
167+
168+
format!("{} - {}", bird, color)
169+
}
170+
171+
#[test]
172+
fn test_filtered_raw_query() {
173+
let rocket = rocket::ignite().mount("/", routes![filtered_raw_query]);
174+
let client = Client::untracked(rocket).unwrap();
175+
176+
#[track_caller]
177+
fn run(client: &Client, birds: &[&str], colors: &[&str], cats: &[&str]) -> (Status, String) {
178+
let join = |slice: &[&str], name: &str| slice.iter()
179+
.map(|v| format!("{}={}", name, v))
180+
.collect::<Vec<_>>()
181+
.join("&");
182+
183+
let q = format!("{}&{}&{}",
184+
join(birds, "bird"),
185+
join(colors, "color"),
186+
join(cats, "cat"));
187+
188+
let response = client.get(format!("/?{}", q)).dispatch();
189+
let status = response.status();
190+
let body = response.into_string().unwrap();
191+
192+
(status, body)
193+
}
194+
195+
let birds = &["2", "3"];
196+
let colors = &["red", "blue", "green"];
197+
let cats = &["bob", "bob"];
198+
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
199+
200+
let birds = &["2", "1", "3"];
201+
let colors = &["red", "green"];
202+
let cats = &["bob", "bob"];
203+
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
204+
205+
let birds = &["2", "1", "3"];
206+
let colors = &["red", "blue", "green"];
207+
let cats = &[];
208+
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
209+
210+
let birds = &["2", "1", "3"];
211+
let colors = &["red", "blue", "green"];
212+
let cats = &["bob", "bob"];
213+
assert_eq!(run(&client, birds, colors, cats).1, "2 - red");
214+
215+
let birds = &["1", "2", "1", "3"];
216+
let colors = &["blue", "red", "blue", "green"];
217+
let cats = &["bob"];
218+
assert_eq!(run(&client, birds, colors, cats).1, "2 - red");
219+
220+
let birds = &["5", "1"];
221+
let colors = &["blue", "orange", "red", "blue", "green"];
222+
let cats = &["bob"];
223+
assert_eq!(run(&client, birds, colors, cats).1, "5 - orange");
224+
}
225+
226+
#[derive(Debug, PartialEq, FromForm)]
227+
struct Dog<'r> {
228+
name: &'r str,
229+
age: usize
230+
}
231+
232+
#[derive(Debug, PartialEq, FromForm)]
233+
struct Q<'r> {
234+
dog: Dog<'r>
235+
}
236+
237+
#[get("/?<color>&color=red&<q..>")]
238+
fn query_collection(color: Vec<&str>, q: Q<'_>) -> String {
239+
format!("{} - {} - {}", color.join("&"), q.dog.name, q.dog.age)
240+
}
241+
242+
#[get("/?<color>&color=red&<dog>")]
243+
fn query_collection_2(color: Vec<&str>, dog: Dog<'_>) -> String {
244+
format!("{} - {} - {}", color.join("&"), dog.name, dog.age)
245+
}
246+
247+
#[test]
248+
fn test_query_collection() {
249+
#[track_caller]
250+
fn run(client: &Client, colors: &[&str], dog: &[&str]) -> (Status, String) {
251+
let join = |slice: &[&str], prefix: &str| slice.iter()
252+
.map(|v| format!("{}{}", prefix, v))
253+
.collect::<Vec<_>>()
254+
.join("&");
255+
256+
let q = format!("{}&{}", join(colors, "color="), join(dog, "dog."));
257+
let response = client.get(format!("/?{}", q)).dispatch();
258+
(response.status(), response.into_string().unwrap())
259+
}
260+
261+
fn run_tests(rocket: rocket::Rocket) {
262+
let client = Client::untracked(rocket).unwrap();
263+
264+
let colors = &["blue", "green"];
265+
let dog = &["name=Fido", "age=10"];
266+
assert_eq!(run(&client, colors, dog).0, Status::NotFound);
267+
268+
let colors = &["red"];
269+
let dog = &["name=Fido"];
270+
assert_eq!(run(&client, colors, dog).0, Status::NotFound);
271+
272+
let colors = &["red"];
273+
let dog = &["name=Fido", "age=2"];
274+
assert_eq!(run(&client, colors, dog).1, " - Fido - 2");
275+
276+
let colors = &["red", "blue", "green"];
277+
let dog = &["name=Fido", "age=10"];
278+
assert_eq!(run(&client, colors, dog).1, "blue&green - Fido - 10");
279+
280+
let colors = &["red", "blue", "green"];
281+
let dog = &["name=Fido", "age=10", "toy=yes"];
282+
assert_eq!(run(&client, colors, dog).1, "blue&green - Fido - 10");
283+
284+
let colors = &["blue", "red", "blue"];
285+
let dog = &["name=Fido", "age=10"];
286+
assert_eq!(run(&client, colors, dog).1, "blue&blue - Fido - 10");
287+
288+
let colors = &["blue", "green", "red", "blue"];
289+
let dog = &["name=Max+Fido", "age=10"];
290+
assert_eq!(run(&client, colors, dog).1, "blue&green&blue - Max Fido - 10");
291+
}
292+
293+
let rocket = rocket::ignite().mount("/", routes![query_collection]);
294+
run_tests(rocket);
295+
296+
let rocket = rocket::ignite().mount("/", routes![query_collection_2]);
297+
run_tests(rocket);
298+
}

0 commit comments

Comments
 (0)