diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index 6e4c2a9f..ececce10 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -23,6 +23,7 @@ fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path { }) } +// FIXME: I think we also want the attributes here. fn media_type_to_expr(ecx: &ExtCtxt, ct: Option) -> Option> { ct.map(|ct| { let (top, sub) = (ct.top().as_str(), ct.sub().as_str()); diff --git a/codegen/src/parser/route.rs b/codegen/src/parser/route.rs index 01182f13..dc3f92b0 100644 --- a/codegen/src/parser/route.rs +++ b/codegen/src/parser/route.rs @@ -258,7 +258,7 @@ fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned) -> isize { fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned) -> MediaType { if let LitKind::Str(ref s, _) = *kv.value() { - if let Ok(ct) = MediaType::from_str(&s.as_str()) { + if let Some(ct) = MediaType::parse_flexible(&s.as_str()) { if !ct.is_known() { let msg = format!("'{}' is not a known media type", s); ecx.span_warn(kv.value.span, &msg); @@ -273,7 +273,8 @@ fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned) -> MediaType { ecx.struct_span_err(kv.span, r#"`format` must be a "media/type""#) .help(r#"format, if specified, must be a key-value pair where the key is `format` and the value is a string representing the - media type accepted. e.g: format = "application/json""#) + media type accepted. e.g: format = "application/json". + shorthand is also accepted: format = "json"#) .emit(); MediaType::Any diff --git a/codegen/tests/compile-fail/unknown-media-type.rs b/codegen/tests/compile-fail/unknown-media-type.rs index 1744aef6..eb2c4148 100644 --- a/codegen/tests/compile-fail/unknown-media-type.rs +++ b/codegen/tests/compile-fail/unknown-media-type.rs @@ -1,3 +1,5 @@ +// must-compile-successfully + #![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] @@ -12,7 +14,4 @@ fn two() -> &'static str { "hi" } #[get("/", format = "x-custom/x-custom")] //~ WARNING not a known media type fn three() -> &'static str { "hi" } -// Make the test fail here so we can actually check for the warnings above. -assert!(false); - fn main() { } diff --git a/codegen/tests/ui/typed-uri-bad-type.stderr b/codegen/tests/ui/typed-uri-bad-type.stderr index 28ca9637..710790e7 100644 --- a/codegen/tests/ui/typed-uri-bad-type.stderr +++ b/codegen/tests/ui/typed-uri-bad-type.stderr @@ -30,4 +30,4 @@ error[E0277]: the trait bound `S: rocket::http::uri::FromUriParam<_>` is not sat error: aborting due to 4 previous errors -If you want more information on this error, try using "rustc --explain E0277" +For more information about this error, try `rustc --explain E0277`. diff --git a/examples/content_types/src/main.rs b/examples/content_types/src/main.rs index 020f19b4..eeac18bb 100644 --- a/examples/content_types/src/main.rs +++ b/examples/content_types/src/main.rs @@ -19,8 +19,9 @@ struct Person { // 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. -#[get("//", format = "application/json")] +// 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) -> content::Json { // In a real application, we'd use the JSON contrib type. let person = Person { name: name, age: age, }; @@ -29,7 +30,7 @@ fn get_hello(name: String, age: u8) -> content::Json { // In a `POST` request and all other payload supporting request types, the // content type is matched against the `format` in the route attribute. -#[post("/", format = "text/plain", data = "")] +#[post("/", format = "plain", data = "")] fn post_hello(age: u8, name: String) -> content::Json { let person = Person { name: name, age: age, }; content::Json(serde_json::to_string(&person).unwrap()) diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 4ec7e1b3..500d93c6 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -25,7 +25,7 @@ struct Message { } // TODO: This example can be improved by using `route` with multiple HTTP verbs. -#[post("/", format = "application/json", data = "")] +#[post("/", format = "json", data = "")] fn new(id: ID, message: Json, map: State) -> JsonValue { let mut hashmap = map.lock().expect("map lock."); if hashmap.contains_key(&id) { @@ -39,7 +39,7 @@ fn new(id: ID, message: Json, map: State) -> JsonValue { } } -#[put("/", format = "application/json", data = "")] +#[put("/", format = "json", data = "")] fn update(id: ID, message: Json, map: State) -> Option { let mut hashmap = map.lock().unwrap(); if hashmap.contains_key(&id) { @@ -50,7 +50,7 @@ fn update(id: ID, message: Json, map: State) -> Option", format = "application/json")] +#[get("/", format = "json")] fn get(id: ID, map: State) -> Option> { let hashmap = map.lock().unwrap(); hashmap.get(&id).map(|contents| { diff --git a/examples/msgpack/src/main.rs b/examples/msgpack/src/main.rs index aa750550..1ff64694 100644 --- a/examples/msgpack/src/main.rs +++ b/examples/msgpack/src/main.rs @@ -15,7 +15,7 @@ struct Message { contents: String } -#[get("/", format = "application/msgpack")] +#[get("/", format = "msgpack")] fn get(id: usize) -> MsgPack { MsgPack(Message { id: id, @@ -23,7 +23,7 @@ fn get(id: usize) -> MsgPack { }) } -#[post("/", data = "", format = "application/msgpack")] +#[post("/", data = "", format = "msgpack")] fn create(data: MsgPack) -> Result { Ok(data.into_inner().contents) } diff --git a/examples/raw_upload/src/main.rs b/examples/raw_upload/src/main.rs index 0fef0dbf..2a9ac7a3 100644 --- a/examples/raw_upload/src/main.rs +++ b/examples/raw_upload/src/main.rs @@ -8,7 +8,7 @@ extern crate rocket; use std::io; use rocket::Data; -#[post("/upload", format = "text/plain", data = "")] +#[post("/upload", format = "plain", data = "")] fn upload(data: Data) -> io::Result { data.stream_to_file("/tmp/upload.txt").map(|n| n.to_string()) } diff --git a/lib/src/docify.rs b/lib/src/docify.rs new file mode 100644 index 00000000..c38d894f --- /dev/null +++ b/lib/src/docify.rs @@ -0,0 +1,76 @@ +macro_rules! docify { + ([$($doc:tt)*]; $($tt:tt)*) => { + docify!([$($doc)*] [] $($tt)*); + }; + + // FIXME: Treat $a just like everywhere else. What if we start with @[]? + ([$a:tt $($b:tt)*] [] $($tt:tt)*) => { + docify!([$($b)*] [stringify!($a), " "] $($tt)*); + }; + + ([@fence @$lang:tt $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, "\n\n```", stringify!($lang), "\n"] $($tt)*); + }; + + ([@fence $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, "\n\n```\n"] $($tt)*); + }; + + ([@{$($a:tt),*}! $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, $($a),*] $($tt)*); + }; + + ([@{$($a:tt),*} $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, $($a),*, " "] $($tt)*); + }; + + ([@code{$($a:tt)+}! $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, "`", $(stringify!($a)),*, "`"] $($tt)*); + }; + + ([@code{$($a:tt)+} $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, "`", $(stringify!($a)),*, "` "] $($tt)*); + }; + + ([@[$($a:tt)*]! $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, $(stringify!($a)),*] $($tt)*); + }; + + ([@[$($a:tt)*] $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, $(stringify!($a)),*, " "] $($tt)*); + }; + + ([@nl $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, "\n"] $($tt)*); + }; + + (@punct [$a:tt $p:tt $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, stringify!($a), stringify!($p), " "] $($tt)*); + }; + + (@upunct [$a:tt $p:tt $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, stringify!($a), stringify!($p)] $($tt)*); + }; + + ([$a:tt . $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a . $($b)*] $($rest)+); }; + ([$a:tt , $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a , $($b)*] $($rest)+); }; + ([$a:tt ; $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a ; $($b)*] $($rest)+); }; + ([$a:tt : $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a : $($b)*] $($rest)+); }; + ([$a:tt ! $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a ! $($b)*] $($rest)+); }; + ([$a:tt ! $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a ! $($b)*] $($rest)+); }; + + ([$a:tt :: $($b:tt)*] $($rest:tt)+) => { docify!(@upunct [$a :: $($b)*] $($rest)+); }; + + ([$a:tt $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { + docify!([$($b)*] [$($c)+, stringify!($a), " "] $($tt)*); + }; + + ([] [$($doc:expr),*] $($tt:tt)*) => { + docify!(concat!($($doc),*), $($tt)*); + }; + + ($x:expr, $($tt:tt)*) => { + #[doc = $x] + $($tt)* + }; +} diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index c8dd358d..6954ec45 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -45,15 +45,119 @@ pub struct ContentType(pub MediaType); macro_rules! content_types { ($($name:ident ($check:ident): $str:expr, $t:expr, $s:expr $(; $k:expr => $v:expr)*,)+) => { - $( - #[doc="Content-Type for "] #[doc=$str] #[doc=": "] - #[doc=$t] #[doc="/"] #[doc=$s] - $(#[doc="; "] #[doc=$k] #[doc=" = "] #[doc=$v])* - #[doc=""] + $( + docify!([ + Content Type for @{"**"}! @{$str}! @{"**"}!: @{"`"} @{$t}! @[/]! @{$s}! + $(; @{$k}! @[=]! @{$v}!)* @{"`"}!. + ]; #[allow(non_upper_case_globals)] pub const $name: ContentType = ContentType(MediaType::$name); - )+ - }; + ); + )+ +}} + +macro_rules! from_extension { + ($($ext:expr => $name:ident,)*) => ( + docify!([ + Returns the @[Content-Type] associated with the extension @code{ext}. + Not all extensions are recognized. If an extensions is not recognized, + @code{None} is returned. The currently recognized extensions are: + + @nl + $(* @{$ext} - @{"`ContentType::"}! @[$name]! @{"`"} @nl)* + @nl + + This list is likely to grow. Extensions are matched + @[case-insensitively.] + ]; + /// # Example + /// + /// Recognized content types: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let xml = ContentType::from_extension("xml"); + /// assert_eq!(xml, Some(ContentType::XML)); + /// + /// let xml = ContentType::from_extension("XML"); + /// assert_eq!(xml, Some(ContentType::XML)); + /// ``` + /// + /// An unrecognized content type: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let foo = ContentType::from_extension("foo"); + /// assert!(foo.is_none()); + /// ``` + #[inline] + pub fn from_extension(ext: &str) -> Option { + MediaType::from_extension(ext).map(ContentType) + } + );) +} + +macro_rules! parse_flexible { + ($($short:expr => $name:ident,)*) => ( + docify!([ + Flexibly parses @code{name} into a @code{ContentType}. The parse is + @[_flexible_] because, in addition to stricly correct content types, it + recognizes the following shorthands: + + @nl + $(* $short - @{"`ContentType::"}! @[$name]! @{"`"} @nl)* + @nl + ]; + /// For regular parsing, use the + /// [`ContentType::from_str()`](#impl-FromStr) method. + /// + /// # Example + /// + /// Using a shorthand: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let html = ContentType::parse_flexible("html"); + /// assert_eq!(html, Some(ContentType::HTML)); + /// + /// let json = ContentType::parse_flexible("json"); + /// assert_eq!(json, Some(ContentType::JSON)); + /// ``` + /// + /// Using the full content-type: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let html = ContentType::parse_flexible("text/html; charset=utf-8"); + /// assert_eq!(html, Some(ContentType::HTML)); + /// + /// let json = ContentType::parse_flexible("application/json"); + /// assert_eq!(json, Some(ContentType::JSON)); + /// + /// let custom = ContentType::parse_flexible("application/x+custom"); + /// assert_eq!(custom, Some(ContentType::new("application", "x+custom"))); + /// ``` + /// + /// An unrecognized content-type: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let foo = ContentType::parse_flexible("foo"); + /// assert_eq!(foo, None); + /// + /// let bar = ContentType::parse_flexible("foo/bar/baz"); + /// assert_eq!(bar, None); + /// ``` + #[inline] + pub fn parse_flexible(name: &str) -> Option { + MediaType::parse_flexible(name).map(ContentType) + } + );) } impl ContentType { @@ -79,39 +183,9 @@ impl ContentType { ContentType(MediaType::new(top, sub)) } - /// Returns the Content-Type associated with the extension `ext` if the - /// extension is recognized. Not all extensions are recognized. If an - /// extensions is not recognized, then this method returns `None`. The - /// currently recognized extensions are txt, html, htm, xml, csv, js, css, - /// json, png, gif, bmp, jpeg, jpg, webp, svg, pdf, ttf, otf, woff, and - /// woff2. Extensions are matched case-insensitively. - /// - /// # Example - /// - /// Recognized content types: - /// - /// ```rust - /// use rocket::http::ContentType; - /// - /// let xml = ContentType::from_extension("xml"); - /// assert_eq!(xml, Some(ContentType::XML)); - /// - /// let xml = ContentType::from_extension("XML"); - /// assert_eq!(xml, Some(ContentType::XML)); - /// ``` - /// - /// An unrecognized content type: - /// - /// ```rust - /// use rocket::http::ContentType; - /// - /// let foo = ContentType::from_extension("foo"); - /// assert!(foo.is_none()); - /// ``` - #[inline] - pub fn from_extension(ext: &str) -> Option { - MediaType::from_extension(ext).map(ContentType) - } + known_shorthands!(parse_flexible); + + known_extensions!(from_extension); /// Creates a new `ContentType` with top-level type `top`, subtype `sub`, /// and parameters `ps`. This should _only_ be used to construct uncommon or diff --git a/lib/src/http/known_media_types.rs b/lib/src/http/known_media_types.rs index 7038822e..1e195a51 100644 --- a/lib/src/http/known_media_types.rs +++ b/lib/src/http/known_media_types.rs @@ -5,7 +5,7 @@ macro_rules! known_media_types { HTML (is_html): "HTML", "text", "html" ; "charset" => "utf-8", Plain (is_plain): "plain text", "text", "plain" ; "charset" => "utf-8", JSON (is_json): "JSON", "application", "json", - MsgPack (is_msgpack): "MessagePack", "application", "msgpack", + MsgPack (is_msgpack): "MsgPack", "application", "msgpack", Form (is_form): "forms", "application", "x-www-form-urlencoded", JavaScript (is_javascript): "JavaScript", "application", "javascript", CSS (is_css): "CSS", "text", "css" ; "charset" => "utf-8", @@ -51,3 +51,19 @@ macro_rules! known_extensions { "woff2" => WOFF2, }) } + +macro_rules! known_shorthands { + ($cont:ident) => ($cont! { + "any" => Any, + "binary" => Binary, + "html" => HTML, + "plain" => Plain, + "json" => JSON, + "msgpack" => MsgPack, + "form" => Form, + "js" => JavaScript, + "css" => CSS, + "multipart" => FormData, + "xml" => XML, + }) +} diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index ee3d72b5..7fd3b8ac 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -106,11 +106,11 @@ macro_rules! media_str { macro_rules! media_types { ($($name:ident ($check:ident): $str:expr, $t:expr, $s:expr $(; $k:expr => $v:expr)*,)+) => { - $( - #[doc="Media type for "] #[doc=$str] #[doc=": "] - #[doc=$t] #[doc="/"] #[doc=$s] - $(#[doc="; "] #[doc=$k] #[doc=" = "] #[doc=$v])* - #[doc=""] + $( + docify!([ + Media Type for @{"**"}! @{$str}! @{"**"}!: @{"`"} @{$t}! @[/]! @{$s}! + $(; @{$k}! @[=]! @{$v}!)* @{"`"}!. + ]; #[allow(non_upper_case_globals)] pub const $name: MediaType = MediaType { source: Source::Known(concat!($t, "/", $s, $("; ", $k, "=", $v),*)), @@ -118,34 +118,48 @@ macro_rules! media_types { sub: media_str!($s), params: MediaParams::Static(&[$((media_str!($k), media_str!($v))),*]) }; + ); + )+ - #[doc="Returns `true` if `self` is the media type for "] - #[doc=$str] - #[doc=", "] - /// without considering parameters. + /// Returns `true` if this MediaType is known to Rocket. In other words, + /// returns `true` if there is an associated constant for `self`. + pub fn is_known(&self) -> bool { + if let Source::Known(_) = self.source { + return true; + } + + $(if self.$check() { return true })+ + false + } + + $( + docify!([ + Returns @code{true} if the @[top-level] and sublevel types of + @code{self} are the same as those of @{"`MediaType::"}! $name + @{"`"}!. + ]; #[inline(always)] pub fn $check(&self) -> bool { *self == MediaType::$name } - )+ - - /// Returns `true` if this MediaType is known to Rocket, that is, - /// there is an associated constant for `self`. - pub fn is_known(&self) -> bool { - $(if self.$check() { return true })+ - false - } - }; -} + ); + )+ +}} macro_rules! from_extension { ($($ext:expr => $name:ident,)*) => ( - /// Returns the Media-Type associated with the extension `ext`. Not all - /// extensions are recognized. If an extensions is not recognized, - /// `None` is returned. The currently recognized extensions are - $(#[doc=$ext]#[doc=","])* - /// and is likely to grow. Extensions are matched case-insensitively. - /// + docify!([ + Returns the @[Media-Type] associated with the extension @code{ext}. Not + all extensions are recognized. If an extensions is not recognized, + @code{None} is returned. The currently recognized extensions are: + + @nl + $(* @{$ext} - @{"`MediaType::"}! @[$name]! @{"`"} @nl)* + @nl + + This list is likely to grow. Extensions are matched + @[case-insensitively.] + ]; /// # Example /// /// Recognized media types: @@ -174,7 +188,70 @@ macro_rules! from_extension { _ => None } } - ) + );) +} + +macro_rules! parse_flexible { + ($($short:expr => $name:ident,)*) => ( + docify!([ + Flexibly parses @code{name} into a @code{MediaType}. The parse is + @[_flexible_] because, in addition to stricly correct media types, it + recognizes the following shorthands: + + @nl + $(* $short - @{"`MediaType::"}! @[$name]! @{"`"} @nl)* + @nl + ]; + /// For regular parsing, use the + /// [`MediaType::from_str()`](#impl-FromStr) method. + /// + /// # Example + /// + /// Using a shorthand: + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let html = MediaType::parse_flexible("html"); + /// assert_eq!(html, Some(MediaType::HTML)); + /// + /// let json = MediaType::parse_flexible("json"); + /// assert_eq!(json, Some(MediaType::JSON)); + /// ``` + /// + /// Using the full media type: + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let html = MediaType::parse_flexible("text/html; charset=utf-8"); + /// assert_eq!(html, Some(MediaType::HTML)); + /// + /// let json = MediaType::parse_flexible("application/json"); + /// assert_eq!(json, Some(MediaType::JSON)); + /// + /// let custom = MediaType::parse_flexible("application/x+custom"); + /// assert_eq!(custom, Some(MediaType::new("application", "x+custom"))); + /// ``` + /// + /// An unrecognized media type: + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let foo = MediaType::parse_flexible("foo"); + /// assert_eq!(foo, None); + /// + /// let bar = MediaType::parse_flexible("foo/bar/baz"); + /// assert_eq!(bar, None); + /// ``` + pub fn parse_flexible(name: &str) -> Option { + match name { + $(x if uncased_eq(x, $short) => Some(MediaType::$name)),*, + _ => MediaType::from_str(name).ok(), + } + } + );) } impl MediaType { @@ -240,7 +317,6 @@ impl MediaType { IndexedStr::Concrete(val.into()) )); - MediaType { source: Source::None, top: IndexedStr::Concrete(top.into()), @@ -249,6 +325,8 @@ impl MediaType { } } + known_shorthands!(parse_flexible); + known_extensions!(from_extension); /// Returns the top-level type for this media type. The return type, diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c28dda0f..81a9074e 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -4,6 +4,7 @@ #![feature(plugin, decl_macro)] #![feature(never_type)] #![feature(try_trait)] +#![recursion_limit="256"] #![plugin(pear_codegen)] @@ -118,6 +119,7 @@ extern crate isatty; #[cfg(test)] #[macro_use] extern crate lazy_static; #[doc(hidden)] #[macro_use] pub mod logger; +#[macro_use] mod docify; pub mod local; pub mod http; pub mod request; diff --git a/lib/src/response/content.rs b/lib/src/response/content.rs index 01562476..9e889ee4 100644 --- a/lib/src/response/content.rs +++ b/lib/src/response/content.rs @@ -90,4 +90,3 @@ ctrs! { Css: CSS, "CSS", "text/css", JavaScript: JavaScript, "JavaScript", "application/javascript" } -