diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index 8084d627..dcea6f6b 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -19,6 +19,7 @@ indexmap = "1.0" quote = "1.0" rocket_http = { version = "0.5.0-dev", path = "../http/" } devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "bd221a4" } +unicode-xid = "0.2" glob = "0.3" [dev-dependencies] diff --git a/core/codegen/src/attribute/async_entry.rs b/core/codegen/src/attribute/async_entry.rs deleted file mode 100644 index b42a3dbc..00000000 --- a/core/codegen/src/attribute/async_entry.rs +++ /dev/null @@ -1,141 +0,0 @@ -use devise::{syn, Diagnostic, Spanned, Result}; -use devise::ext::SpanDiagnosticExt; -use devise::proc_macro2::{TokenStream, Span}; - -trait EntryAttr { - /// Whether the attribute requires the attributed function to be `async`. - const REQUIRES_ASYNC: bool; - - /// Return a new or rewritten function, using block as the main execution. - fn function(f: &mut syn::ItemFn) -> Result; -} - -struct Main; - -impl EntryAttr for Main { - const REQUIRES_ASYNC: bool = true; - - fn function(f: &mut syn::ItemFn) -> Result { - let (attrs, vis, block, sig) = (&f.attrs, &f.vis, &f.block, &mut f.sig); - if sig.ident != "main" { - // FIXME(diag): warning! - Span::call_site() - .warning("attribute is typically applied to `main` function") - .span_note(sig.ident.span(), "this function is not `main`") - .emit_as_item_tokens(); - } - - sig.asyncness = None; - Ok(quote_spanned!(block.span().into() => #(#attrs)* #vis #sig { - ::rocket::async_main(async move #block) - })) - } -} - -struct Test; - -impl EntryAttr for Test { - const REQUIRES_ASYNC: bool = true; - - fn function(f: &mut syn::ItemFn) -> Result { - let (attrs, vis, block, sig) = (&f.attrs, &f.vis, &f.block, &mut f.sig); - sig.asyncness = None; - Ok(quote_spanned!(block.span().into() => #(#attrs)* #[test] #vis #sig { - ::rocket::async_test(async move #block) - })) - } -} - -struct Launch; - -impl EntryAttr for Launch { - const REQUIRES_ASYNC: bool = false; - - fn function(f: &mut syn::ItemFn) -> Result { - if f.sig.ident == "main" { - return Err(Span::call_site() - .error("attribute cannot be applied to `main` function") - .note("this attribute generates a `main` function") - .span_note(f.sig.ident.span(), "this function cannot be `main`")); - } - - // Always infer the type as `::rocket::Rocket`. - if let syn::ReturnType::Type(_, ref mut ty) = &mut f.sig.output { - if let syn::Type::Infer(_) = &mut **ty { - let new = quote_spanned!(ty.span() => ::rocket::Rocket); - *ty = syn::parse2(new).expect("path is type"); - } - } - - let ty = match &f.sig.output { - syn::ReturnType::Type(_, ty) => ty, - _ => return Err(Span::call_site() - .error("attribute can only be applied to functions that return a value") - .span_note(f.sig.span(), "this function must return a value")) - }; - - let block = &f.block; - let rocket = quote_spanned!(ty.span().into() => { - let ___rocket: #ty = #block; - let ___rocket: ::rocket::Rocket = ___rocket; - ___rocket - }); - - let (vis, mut sig) = (&f.vis, f.sig.clone()); - sig.ident = syn::Ident::new("main", sig.ident.span()); - sig.output = syn::ReturnType::Default; - sig.asyncness = None; - - Ok(quote_spanned!(block.span().into() => - #[allow(dead_code)] #f - - #vis #sig { - ::rocket::async_main(async move { let _ = #rocket.launch().await; }) - } - )) - } -} - -fn parse_input(input: proc_macro::TokenStream) -> Result { - let function: syn::ItemFn = syn::parse(input) - .map_err(Diagnostic::from) - .map_err(|d| d.help("attribute can only be applied to functions"))?; - - if A::REQUIRES_ASYNC && function.sig.asyncness.is_none() { - return Err(Span::call_site() - .error("attribute can only be applied to `async` functions") - .span_note(function.sig.span(), "this function must be `async`")); - } - - if !function.sig.inputs.is_empty() { - return Err(Span::call_site() - .error("attribute can only be applied to functions without arguments") - .span_note(function.sig.span(), "this function must take no arguments")); - } - - Ok(function) -} - -fn _async_entry( - _args: proc_macro::TokenStream, - input: proc_macro::TokenStream -) -> Result { - let mut function = parse_input::(input)?; - A::function(&mut function).map(|t| t.into()) -} - -macro_rules! async_entry { - ($name:ident, $kind:ty, $default:expr) => ( - pub fn $name(a: proc_macro::TokenStream, i: proc_macro::TokenStream) -> TokenStream { - _async_entry::<$kind>(a, i).unwrap_or_else(|d| { - let d = d.emit_as_item_tokens(); - let default = $default; - quote!(#d #default) - }) - } - ) -} - -async_entry!(async_test_attribute, Test, quote!()); -async_entry!(main_attribute, Main, quote!(fn main() {})); -async_entry!(launch_attribute, Launch, quote!(fn main() {})); diff --git a/core/codegen/src/attribute/catch.rs b/core/codegen/src/attribute/catch/mod.rs similarity index 59% rename from core/codegen/src/attribute/catch.rs rename to core/codegen/src/attribute/catch/mod.rs index d92ac9d4..11ae3597 100644 --- a/core/codegen/src/attribute/catch.rs +++ b/core/codegen/src/attribute/catch/mod.rs @@ -1,79 +1,25 @@ -use devise::ext::SpanDiagnosticExt; -use devise::{syn, MetaItem, Spanned, Result, FromMeta, Diagnostic}; +mod parse; -use crate::http_codegen::{self, Optional}; +use devise::ext::SpanDiagnosticExt; +use devise::{syn, Spanned, Result}; + +use crate::http_codegen::Optional; use crate::proc_macro2::{TokenStream, Span}; use crate::syn_ext::ReturnTypeExt; - -/// The raw, parsed `#[catch(code)]` attribute. -#[derive(Debug, FromMeta)] -struct CatchAttribute { - #[meta(naked)] - status: CatcherCode -} - -/// `Some` if there's a code, `None` if it's `default`. -#[derive(Debug)] -struct CatcherCode(Option); - -impl FromMeta for CatcherCode { - fn from_meta(meta: &MetaItem) -> Result { - if usize::from_meta(meta).is_ok() { - let status = http_codegen::Status::from_meta(meta)?; - Ok(CatcherCode(Some(status))) - } else if let MetaItem::Path(path) = meta { - if path.is_ident("default") { - Ok(CatcherCode(None)) - } else { - Err(meta.span().error("expected `default`")) - } - } else { - let msg = format!("expected integer or identifier, found {}", meta.description()); - Err(meta.span().error(msg)) - } - } -} - -/// This structure represents the parsed `catch` attribute and associated items. -struct CatchParams { - /// The status associated with the code in the `#[catch(code)]` attribute. - status: Option, - /// The function that was decorated with the `catch` attribute. - function: syn::ItemFn, -} - -fn parse_params( - args: TokenStream, - input: proc_macro::TokenStream -) -> Result { - let function: syn::ItemFn = syn::parse(input) - .map_err(Diagnostic::from) - .map_err(|diag| diag.help("`#[catch]` can only be used on functions"))?; - - let attribute = CatchAttribute::from_meta(&syn::parse2(quote!(catch(#args)))?) - .map_err(|diag| diag.help("`#[catch]` expects a status code int or `default`: \ - `#[catch(404)]` or `#[catch(default)]`"))?; - - Ok(CatchParams { status: attribute.status.0, function }) -} +use crate::exports::*; pub fn _catch( args: proc_macro::TokenStream, input: proc_macro::TokenStream ) -> Result { // Parse and validate all of the user's input. - let catch = parse_params(args.into(), input)?; + let catch = parse::Attribute::parse(args.into(), input)?; // Gather everything we'll need to generate the catcher. let user_catcher_fn = &catch.function; - let user_catcher_fn_name = catch.function.sig.ident.clone(); - let (vis, catcher_status) = (&catch.function.vis, &catch.status); - let status_code = Optional(catcher_status.as_ref().map(|s| s.0.code)); - - // Variables names we'll use and reuse. - define_spanned_export!(catch.function.span().into() => - __req, __status, _Box, Request, Response, StaticCatcherInfo, Catcher, - ErrorHandlerFuture, Status); + let user_catcher_fn_name = &catch.function.sig.ident; + let vis = &catch.function.vis; + let status_code = Optional(catch.status.map(|s| s.code)); // Determine the number of parameters that will be passed in. if catch.function.sig.inputs.len() > 2 { diff --git a/core/codegen/src/attribute/catch/parse.rs b/core/codegen/src/attribute/catch/parse.rs new file mode 100644 index 00000000..6ce31178 --- /dev/null +++ b/core/codegen/src/attribute/catch/parse.rs @@ -0,0 +1,58 @@ +use devise::ext::SpanDiagnosticExt; +use devise::{syn, MetaItem, Spanned, Result, FromMeta, Diagnostic}; + +use crate::{http, http_codegen}; +use crate::proc_macro2::TokenStream; + +/// This structure represents the parsed `catch` attribute and associated items. +pub struct Attribute { + /// The status associated with the code in the `#[catch(code)]` attribute. + pub status: Option, + /// The function that was decorated with the `catch` attribute. + pub function: syn::ItemFn, +} + +/// We generate a full parser for the meta-item for great error messages. +#[derive(FromMeta)] +struct Meta { + #[meta(naked)] + code: Code, +} + +/// `Some` if there's a code, `None` if it's `default`. +#[derive(Debug)] +struct Code(Option); + +impl FromMeta for Code { + fn from_meta(meta: &MetaItem) -> Result { + if usize::from_meta(meta).is_ok() { + let status = http_codegen::Status::from_meta(meta)?; + Ok(Code(Some(status.0))) + } else if let MetaItem::Path(path) = meta { + if path.is_ident("default") { + Ok(Code(None)) + } else { + Err(meta.span().error("expected `default`")) + } + } else { + let msg = format!("expected integer or `default`, found {}", meta.description()); + Err(meta.span().error(msg)) + } + } +} + +impl Attribute { + pub fn parse(args: TokenStream, input: proc_macro::TokenStream) -> Result { + let function: syn::ItemFn = syn::parse(input) + .map_err(Diagnostic::from) + .map_err(|diag| diag.help("`#[catch]` can only be used on functions"))?; + + let attr: MetaItem = syn::parse2(quote!(catch(#args)))?; + let status = Meta::from_meta(&attr) + .map(|meta| meta.code.0) + .map_err(|diag| diag.help("`#[catch]` expects a status code int or `default`: \ + `#[catch(404)]` or `#[catch(default)]`"))?; + + Ok(Attribute { status, function }) + } +} diff --git a/core/codegen/src/attribute/entry/launch.rs b/core/codegen/src/attribute/entry/launch.rs new file mode 100644 index 00000000..bfb4ad96 --- /dev/null +++ b/core/codegen/src/attribute/entry/launch.rs @@ -0,0 +1,58 @@ +use super::EntryAttr; + +use devise::{syn, Spanned, Result}; +use devise::ext::SpanDiagnosticExt; +use devise::proc_macro2::{TokenStream, Span}; + +/// `#[rocket::launch]`: generates a `main` function that calls the attributed +/// function to generate a `Rocket` instance. Then calls `.launch()` on the +/// returned instance inside of an `rocket::async_main`. +pub struct Launch; + +impl EntryAttr for Launch { + const REQUIRES_ASYNC: bool = false; + + fn function(f: &mut syn::ItemFn) -> Result { + if f.sig.ident == "main" { + return Err(Span::call_site() + .error("attribute cannot be applied to `main` function") + .note("this attribute generates a `main` function") + .span_note(f.sig.ident.span(), "this function cannot be `main`")); + } + + // Always infer the type as `::rocket::Rocket`. + if let syn::ReturnType::Type(_, ref mut ty) = &mut f.sig.output { + if let syn::Type::Infer(_) = &mut **ty { + let new = quote_spanned!(ty.span() => ::rocket::Rocket); + *ty = syn::parse2(new).expect("path is type"); + } + } + + let ty = match &f.sig.output { + syn::ReturnType::Type(_, ty) => ty, + _ => return Err(Span::call_site() + .error("attribute can only be applied to functions that return a value") + .span_note(f.sig.span(), "this function must return a value")) + }; + + let block = &f.block; + let rocket = quote_spanned!(ty.span().into() => { + let ___rocket: #ty = #block; + let ___rocket: ::rocket::Rocket = ___rocket; + ___rocket + }); + + let (vis, mut sig) = (&f.vis, f.sig.clone()); + sig.ident = syn::Ident::new("main", sig.ident.span()); + sig.output = syn::ReturnType::Default; + sig.asyncness = None; + + Ok(quote_spanned!(block.span().into() => + #[allow(dead_code)] #f + + #vis #sig { + ::rocket::async_main(async move { let _ = #rocket.launch().await; }) + } + )) + } +} diff --git a/core/codegen/src/attribute/entry/main.rs b/core/codegen/src/attribute/entry/main.rs new file mode 100644 index 00000000..e83261e6 --- /dev/null +++ b/core/codegen/src/attribute/entry/main.rs @@ -0,0 +1,28 @@ +use super::EntryAttr; + +use devise::{syn, Spanned, Result}; +use devise::ext::SpanDiagnosticExt; +use devise::proc_macro2::{TokenStream, Span}; + +/// `#[rocket::async_main]`: calls the attributed fn inside `rocket::async_main` +pub struct Main; + +impl EntryAttr for Main { + const REQUIRES_ASYNC: bool = true; + + fn function(f: &mut syn::ItemFn) -> Result { + let (attrs, vis, block, sig) = (&f.attrs, &f.vis, &f.block, &mut f.sig); + if sig.ident != "main" { + // FIXME(diag): warning! + Span::call_site() + .warning("attribute is typically applied to `main` function") + .span_note(sig.ident.span(), "this function is not `main`") + .emit_as_item_tokens(); + } + + sig.asyncness = None; + Ok(quote_spanned!(block.span().into() => #(#attrs)* #vis #sig { + ::rocket::async_main(async move #block) + })) + } +} diff --git a/core/codegen/src/attribute/entry/mod.rs b/core/codegen/src/attribute/entry/mod.rs new file mode 100644 index 00000000..0698ed83 --- /dev/null +++ b/core/codegen/src/attribute/entry/mod.rs @@ -0,0 +1,55 @@ +mod main; +mod launch; +mod test; + +use devise::{syn, Diagnostic, Spanned, Result}; +use devise::ext::SpanDiagnosticExt; +use devise::proc_macro2::{TokenStream, Span}; + +// Common trait implemented by `async` entry generating attributes. +trait EntryAttr { + /// Whether the attribute requires the attributed function to be `async`. + const REQUIRES_ASYNC: bool; + + /// Return a new or rewritten function, using block as the main execution. + fn function(f: &mut syn::ItemFn) -> Result; +} + +fn _async_entry( + _args: proc_macro::TokenStream, + input: proc_macro::TokenStream +) -> Result { + let mut function: syn::ItemFn = syn::parse(input) + .map_err(Diagnostic::from) + .map_err(|d| d.help("attribute can only be applied to functions"))?; + + if A::REQUIRES_ASYNC && function.sig.asyncness.is_none() { + return Err(Span::call_site() + .error("attribute can only be applied to `async` functions") + .span_note(function.sig.span(), "this function must be `async`")); + } + + if !function.sig.inputs.is_empty() { + return Err(Span::call_site() + .error("attribute can only be applied to functions without arguments") + .span_note(function.sig.span(), "this function must take no arguments")); + } + + A::function(&mut function).map(|t| t.into()) +} + +macro_rules! async_entry { + ($name:ident, $kind:ty, $default:expr) => ( + pub fn $name(a: proc_macro::TokenStream, i: proc_macro::TokenStream) -> TokenStream { + _async_entry::<$kind>(a, i).unwrap_or_else(|d| { + let d = d.emit_as_item_tokens(); + let default = $default; + quote!(#d #default) + }) + } + ) +} + +async_entry!(async_test_attribute, test::Test, quote!()); +async_entry!(main_attribute, main::Main, quote!(fn main() {})); +async_entry!(launch_attribute, launch::Launch, quote!(fn main() {})); diff --git a/core/codegen/src/attribute/entry/test.rs b/core/codegen/src/attribute/entry/test.rs new file mode 100644 index 00000000..5804748d --- /dev/null +++ b/core/codegen/src/attribute/entry/test.rs @@ -0,0 +1,19 @@ +use super::EntryAttr; + +use devise::{syn, Spanned, Result}; +use devise::proc_macro2::TokenStream; + +/// `#[rocket::async_test]`: calls the attributed fn inside `rocket::async_test` +pub struct Test; + +impl EntryAttr for Test { + const REQUIRES_ASYNC: bool = true; + + fn function(f: &mut syn::ItemFn) -> Result { + let (attrs, vis, block, sig) = (&f.attrs, &f.vis, &f.block, &mut f.sig); + sig.asyncness = None; + Ok(quote_spanned!(block.span().into() => #(#attrs)* #[test] #vis #sig { + ::rocket::async_test(async move #block) + })) + } +} diff --git a/core/codegen/src/attribute/mod.rs b/core/codegen/src/attribute/mod.rs index f3b72770..4d06591d 100644 --- a/core/codegen/src/attribute/mod.rs +++ b/core/codegen/src/attribute/mod.rs @@ -1,4 +1,4 @@ -pub mod async_entry; +pub mod entry; pub mod catch; pub mod route; -pub mod segments; +pub mod param; diff --git a/core/codegen/src/attribute/param/guard.rs b/core/codegen/src/attribute/param/guard.rs new file mode 100644 index 00000000..9a7db661 --- /dev/null +++ b/core/codegen/src/attribute/param/guard.rs @@ -0,0 +1,39 @@ +use std::hash::{Hash, Hasher}; + +use devise::{syn, FromMeta, MetaItem, Result}; + +use crate::name::Name; +use crate::proc_macro2::Span; +use crate::proc_macro_ext::StringLit; +use crate::http::uri; + + +impl Dynamic { + pub fn is_wild(&self) -> bool { + self.value == "_" + } +} + +impl FromMeta for Dynamic { + fn from_meta(meta: &MetaItem) -> Result { + let string = StringLit::from_meta(meta)?; + let span = string.subspan(1..string.len() + 1); + + // We don't allow `_`. We abuse `uri::Query` to enforce this. + Ok(Dynamic::parse::(&string, span)?) + } +} + +impl PartialEq for Dynamic { + fn eq(&self, other: &Dynamic) -> bool { + self.value == other.value + } +} + +impl Eq for Dynamic {} + +impl Hash for Dynamic { + fn hash(&self, state: &mut H) { + self.value.hash(state) + } +} diff --git a/core/codegen/src/attribute/param/mod.rs b/core/codegen/src/attribute/param/mod.rs new file mode 100644 index 00000000..07b6fa94 --- /dev/null +++ b/core/codegen/src/attribute/param/mod.rs @@ -0,0 +1,118 @@ +mod parse; + +use std::ops::Deref; +use std::hash::Hash; + +use crate::syn; +use crate::name::Name; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Parameter { + Static(Name), + Ignored(Dynamic), + Dynamic(Dynamic), + Guard(Guard), +} + +#[derive(Debug, Clone)] +pub struct Dynamic { + pub name: Name, + pub index: usize, + pub trailing: bool, +} + +#[derive(Debug, Clone)] +pub struct Guard { + pub source: Dynamic, + pub fn_ident: syn::Ident, + pub ty: syn::Type, +} + +impl Parameter { + pub fn r#static(&self) -> Option<&Name> { + match self { + Parameter::Static(s) => Some(s), + _ => None + } + } + + pub fn ignored(&self) -> Option<&Dynamic> { + match self { + Parameter::Ignored(d) => Some(d), + _ => None + } + } + + pub fn take_dynamic(self) -> Option { + match self { + Parameter::Dynamic(d) => Some(d), + Parameter::Guard(g) => Some(g.source), + _ => None + } + } + + pub fn dynamic(&self) -> Option<&Dynamic> { + match self { + Parameter::Dynamic(d) => Some(d), + Parameter::Guard(g) => Some(&g.source), + _ => None + } + } + + pub fn dynamic_mut(&mut self) -> Option<&mut Dynamic> { + match self { + Parameter::Dynamic(d) => Some(d), + Parameter::Guard(g) => Some(&mut g.source), + _ => None + } + } + + pub fn guard(&self) -> Option<&Guard> { + match self { + Parameter::Guard(g) => Some(g), + _ => None + } + } +} + +impl Dynamic { + // This isn't public since this `Dynamic` should always be an `Ignored`. + pub fn is_wild(&self) -> bool { + self.name == "_" + } +} + +impl Guard { + pub fn from(source: Dynamic, fn_ident: syn::Ident, ty: syn::Type) -> Self { + Guard { source, fn_ident, ty } + } +} + +macro_rules! impl_derived { + ($T:ty => $U:ty = $v:ident) => ( + impl Deref for $T { + type Target = $U; + + fn deref(&self) -> &Self::Target { + &self.$v + } + } + + impl PartialEq for $T { + fn eq(&self, other: &Self) -> bool { + self.$v == other.$v + } + } + + impl Eq for $T { } + + impl Hash for $T { + fn hash(&self, state: &mut H) { + self.$v.hash(state) + } + } + ) +} + +impl_derived!(Dynamic => Name = name); +impl_derived!(Guard => Dynamic = source); diff --git a/core/codegen/src/attribute/param/parse.rs b/core/codegen/src/attribute/param/parse.rs new file mode 100644 index 00000000..2fb933c7 --- /dev/null +++ b/core/codegen/src/attribute/param/parse.rs @@ -0,0 +1,245 @@ +use unicode_xid::UnicodeXID; +use devise::{Diagnostic, ext::SpanDiagnosticExt}; + +use crate::name::Name; +use crate::http::uri::{self, UriPart}; +use crate::proc_macro_ext::StringLit; +use crate::proc_macro2::Span; +use crate::attribute::param::{Parameter, Dynamic}; + +#[derive(Debug)] +pub struct Error<'a> { + segment: &'a str, + span: Span, + source: &'a str, + source_span: Span, + kind: ErrorKind, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum ErrorKind { + Empty, + BadIdent, + Ignored, + EarlyTrailing, + NoTrailing, + Static, +} + +impl Dynamic { + pub fn parse( + segment: &str, + span: Span, + ) -> Result> { + match Parameter::parse::

(&segment, span)? { + Parameter::Dynamic(d) | Parameter::Ignored(d) => Ok(d), + Parameter::Guard(g) => Ok(g.source), + Parameter::Static(_) => Err(Error::new(segment, span, ErrorKind::Static)), + } + } +} + +impl Parameter { + pub fn parse( + segment: &str, + source_span: Span, + ) -> Result> { + let mut trailing = false; + + // Check if this is a dynamic param. If so, check its well-formedness. + if segment.starts_with('<') && segment.ends_with('>') { + let mut name = &segment[1..(segment.len() - 1)]; + if name.ends_with("..") { + trailing = true; + name = &name[..(name.len() - 2)]; + } + + let span = subspan(name, segment, source_span); + if name.is_empty() { + return Err(Error::new(name, source_span, ErrorKind::Empty)); + } else if !is_valid_ident(name) { + return Err(Error::new(name, span, ErrorKind::BadIdent)); + } + + let dynamic = Dynamic { name: Name::new(name, span), trailing, index: 0 }; + if dynamic.is_wild() && P::KIND != uri::Kind::Path { + return Err(Error::new(name, span, ErrorKind::Ignored)); + } else if dynamic.is_wild() { + return Ok(Parameter::Ignored(dynamic)); + } else { + return Ok(Parameter::Dynamic(dynamic)); + } + } else if segment.is_empty() { + return Err(Error::new(segment, source_span, ErrorKind::Empty)); + } else if segment.starts_with('<') { + let candidate = candidate_from_malformed(segment); + source_span.warning("`segment` starts with `<` but does not end with `>`") + .help(format!("perhaps you meant the dynamic parameter `<{}>`?", candidate)) + .emit_as_item_tokens(); + } else if segment.contains('>') || segment.contains('<') { + source_span.warning("`segment` contains `<` or `>` but is not a dynamic parameter") + .emit_as_item_tokens(); + } + + Ok(Parameter::Static(Name::new(segment, source_span))) + } + + pub fn parse_many( + source: &str, + source_span: Span, + ) -> impl Iterator>> { + let mut trailing: Option<(&str, Span)> = None; + + // We check for empty segments when we parse an `Origin` in `FromMeta`. + source.split(P::DELIMITER) + .filter(|s| !s.is_empty()) + .enumerate() + .map(move |(i, segment)| { + if let Some((trail, span)) = trailing { + let error = Error::new(trail, span, ErrorKind::EarlyTrailing) + .source(source, source_span); + + return Err(error); + } + + let segment_span = subspan(segment, source, source_span); + let mut parsed = Self::parse::

(segment, segment_span) + .map_err(|e| e.source(source, source_span))?; + + if let Some(ref mut d) = parsed.dynamic_mut() { + if d.trailing { + trailing = Some((segment, segment_span)); + } + + d.index = i; + } + + Ok(parsed) + }) + } +} + +impl std::fmt::Display for ErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ErrorKind::Empty => "parameters cannot be empty".fmt(f), + ErrorKind::BadIdent => "invalid identifier".fmt(f), + ErrorKind::Ignored => "parameter must be named".fmt(f), + ErrorKind::NoTrailing => "parameter cannot be trailing".fmt(f), + ErrorKind::EarlyTrailing => "unexpected text after trailing parameter".fmt(f), + ErrorKind::Static => "unexpected static parameter".fmt(f), + } + } +} + +impl<'a> Error<'a> { + pub fn new(segment: &str, span: Span, kind: ErrorKind) -> Error<'_> { + Error { segment, source: segment, span, source_span: span, kind } + } + + pub fn source(mut self, source: &'a str, span: Span) -> Self { + self.source = source; + self.source_span = span; + self + } +} + +impl From> for Diagnostic { + fn from(error: Error<'_>) -> Self { + match error.kind { + ErrorKind::Empty => error.span.error(error.kind.to_string()), + ErrorKind::BadIdent => { + let candidate = candidate_from_malformed(error.segment); + error.span.error(format!("{}: `{}`", error.kind, error.segment)) + .help("dynamic parameters must be valid identifiers") + .help(format!("did you mean `<{}>`?", candidate)) + } + ErrorKind::Ignored => { + error.span.error(error.kind.to_string()) + .help("use a name such as `_guard` or `_param`") + } + ErrorKind::EarlyTrailing => { + trailspan(error.segment, error.source, error.source_span) + .error(error.kind.to_string()) + .help("a trailing parameter must be the final component") + .span_note(error.span, "trailing param is here") + } + ErrorKind::NoTrailing => { + let candidate = candidate_from_malformed(error.segment); + error.span.error(error.kind.to_string()) + .help(format!("did you mean `<{}>`?", candidate)) + } + ErrorKind::Static => { + let candidate = candidate_from_malformed(error.segment); + error.span.error(error.kind.to_string()) + .help(format!("parameter must be dynamic: `<{}>`", candidate)) + } + } + } +} + +impl devise::FromMeta for Dynamic { + fn from_meta(meta: &devise::MetaItem) -> devise::Result { + let string = StringLit::from_meta(meta)?; + let span = string.subspan(1..string.len() + 1); + let param = Dynamic::parse::(&string, span)?; + + if param.is_wild() { + return Err(Error::new(&string, span, ErrorKind::Ignored).into()); + } else if param.trailing { + return Err(Error::new(&string, span, ErrorKind::NoTrailing).into()); + } else { + Ok(param) + } + } +} + +fn subspan(needle: &str, haystack: &str, span: Span) -> Span { + let index = needle.as_ptr() as usize - haystack.as_ptr() as usize; + StringLit::new(haystack, span).subspan(index..index + needle.len()) +} + +fn trailspan(needle: &str, haystack: &str, span: Span) -> Span { + let index = needle.as_ptr() as usize - haystack.as_ptr() as usize; + let lit = StringLit::new(haystack, span); + if needle.as_ptr() as usize > haystack.as_ptr() as usize { + lit.subspan((index - 1)..) + } else { + lit.subspan(index..) + } +} + +fn candidate_from_malformed(segment: &str) -> String { + let candidate = segment.chars().enumerate() + .filter(|(i, c)| *i == 0 && is_ident_start(*c) || *i != 0 && is_ident_continue(*c)) + .map(|(_, c)| c) + .collect::(); + + if candidate.is_empty() { + "param".into() + } else { + candidate + } +} + +#[inline] +fn is_ident_start(c: char) -> bool { + c.is_ascii_alphabetic() + || c == '_' + || (c > '\x7f' && UnicodeXID::is_xid_start(c)) +} + +#[inline] +fn is_ident_continue(c: char) -> bool { + c.is_ascii_alphanumeric() + || c == '_' + || (c > '\x7f' && UnicodeXID::is_xid_continue(c)) +} + +fn is_valid_ident(string: &str) -> bool { + let mut chars = string.chars(); + match chars.next() { + Some(c) => is_ident_start(c) && chars.all(is_ident_continue), + None => false + } +} diff --git a/core/codegen/src/attribute/route.rs b/core/codegen/src/attribute/route.rs deleted file mode 100644 index 386dd77d..00000000 --- a/core/codegen/src/attribute/route.rs +++ /dev/null @@ -1,498 +0,0 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; - -use devise::{syn, Spanned, SpanWrapped, Result, FromMeta, Diagnostic}; -use devise::ext::{SpanDiagnosticExt, TypeExt}; -use indexmap::IndexSet; - -use crate::proc_macro_ext::{Diagnostics, StringLit}; -use crate::syn_ext::{IdentExt, NameSource}; -use crate::proc_macro2::{TokenStream, Span}; -use crate::http_codegen::{Method, MediaType, RoutePath, DataSegment, Optional}; -use crate::attribute::segments::{Source, Kind, Segment}; - -use crate::{URI_MACRO_PREFIX, ROCKET_PARAM_PREFIX}; - -/// The raw, parsed `#[route]` attribute. -#[derive(Debug, FromMeta)] -struct RouteAttribute { - #[meta(naked)] - method: SpanWrapped, - path: RoutePath, - data: Option>, - format: Option, - rank: Option, -} - -/// The raw, parsed `#[method]` (e.g, `get`, `put`, `post`, etc.) attribute. -#[derive(Debug, FromMeta)] -struct MethodRouteAttribute { - #[meta(naked)] - path: RoutePath, - data: Option>, - format: Option, - rank: Option, -} - -/// This structure represents the parsed `route` attribute and associated items. -#[derive(Debug)] -struct Route { - /// The attribute: `#[get(path, ...)]`. - attribute: RouteAttribute, - /// The function the attribute decorated, i.e, the handler. - function: syn::ItemFn, - /// The non-static parameters declared in the route segments. - segments: IndexSet, - /// The parsed inputs to the user's function. The name is the param as the - /// user wrote it, while the ident is the identifier that should be used - /// during code generation, the `rocket_ident`. - inputs: Vec<(NameSource, syn::Ident, syn::Type)>, -} - -impl Route { - fn find_input(&self, name: &T) -> Option<&(NameSource, syn::Ident, syn::Type)> - where T: PartialEq - { - self.inputs.iter().find(|(n, ..)| name == n) - } -} - -fn parse_route(attr: RouteAttribute, function: syn::ItemFn) -> Result { - // Gather diagnostics as we proceed. - let mut diags = Diagnostics::new(); - - // Emit a warning if a `data` param was supplied for non-payload methods. - if let Some(ref data) = attr.data { - if !attr.method.0.supports_payload() { - let msg = format!("'{}' does not typically support payloads", attr.method.0); - // FIXME(diag: warning) - data.full_span.warning("`data` used with non-payload-supporting method") - .span_note(attr.method.span, msg) - .emit_as_item_tokens(); - } - } - - // Collect non-wild dynamic segments in an `IndexSet`, checking for dups. - let mut segments: IndexSet = IndexSet::new(); - fn dup_check<'a, I>(set: &mut IndexSet, iter: I, diags: &mut Diagnostics) - where I: Iterator - { - for segment in iter.filter(|s| s.is_dynamic()) { - let span = segment.span; - if let Some(previous) = set.replace(segment.clone()) { - diags.push(span.error(format!("duplicate parameter: `{}`", previous.name)) - .span_note(previous.span, "previous parameter with the same name here")) - } - } - } - - dup_check(&mut segments, attr.path.path.iter().filter(|s| !s.is_wild()), &mut diags); - attr.path.query.as_ref().map(|q| dup_check(&mut segments, q.iter(), &mut diags)); - dup_check(&mut segments, attr.data.as_ref().map(|s| &s.value.0).into_iter(), &mut diags); - - // Check the validity of function arguments. - let mut inputs = vec![]; - let mut fn_segments: IndexSet = IndexSet::new(); - for input in &function.sig.inputs { - let help = "all handler arguments must be of the form: `ident: Type`"; - let span = input.span(); - let (ident, ty) = match input { - syn::FnArg::Typed(arg) => match *arg.pat { - syn::Pat::Ident(ref pat) => (&pat.ident, &arg.ty), - syn::Pat::Wild(_) => { - diags.push(span.error("handler arguments cannot be ignored").help(help)); - continue; - } - _ => { - diags.push(span.error("invalid use of pattern").help(help)); - continue; - } - } - // Other cases shouldn't happen since we parsed an `ItemFn`. - _ => { - diags.push(span.error("invalid handler argument").help(help)); - continue; - } - }; - - let rocket_ident = ident.prepend(ROCKET_PARAM_PREFIX); - inputs.push((ident.clone().into(), rocket_ident, ty.with_stripped_lifetimes())); - fn_segments.insert(ident.into()); - } - - // Check that all of the declared parameters are function inputs. - let span = function.sig.paren_token.span; - for missing in segments.difference(&fn_segments) { - diags.push(missing.span.error("unused dynamic parameter") - .span_note(span, format!("expected argument named `{}` here", missing.name))) - } - - diags.head_err_or(Route { attribute: attr, function, inputs, segments }) -} - -fn param_expr(seg: &Segment, ident: &syn::Ident, ty: &syn::Type) -> TokenStream { - let i = seg.index.expect("dynamic parameters must be indexed"); - let span = ident.span().join(ty.span()).unwrap_or_else(|| ty.span()); - let name = ident.to_string(); - - define_spanned_export!(span => - __req, __data, _log, _request, _None, _Some, _Ok, _Err, Outcome - ); - - // All dynamic parameter should be found if this function is being called; - // that's the point of statically checking the URI parameters. - let internal_error = quote!({ - #_log::error("Internal invariant error: expected dynamic parameter not found."); - #_log::error("Please report this error to the Rocket issue tracker."); - #Outcome::Forward(#__data) - }); - - // Returned when a dynamic parameter fails to parse. - let parse_error = quote!({ - #_log::warn_(&format!("Failed to parse '{}': {:?}", #name, __error)); - #Outcome::Forward(#__data) - }); - - let expr = match seg.kind { - Kind::Single => quote_spanned! { span => - match #__req.routed_segment(#i) { - #_Some(__s) => match <#ty as #_request::FromParam>::from_param(__s) { - #_Ok(__v) => __v, - #_Err(__error) => return #parse_error, - }, - #_None => return #internal_error - } - }, - Kind::Multi => quote_spanned! { span => - match <#ty as #_request::FromSegments>::from_segments(#__req.routed_segments(#i..)) { - #_Ok(__v) => __v, - #_Err(__error) => return #parse_error, - } - }, - Kind::Static => return quote!() - }; - - quote! { - let #ident: #ty = #expr; - } -} - -fn data_expr(ident: &syn::Ident, ty: &syn::Type) -> TokenStream { - let span = ident.span().join(ty.span()).unwrap_or_else(|| ty.span()); - define_spanned_export!(span => __req, __data, FromData, Outcome); - - quote_spanned! { span => - let __outcome = <#ty as #FromData>::from_data(#__req, #__data).await; - - let #ident: #ty = match __outcome { - #Outcome::Success(__d) => __d, - #Outcome::Forward(__d) => return #Outcome::Forward(__d), - #Outcome::Failure((__c, _)) => return #Outcome::Failure(__c), - }; - } -} - -fn query_exprs(route: &Route) -> Option { - use devise::ext::{Split2, Split6}; - - define_spanned_export!(Span::call_site() => - __req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None - ); - - let query_segments = route.attribute.path.query.as_ref()?; - - // Record all of the static parameters for later filtering. - let (raw_name, raw_value) = query_segments.iter() - .filter(|s| !s.is_dynamic()) - .map(|s| { - let name = s.name.name(); - match name.find('=') { - Some(i) => (&name[..i], &name[i + 1..]), - None => (name, "") - } - }) - .split2(); - - // Now record all of the dynamic parameters. - let (name, matcher, ident, init_expr, push_expr, finalize_expr) = query_segments.iter() - .filter(|s| s.is_dynamic()) - .map(|s| (s, s.name.name(), route.find_input(&s.name).expect("dynamic has input"))) - .map(|(seg, name, (_, ident, ty))| { - let matcher = match seg.kind { - Kind::Multi => quote_spanned!(seg.span => _), - _ => quote_spanned!(seg.span => #name) - }; - - let span = ty.span(); - define_spanned_export!(span => FromForm, _form); - - let ty = quote_spanned!(span => <#ty as #FromForm>); - let i = ident.clone().with_span(span); - let init = quote_spanned!(span => #ty::init(#_form::Options::Lenient)); - let finalize = quote_spanned!(span => #ty::finalize(#i)); - let push = match seg.kind { - Kind::Multi => quote_spanned!(span => #ty::push_value(&mut #i, _f)), - _ => quote_spanned!(span => #ty::push_value(&mut #i, _f.shift())), - }; - - (name, matcher, ident, init, push, finalize) - }) - .split6(); - - #[allow(non_snake_case)] - Some(quote! { - let mut _e = #_form::Errors::new(); - #(let mut #ident = #init_expr;)* - - for _f in #__req.query_fields() { - let _raw = (_f.name.source().as_str(), _f.value); - let _key = _f.name.key_lossy().as_str(); - match (_raw, _key) { - // Skip static parameters so doesn't see them. - #(((#raw_name, #raw_value), _) => { /* skip */ },)* - #((_, #matcher) => #push_expr,)* - _ => { /* in case we have no trailing, ignore all else */ }, - } - } - - #( - let #ident = match #finalize_expr { - #_Ok(_v) => #_Some(_v), - #_Err(_err) => { - _e.extend(_err.with_name(#_form::NameView::new(#name))); - #_None - }, - }; - )* - - if !_e.is_empty() { - #_log::warn_("query string failed to match declared route"); - for _err in _e { #_log::warn_(_err); } - return #Outcome::Forward(#__data); - } - - #(let #ident = #ident.unwrap();)* - }) -} - -fn request_guard_expr(ident: &syn::Ident, ty: &syn::Type) -> TokenStream { - let span = ident.span().join(ty.span()).unwrap_or_else(|| ty.span()); - define_spanned_export!(span => __req, __data, _request, Outcome); - quote_spanned! { span => - let #ident: #ty = match <#ty as #_request::FromRequest>::from_request(#__req).await { - #Outcome::Success(__v) => __v, - #Outcome::Forward(_) => return #Outcome::Forward(#__data), - #Outcome::Failure((__c, _)) => return #Outcome::Failure(__c), - }; - } -} - -fn generate_internal_uri_macro(route: &Route) -> TokenStream { - // Keep a global counter (+ thread ID later) to generate unique ids. - static COUNTER: AtomicUsize = AtomicUsize::new(0); - - let dynamic_args = route.segments.iter() - .filter(|seg| seg.source == Source::Path || seg.source == Source::Query) - .filter(|seg| seg.kind != Kind::Static) - .map(|seg| &seg.name) - .map(|seg_name| route.find_input(seg_name).unwrap()) - .map(|(name, _, ty)| (name.ident(), ty)) - .map(|(ident, ty)| quote!(#ident: #ty)); - - let mut hasher = DefaultHasher::new(); - route.function.sig.ident.hash(&mut hasher); - route.attribute.path.origin.0.path().hash(&mut hasher); - std::process::id().hash(&mut hasher); - std::thread::current().id().hash(&mut hasher); - COUNTER.fetch_add(1, Ordering::AcqRel).hash(&mut hasher); - - let generated_macro_name = route.function.sig.ident.prepend(URI_MACRO_PREFIX); - let inner_generated_macro_name = generated_macro_name.append(&hasher.finish().to_string()); - let route_uri = route.attribute.path.origin.0.to_string(); - - quote_spanned! { Span::call_site() => - #[doc(hidden)] - #[macro_export] - macro_rules! #inner_generated_macro_name { - ($($token:tt)*) => {{ - extern crate std; - extern crate rocket; - rocket::rocket_internal_uri!(#route_uri, (#(#dynamic_args),*), $($token)*) - }}; - } - - #[doc(hidden)] - pub use #inner_generated_macro_name as #generated_macro_name; - } -} - -fn generate_respond_expr(route: &Route) -> TokenStream { - let ret_span = match route.function.sig.output { - syn::ReturnType::Default => route.function.sig.ident.span(), - syn::ReturnType::Type(_, ref ty) => ty.span().into() - }; - - define_spanned_export!(ret_span => __req, _handler); - let user_handler_fn_name = &route.function.sig.ident; - let parameter_names = route.inputs.iter() - .map(|(_, rocket_ident, _)| rocket_ident); - - let _await = route.function.sig.asyncness - .map(|a| quote_spanned!(a.span().into() => .await)); - - let responder_stmt = quote_spanned! { ret_span => - let ___responder = #user_handler_fn_name(#(#parameter_names),*) #_await; - }; - - quote_spanned! { ret_span => - #responder_stmt - #_handler::Outcome::from(#__req, ___responder) - } -} - -fn codegen_route(route: Route) -> Result { - // Generate the declarations for path, data, and request guard parameters. - let mut data_stmt = None; - let mut req_guard_definitions = vec![]; - let mut parameter_definitions = vec![]; - for (name, rocket_ident, ty) in &route.inputs { - let fn_segment: Segment = name.ident().into(); - match route.segments.get(&fn_segment) { - Some(seg) if seg.source == Source::Path => { - parameter_definitions.push(param_expr(seg, rocket_ident, &ty)); - } - Some(seg) if seg.source == Source::Data => { - // the data statement needs to come last, so record it specially - data_stmt = Some(data_expr(rocket_ident, &ty)); - } - Some(_) => continue, // handle query parameters later - None => { - req_guard_definitions.push(request_guard_expr(rocket_ident, &ty)); - } - }; - } - - // Generate the declarations for query parameters. - if let Some(exprs) = query_exprs(&route) { - parameter_definitions.push(exprs); - } - - // Gather everything we need. - use crate::exports::{ - __req, __data, _Box, Request, Data, Route, StaticRouteInfo, HandlerFuture - }; - - let (vis, user_handler_fn) = (&route.function.vis, &route.function); - let user_handler_fn_name = &user_handler_fn.sig.ident; - let generated_internal_uri_macro = generate_internal_uri_macro(&route); - let generated_respond_expr = generate_respond_expr(&route); - - let method = route.attribute.method; - let path = route.attribute.path.origin.0.to_string(); - let rank = Optional(route.attribute.rank); - let format = Optional(route.attribute.format); - - Ok(quote! { - #user_handler_fn - - #[doc(hidden)] - #[allow(non_camel_case_types)] - /// Rocket code generated proxy structure. - #vis struct #user_handler_fn_name { } - - /// Rocket code generated proxy static conversion implementation. - impl From<#user_handler_fn_name> for #StaticRouteInfo { - #[allow(non_snake_case, unreachable_patterns, unreachable_code)] - fn from(_: #user_handler_fn_name) -> #StaticRouteInfo { - fn monomorphized_function<'_b>( - #__req: &'_b #Request<'_>, - #__data: #Data - ) -> #HandlerFuture<'_b> { - #_Box::pin(async move { - #(#req_guard_definitions)* - #(#parameter_definitions)* - #data_stmt - - #generated_respond_expr - }) - } - - #StaticRouteInfo { - name: stringify!(#user_handler_fn_name), - method: #method, - path: #path, - handler: monomorphized_function, - format: #format, - rank: #rank, - } - } - } - - /// Rocket code generated proxy conversion implementation. - impl From<#user_handler_fn_name> for #Route { - #[inline] - fn from(_: #user_handler_fn_name) -> #Route { - #StaticRouteInfo::from(#user_handler_fn_name {}).into() - } - } - - /// Rocket code generated wrapping URI macro. - #generated_internal_uri_macro - }.into()) -} - -fn complete_route(args: TokenStream, input: TokenStream) -> Result { - let function: syn::ItemFn = syn::parse2(input) - .map_err(|e| Diagnostic::from(e)) - .map_err(|diag| diag.help("`#[route]` can only be used on functions"))?; - - let attr_tokens = quote!(route(#args)); - let attribute = RouteAttribute::from_meta(&syn::parse2(attr_tokens)?)?; - codegen_route(parse_route(attribute, function)?) -} - -fn incomplete_route( - method: crate::http::Method, - args: TokenStream, - input: TokenStream -) -> Result { - let method_str = method.to_string().to_lowercase(); - // FIXME(proc_macro): there should be a way to get this `Span`. - let method_span = StringLit::new(format!("#[{}]", method), Span::call_site()) - .subspan(2..2 + method_str.len()); - - let method_ident = syn::Ident::new(&method_str, method_span.into()); - - let function: syn::ItemFn = syn::parse2(input) - .map_err(|e| Diagnostic::from(e)) - .map_err(|d| d.help(format!("#[{}] can only be used on functions", method_str)))?; - - let full_attr = quote!(#method_ident(#args)); - let method_attribute = MethodRouteAttribute::from_meta(&syn::parse2(full_attr)?)?; - - let attribute = RouteAttribute { - method: SpanWrapped { - full_span: method_span, span: method_span, value: Method(method) - }, - path: method_attribute.path, - data: method_attribute.data, - format: method_attribute.format, - rank: method_attribute.rank, - }; - - codegen_route(parse_route(attribute, function)?) -} - -pub fn route_attribute>>( - method: M, - args: proc_macro::TokenStream, - input: proc_macro::TokenStream -) -> TokenStream { - let result = match method.into() { - Some(method) => incomplete_route(method, args.into(), input.into()), - None => complete_route(args.into(), input.into()) - }; - - result.unwrap_or_else(|diag| diag.emit_as_item_tokens()) -} diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs new file mode 100644 index 00000000..b0778544 --- /dev/null +++ b/core/codegen/src/attribute/route/mod.rs @@ -0,0 +1,353 @@ +mod parse; + +use proc_macro2::{TokenStream, Span}; +use devise::{Spanned, SpanWrapped, Result, FromMeta, Diagnostic}; + +use crate::{proc_macro2, syn}; +use crate::proc_macro_ext::StringLit; +use crate::syn_ext::IdentExt; +use crate::http_codegen::{Method, Optional}; + +use crate::attribute::param::Guard; +use parse::{Route, Attribute, MethodAttribute}; + +impl Route { + pub fn param_guards(&self) -> impl Iterator { + self.path_params.iter().filter_map(|p| p.guard()) + } + + pub fn query_guards(&self) -> impl Iterator { + self.query_params.iter().filter_map(|p| p.guard()) + } +} + +fn query_decls(route: &Route) -> Option { + use devise::ext::{Split2, Split6}; + + define_spanned_export!(Span::call_site() => + __req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None + ); + + // Record all of the static parameters for later filtering. + let (raw_name, raw_value) = route.query_params.iter() + .filter_map(|s| s.r#static()) + .map(|name| match name.find('=') { + Some(i) => (&name[..i], &name[i + 1..]), + None => (name.as_str(), "") + }) + .split2(); + + // Now record all of the dynamic parameters. + let (name, matcher, ident, init_expr, push_expr, finalize_expr) = route.query_guards() + .map(|guard| { + let (name, ty) = (&guard.name, &guard.ty); + let ident = guard.fn_ident.rocketized().with_span(ty.span()); + let matcher = match guard.trailing { + true => quote_spanned!(name.span() => _), + _ => quote!(#name) + }; + + define_spanned_export!(ty.span() => FromForm, _form); + + let ty = quote_spanned!(ty.span() => <#ty as #FromForm>); + let init = quote_spanned!(ty.span() => #ty::init(#_form::Options::Lenient)); + let finalize = quote_spanned!(ty.span() => #ty::finalize(#ident)); + let push = match guard.trailing { + true => quote_spanned!(ty.span() => #ty::push_value(&mut #ident, _f)), + _ => quote_spanned!(ty.span() => #ty::push_value(&mut #ident, _f.shift())), + }; + + (name, matcher, ident, init, push, finalize) + }) + .split6(); + + #[allow(non_snake_case)] + Some(quote! { + let mut _e = #_form::Errors::new(); + #(let mut #ident = #init_expr;)* + + for _f in #__req.query_fields() { + let _raw = (_f.name.source().as_str(), _f.value); + let _key = _f.name.key_lossy().as_str(); + match (_raw, _key) { + // Skip static parameters so doesn't see them. + #(((#raw_name, #raw_value), _) => { /* skip */ },)* + #((_, #matcher) => #push_expr,)* + _ => { /* in case we have no trailing, ignore all else */ }, + } + } + + #( + let #ident = match #finalize_expr { + #_Ok(_v) => #_Some(_v), + #_Err(_err) => { + _e.extend(_err.with_name(#_form::NameView::new(#name))); + #_None + }, + }; + )* + + if !_e.is_empty() { + #_log::warn_("query string failed to match declared route"); + for _err in _e { #_log::warn_(_err); } + return #Outcome::Forward(#__data); + } + + #(let #ident = #ident.unwrap();)* + }) +} + +fn request_guard_decl(guard: &Guard) -> TokenStream { + let (ident, ty) = (guard.fn_ident.rocketized(), &guard.ty); + define_spanned_export!(ty.span() => __req, __data, _request, FromRequest, Outcome); + quote_spanned! { ty.span() => + let #ident: #ty = match <#ty as #FromRequest>::from_request(#__req).await { + #Outcome::Success(__v) => __v, + #Outcome::Forward(_) => return #Outcome::Forward(#__data), + #Outcome::Failure((__c, _)) => return #Outcome::Failure(__c), + }; + } +} + +fn param_guard_decl(guard: &Guard) -> TokenStream { + let (i, name, ty) = (guard.index, &guard.name, &guard.ty); + define_spanned_export!(ty.span() => + __req, __data, _log, _None, _Some, _Ok, _Err, + Outcome, FromSegments, FromParam + ); + + // Returned when a dynamic parameter fails to parse. + let parse_error = quote!({ + #_log::warn_(&format!("Failed to parse '{}': {:?}", #name, __error)); + #Outcome::Forward(#__data) + }); + + // All dynamic parameters should be found if this function is being called; + // that's the point of statically checking the URI parameters. + let expr = match guard.trailing { + false => quote_spanned! { ty.span() => + match #__req.routed_segment(#i) { + #_Some(__s) => match <#ty as #FromParam>::from_param(__s) { + #_Ok(__v) => __v, + #_Err(__error) => return #parse_error, + }, + #_None => { + #_log::error("Internal invariant: dynamic parameter not found."); + #_log::error("Please report this error to the Rocket issue tracker."); + return #Outcome::Forward(#__data); + } + } + }, + true => quote_spanned! { ty.span() => + match <#ty as #FromSegments>::from_segments(#__req.routed_segments(#i..)) { + #_Ok(__v) => __v, + #_Err(__error) => return #parse_error, + } + }, + }; + + let ident = guard.fn_ident.rocketized(); + quote!(let #ident: #ty = #expr;) +} + +fn data_guard_decl(guard: &Guard) -> TokenStream { + let (ident, ty) = (guard.fn_ident.rocketized(), &guard.ty); + define_spanned_export!(ty.span() => __req, __data, FromData, Outcome); + + quote_spanned! { ty.span() => + let #ident: #ty = match <#ty as #FromData>::from_data(#__req, #__data).await { + #Outcome::Success(__d) => __d, + #Outcome::Forward(__d) => return #Outcome::Forward(__d), + #Outcome::Failure((__c, _)) => return #Outcome::Failure(__c), + }; + } +} + +fn internal_uri_macro_decl(route: &Route) -> TokenStream { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + // Keep a global counter (+ thread ID later) to generate unique ids. + static COUNTER: AtomicUsize = AtomicUsize::new(0); + + // FIXME: Is this the right order? Does order matter? + let uri_args = route.param_guards() + .chain(route.query_guards()) + .map(|guard| (&guard.fn_ident, &guard.ty)) + .map(|(ident, ty)| quote!(#ident: #ty)); + + // Generate entropy based on the route's metadata. + let mut hasher = DefaultHasher::new(); + route.handler.sig.ident.hash(&mut hasher); + route.attr.uri.path().hash(&mut hasher); + route.attr.uri.query().hash(&mut hasher); + std::process::id().hash(&mut hasher); + std::thread::current().id().hash(&mut hasher); + COUNTER.fetch_add(1, Ordering::AcqRel).hash(&mut hasher); + + let macro_name = route.handler.sig.ident.prepend(crate::URI_MACRO_PREFIX); + let inner_macro_name = macro_name.append(&hasher.finish().to_string()); + let route_uri = route.attr.uri.to_string(); + + quote_spanned! { Span::call_site() => + #[doc(hidden)] + #[macro_export] + /// Rocket generated URI macro. + macro_rules! #inner_macro_name { + ($($token:tt)*) => {{ + extern crate std; + extern crate rocket; + rocket::rocket_internal_uri!(#route_uri, (#(#uri_args),*), $($token)*) + }}; + } + + #[doc(hidden)] + pub use #inner_macro_name as #macro_name; + } +} + +fn responder_outcome_expr(route: &Route) -> TokenStream { + let ret_span = match route.handler.sig.output { + syn::ReturnType::Default => route.handler.sig.ident.span(), + syn::ReturnType::Type(_, ref ty) => ty.span().into() + }; + + let user_handler_fn_name = &route.handler.sig.ident; + let parameter_names = route.arguments.map.values() + .map(|(ident, _)| ident.rocketized()); + + let _await = route.handler.sig.asyncness + .map(|a| quote_spanned!(a.span().into() => .await)); + + define_spanned_export!(ret_span => __req, _handler); + quote_spanned! { ret_span => + let ___responder = #user_handler_fn_name(#(#parameter_names),*) #_await; + #_handler::Outcome::from(#__req, ___responder) + } +} + +fn codegen_route(route: Route) -> Result { + use crate::exports::*; + + // Generate the declarations for all of the guards. + let request_guards = route.request_guards.iter().map(request_guard_decl); + let param_guards = route.param_guards().map(param_guard_decl); + let query_guards = query_decls(&route); + let data_guard = route.data_guard.as_ref().map(data_guard_decl); + + // Gather info about the function. + let (vis, handler_fn) = (&route.handler.vis, &route.handler); + let handler_fn_name = &handler_fn.sig.ident; + let internal_uri_macro = internal_uri_macro_decl(&route); + let responder_outcome = responder_outcome_expr(&route); + + let method = route.attr.method; + let path = route.attr.uri.to_string(); + let rank = Optional(route.attr.rank); + let format = Optional(route.attr.format.as_ref()); + + Ok(quote! { + #handler_fn + + #[doc(hidden)] + #[allow(non_camel_case_types)] + /// Rocket code generated proxy structure. + #vis struct #handler_fn_name { } + + /// Rocket code generated proxy static conversion implementation. + impl From<#handler_fn_name> for #StaticRouteInfo { + #[allow(non_snake_case, unreachable_patterns, unreachable_code)] + fn from(_: #handler_fn_name) -> #StaticRouteInfo { + fn monomorphized_function<'_b>( + #__req: &'_b #Request, + #__data: #Data + ) -> #HandlerFuture<'_b> { + #_Box::pin(async move { + #(#request_guards)* + #(#param_guards)* + #query_guards + #data_guard + + #responder_outcome + }) + } + + #StaticRouteInfo { + name: stringify!(#handler_fn_name), + method: #method, + path: #path, + handler: monomorphized_function, + format: #format, + rank: #rank, + } + } + } + + /// Rocket code generated proxy conversion implementation. + impl From<#handler_fn_name> for #Route { + #[inline] + fn from(_: #handler_fn_name) -> #Route { + #StaticRouteInfo::from(#handler_fn_name {}).into() + } + } + + /// Rocket code generated wrapping URI macro. + #internal_uri_macro + }.into()) +} + +fn complete_route(args: TokenStream, input: TokenStream) -> Result { + let function: syn::ItemFn = syn::parse2(input) + .map_err(|e| Diagnostic::from(e)) + .map_err(|diag| diag.help("`#[route]` can only be used on functions"))?; + + let attr_tokens = quote!(route(#args)); + let attribute = Attribute::from_meta(&syn::parse2(attr_tokens)?)?; + codegen_route(Route::from(attribute, function)?) +} + +fn incomplete_route( + method: crate::http::Method, + args: TokenStream, + input: TokenStream +) -> Result { + let method_str = method.to_string().to_lowercase(); + // FIXME(proc_macro): there should be a way to get this `Span`. + let method_span = StringLit::new(format!("#[{}]", method), Span::call_site()) + .subspan(2..2 + method_str.len()); + + let method_ident = syn::Ident::new(&method_str, method_span.into()); + + let function: syn::ItemFn = syn::parse2(input) + .map_err(|e| Diagnostic::from(e)) + .map_err(|d| d.help(format!("#[{}] can only be used on functions", method_str)))?; + + let full_attr = quote!(#method_ident(#args)); + let method_attribute = MethodAttribute::from_meta(&syn::parse2(full_attr)?)?; + + let attribute = Attribute { + method: SpanWrapped { + full_span: method_span, span: method_span, value: Method(method) + }, + uri: method_attribute.uri, + data: method_attribute.data, + format: method_attribute.format, + rank: method_attribute.rank, + }; + + codegen_route(Route::from(attribute, function)?) +} + +pub fn route_attribute>>( + method: M, + args: proc_macro::TokenStream, + input: proc_macro::TokenStream +) -> TokenStream { + let result = match method.into() { + Some(method) => incomplete_route(method, args.into(), input.into()), + None => complete_route(args.into(), input.into()) + }; + + result.unwrap_or_else(|diag| diag.emit_as_item_tokens()) +} diff --git a/core/codegen/src/attribute/route/parse.rs b/core/codegen/src/attribute/route/parse.rs new file mode 100644 index 00000000..222a362d --- /dev/null +++ b/core/codegen/src/attribute/route/parse.rs @@ -0,0 +1,226 @@ +use devise::{syn, Spanned, SpanWrapped, Result, FromMeta}; +use devise::ext::{SpanDiagnosticExt, TypeExt}; +use indexmap::{IndexSet, IndexMap}; + +use crate::proc_macro_ext::Diagnostics; +use crate::http_codegen::{Method, MediaType}; +use crate::attribute::param::{Parameter, Dynamic, Guard}; +use crate::syn_ext::FnArgExt; + +use crate::name::Name; +use crate::proc_macro2::Span; +use crate::http::ext::IntoOwned; +use crate::http::uri::{self, Origin}; + +/// This structure represents the parsed `route` attribute and associated items. +#[derive(Debug)] +pub struct Route { + /// The attribute: `#[get(path, ...)]`. + pub attr: Attribute, + /// The static and dynamic path parameters. + pub path_params: Vec, + /// The static and dynamic query parameters. + pub query_params: Vec, + /// The data guard, if any. + pub data_guard: Option, + /// The request guards. + pub request_guards: Vec, + /// The decorated function: the handler. + pub handler: syn::ItemFn, + /// The parsed arguments to the user's function. + pub arguments: Arguments, +} + +type ArgumentMap = IndexMap; + +#[derive(Debug)] +pub struct Arguments { + pub span: Span, + pub map: ArgumentMap +} + +/// The parsed `#[route(..)]` attribute. +#[derive(Debug, FromMeta)] +pub struct Attribute { + #[meta(naked)] + pub method: SpanWrapped, + pub uri: RouteUri, + pub data: Option>, + pub format: Option, + pub rank: Option, +} + +/// The parsed `#[method(..)]` (e.g, `get`, `put`, etc.) attribute. +#[derive(Debug, FromMeta)] +pub struct MethodAttribute { + #[meta(naked)] + pub uri: RouteUri, + pub data: Option>, + pub format: Option, + pub rank: Option, +} + +#[derive(Debug)] +pub struct RouteUri { + origin: Origin<'static>, + path_span: Span, + query_span: Option, +} + +impl FromMeta for RouteUri { + fn from_meta(meta: &devise::MetaItem) -> Result { + let string = crate::proc_macro_ext::StringLit::from_meta(meta)?; + + let origin = Origin::parse_route(&string) + .map_err(|e| { + let span = string.subspan(e.index() + 1..(e.index() + 2)); + span.error(format!("invalid route URI: {}", e)) + .help("expected URI in origin form: \"/path/\"") + })?; + + if !origin.is_normalized() { + let normalized = origin.clone().into_normalized(); + let span = origin.path().find("//") + .or_else(|| origin.query() + .and_then(|q| q.find("&&")) + .map(|i| origin.path().len() + 1 + i)) + .map(|i| string.subspan((1 + i)..(1 + i + 2))) + .unwrap_or(string.span()); + + return Err(span.error("route URIs cannot contain empty segments") + .note(format!("expected \"{}\", found \"{}\"", normalized, origin))); + } + + let path_span = string.subspan(1..origin.path().len() + 1); + let query_span = origin.query().map(|q| { + let len_to_q = 1 + origin.path().len() + 1; + let end_of_q = len_to_q + q.len(); + string.subspan(len_to_q..end_of_q) + }); + + Ok(RouteUri { origin: origin.into_owned(), path_span, query_span }) + } +} + +impl Route { + pub fn upgrade_param(param: Parameter, args: &Arguments) -> Result { + if !param.dynamic().is_some() { + return Ok(param); + } + + let param = param.take_dynamic().expect("dynamic() => take_dynamic()"); + Route::upgrade_dynamic(param, args).map(Parameter::Guard) + } + + pub fn upgrade_dynamic(param: Dynamic, args: &Arguments) -> Result { + if let Some((ident, ty)) = args.map.get(¶m.name) { + Ok(Guard::from(param, ident.clone(), ty.clone())) + } else { + let msg = format!("expected argument named `{}` here", param.name); + let diag = param.span().error("unused parameter").span_note(args.span, msg); + Err(diag) + } + } + + pub fn from(attr: Attribute, handler: syn::ItemFn) -> Result { + // Collect diagnostics as we proceed. + let mut diags = Diagnostics::new(); + + // Emit a warning if a `data` param was supplied for non-payload methods. + if let Some(ref data) = attr.data { + if !attr.method.0.supports_payload() { + let msg = format!("'{}' does not typically support payloads", attr.method.0); + // FIXME(diag: warning) + data.full_span.warning("`data` used with non-payload-supporting method") + .span_note(attr.method.span, msg) + .emit_as_item_tokens(); + } + } + + // Check the validity of function arguments. + let span = handler.sig.paren_token.span; + let mut arguments = Arguments { map: ArgumentMap::new(), span }; + for arg in &handler.sig.inputs { + if let Some((ident, ty)) = arg.typed() { + let value = (ident.clone(), ty.with_stripped_lifetimes()); + arguments.map.insert(Name::from(ident), value); + } else { + let error = match arg.wild() { + Some(_) => "handler arguments cannot be ignored", + None => "handler arguments must be of the form `ident: Type`" + }; + + let diag = arg.span() + .error(error) + .note("handler arguments must be of the form: `ident: Type`"); + + diags.push(diag); + } + } + + // Parse and collect the path parameters. + let (source, span) = (attr.uri.path(), attr.uri.path_span); + let path_params = Parameter::parse_many::(source.as_str(), span) + .map(|p| Route::upgrade_param(p?, &arguments)) + .filter_map(|p| p.map_err(|e| diags.push(e.into())).ok()) + .collect::>(); + + // Parse and collect the query parameters. + let query_params = match (attr.uri.query(), attr.uri.query_span) { + (Some(r), Some(span)) => Parameter::parse_many::(r.as_str(), span) + .map(|p| Route::upgrade_param(p?, &arguments)) + .filter_map(|p| p.map_err(|e| diags.push(e.into())).ok()) + .collect::>(), + _ => vec![] + }; + + // Remove the `SpanWrapped` layer and upgrade to a guard. + let data_guard = attr.data.clone() + .map(|p| Route::upgrade_dynamic(p.value, &arguments)) + .and_then(|p| p.map_err(|e| diags.push(e.into())).ok()); + + // Collect all of the declared dynamic route parameters. + let all_dyn_params = path_params.iter().filter_map(|p| p.dynamic()) + .chain(query_params.iter().filter_map(|p| p.dynamic())) + .chain(data_guard.as_ref().map(|g| &g.source).into_iter()); + + // Check for any duplicates in the dynamic route parameters. + let mut dyn_params: IndexSet<&Dynamic> = IndexSet::new(); + for p in all_dyn_params { + if let Some(prev) = dyn_params.replace(p) { + diags.push(p.span().error(format!("duplicate parameter: `{}`", p.name)) + .span_note(prev.span(), "previous parameter with the same name here")) + } + } + + // Collect the request guards: all the arguments not already a guard. + let request_guards = arguments.map.iter() + .filter(|(name, _)| { + let mut all_other_guards = path_params.iter().filter_map(|p| p.guard()) + .chain(query_params.iter().filter_map(|p| p.guard())) + .chain(data_guard.as_ref().into_iter()); + + all_other_guards.all(|g| g.name != name) + }) + .enumerate() + .map(|(index, (name, (ident, ty)))| Guard { + source: Dynamic { index, name: name.clone(), trailing: false }, + fn_ident: ident.clone(), + ty: ty.clone(), + }) + .collect(); + + diags.head_err_or(Route { + attr, path_params, query_params, data_guard, request_guards, + handler, arguments, + }) + } +} + +impl std::ops::Deref for RouteUri { + type Target = Origin<'static>; + + fn deref(&self) -> &Self::Target { + &self.origin + } +} diff --git a/core/codegen/src/attribute/segments.rs b/core/codegen/src/attribute/segments.rs deleted file mode 100644 index fc501ee1..00000000 --- a/core/codegen/src/attribute/segments.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::hash::{Hash, Hasher}; - -use devise::{syn, Diagnostic, ext::SpanDiagnosticExt}; -use crate::proc_macro2::Span; - -use crate::http::RawStr; -use crate::http::uri::{self, UriPart}; -use crate::http::route::RouteSegment; -use crate::proc_macro_ext::{Diagnostics, StringLit, PResult, DResult}; -use crate::syn_ext::NameSource; - -pub use crate::http::route::{Error, Kind}; - -#[derive(Debug, Clone)] -pub struct Segment { - pub span: Span, - pub kind: Kind, - pub source: Source, - pub name: NameSource, - pub index: Option, -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum Source { - Path, - Query, - Data, - Unknown, -} - -impl Segment { - fn from(segment: RouteSegment<'_, P>, span: Span) -> Segment { - let source = match P::DELIMITER { - '/' => Source::Path, - '&' => Source::Query, - _ => unreachable!("only paths and queries") - }; - - let (kind, index) = (segment.kind, segment.index); - Segment { span, kind, source, index, name: NameSource::new(&segment.name, span) } - } - - pub fn is_wild(&self) -> bool { - self.name == "_" - } - - pub fn is_dynamic(&self) -> bool { - match self.kind { - Kind::Static => false, - Kind::Single | Kind::Multi => true, - } - } -} - -impl From<&syn::Ident> for Segment { - fn from(ident: &syn::Ident) -> Segment { - Segment { - kind: Kind::Static, - source: Source::Unknown, - span: ident.span(), - name: ident.clone().into(), - index: None, - } - } -} - -impl PartialEq for Segment { - fn eq(&self, other: &Segment) -> bool { - self.name == other.name - } -} - -impl Eq for Segment { } - -impl Hash for Segment { - fn hash(&self, state: &mut H) { - self.name.hash(state); - } -} - -fn subspan(needle: &str, haystack: &str, span: Span) -> Span { - let index = needle.as_ptr() as usize - haystack.as_ptr() as usize; - StringLit::new(haystack, span).subspan(index..index + needle.len()) -} - -fn trailspan(needle: &str, haystack: &str, span: Span) -> Span { - let index = needle.as_ptr() as usize - haystack.as_ptr() as usize; - let lit = StringLit::new(haystack, span); - if needle.as_ptr() as usize > haystack.as_ptr() as usize { - lit.subspan((index - 1)..) - } else { - lit.subspan(index..) - } -} - -fn into_diagnostic( - segment: &str, // The segment that failed. - source: &str, // The haystack where `segment` can be found. - span: Span, // The `Span` of `Source`. - error: &Error<'_>, // The error. -) -> Diagnostic { - let seg_span = subspan(segment, source, span); - match error { - Error::Empty => seg_span.error(error.to_string()), - Error::Ident(_) => { - seg_span.error(error.to_string()) - .help("parameter names must be valid identifiers") - } - Error::Ignored => { - seg_span.error(error.to_string()) - .help("use a name such as `_guard` or `_param`") - } - Error::MissingClose => { - seg_span.error(error.to_string()) - .help(format!("did you mean '{}>'?", segment)) - } - Error::Malformed => { - seg_span.error(error.to_string()) - .help("parameters must be of the form ''") - .help("identifiers cannot contain '<' or '>'") - } - Error::Uri => { - seg_span.error(error.to_string()) - .note("components cannot contain reserved characters") - .help("reserved characters include: '%', '+', '&', etc.") - } - Error::Trailing(multi) => { - let multi_span = subspan(multi, source, span); - trailspan(segment, source, span) - .error(error.to_string()) - .help("a multi-segment param must be the final component") - .span_note(multi_span, "multi-segment param is here") - } - } -} - -pub fn parse_data_segment(segment: &str, span: Span) -> PResult { - >::parse_one(segment) - .map(|segment| { - let mut seg = Segment::from(segment, span); - seg.source = Source::Data; - seg.index = Some(0); - seg - }) - .map_err(|e| into_diagnostic(segment, segment, span, &e)) -} - -pub fn parse_segments( - string: &RawStr, - span: Span -) -> DResult> { - let mut segments = vec![]; - let mut diags = Diagnostics::new(); - - for result in >::parse_many(string) { - match result { - Ok(segment) => { - let seg_span = subspan(&segment.string, string.as_str(), span); - segments.push(Segment::from(segment, seg_span)); - }, - Err((segment_string, error)) => { - diags.push(into_diagnostic(segment_string, string.as_str(), span, &error)); - if let Error::Trailing(..) = error { - break; - } - } - } - } - - diags.err_or(segments) -} diff --git a/core/codegen/src/bang/uri.rs b/core/codegen/src/bang/uri.rs index 10647c4e..4490a5ef 100644 --- a/core/codegen/src/bang/uri.rs +++ b/core/codegen/src/bang/uri.rs @@ -3,15 +3,14 @@ use std::fmt::Display; use devise::{syn, Result}; use devise::ext::{SpanDiagnosticExt, quote_respanned}; -use crate::http::{uri::{Origin, Path, Query}, ext::IntoOwned}; -use crate::http::route::{RouteSegment, Kind}; -use crate::attribute::segments::Source; - +use crate::http::uri; use crate::syn::{Expr, Ident, Type, spanned::Spanned}; use crate::http_codegen::Optional; use crate::syn_ext::IdentExt; use crate::bang::uri_parsing::*; use crate::proc_macro2::TokenStream; +use crate::attribute::param::Parameter; +use crate::exports::_uri; use crate::URI_MACRO_PREFIX; @@ -40,42 +39,41 @@ pub fn _uri_macro(input: TokenStream) -> Result { } fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( - impl Iterator, - impl Iterator, + impl Iterator, // path exprs + impl Iterator, // query exprs + impl Iterator, // types for both path || query )> { let route_name = &internal.uri_params.route_path; match internal.validate() { Validation::Ok(exprs) => { - let path_param_count = internal.route_uri.path().as_str().matches('<').count(); + let path_params = internal.dynamic_path_params(); + let path_param_count = path_params.clone().count(); for expr in exprs.iter().take(path_param_count) { if !expr.as_expr().is_some() { return Err(expr.span().error("path parameters cannot be ignored")); } } - // Create an iterator over all `ident`, `ty`, and `expr` triples. - let arguments = internal.fn_args.iter() - .zip(exprs.into_iter()) - .map(|(FnArg { ident, ty }, expr)| (ident, ty, expr)); + let query_exprs = exprs.clone().into_iter().skip(path_param_count); + let path_exprs = exprs.into_iter().map(|e| e.unwrap_expr()).take(path_param_count); + let types = internal.fn_args.iter().map(|a| (&a.ident, &a.ty)); + Ok((path_exprs, query_exprs, types)) + } + Validation::NamedIgnored(_) => { + let diag = internal.uri_params.args_span() + .error("expected unnamed arguments due to ignored parameters") + .note(format!("uri for route `{}` ignores path parameters: \"{}\"", + quote!(#route_name), internal.route_uri)); - // Create iterators for just the path and query parts. - let path_params = arguments.clone() - .take(path_param_count) - .map(|(i, t, e)| (i, t, e.unwrap_expr())); - - let query_params = arguments.skip(path_param_count); - Ok((path_params, query_params)) + Err(diag) } Validation::Unnamed(expected, actual) => { - let mut diag = internal.uri_params.args_span().error( - format!("`{}` route uri expects {} but {} supplied", quote!(#route_name), - p!(expected, "parameter"), p!(actual, "was"))); - - if expected > 0 { - let ps = p!("parameter", expected); - diag = diag.note(format!("expected {}: {}", ps, internal.fn_args_str())); - } + let diag = internal.uri_params.args_span() + .error(format!("expected {} but {} supplied", + p!(expected, "parameter"), p!(actual, "was"))) + .note(format!("route `{}` has uri \"{}\"", + quote!(#route_name), internal.route_uri)); Err(diag) } @@ -112,12 +110,11 @@ fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( } } -fn add_binding(to: &mut Vec, ident: &Ident, ty: &Type, expr: &Expr, source: Source) { +fn add_binding(to: &mut Vec, ident: &Ident, ty: &Type, expr: &Expr) { let span = expr.span(); - define_spanned_export!(span => _uri); - let part = match source { - Source::Query => quote_spanned!(span => #_uri::Query), - _ => quote_spanned!(span => #_uri::Path), + let part = match P::KIND { + uri::Kind::Path => quote_spanned!(span => #_uri::Path), + uri::Kind::Query => quote_spanned!(span => #_uri::Query), }; let tmp_ident = ident.clone().with_span(expr.span()); @@ -129,107 +126,117 @@ fn add_binding(to: &mut Vec, ident: &Ident, ty: &Type, expr: &Expr, )); } -fn explode_path<'a, I: Iterator>( - uri: &Origin<'_>, +fn explode_path<'a>( + internal: &InternalUriParams, bindings: &mut Vec, - mut items: I + mut exprs: impl Iterator, + mut args: impl Iterator, ) -> TokenStream { - let (uri_mod, path) = (quote!(rocket::http::uri), uri.path().as_str()); - if !path.contains('<') { - return quote!(#uri_mod::UriArgumentsKind::Static(#path)); + if internal.dynamic_path_params().count() == 0 { + let route_uri = &internal.route_uri; + if let Some(ref mount) = internal.uri_params.mount_point { + let full_uri = route_uri.map_path(|p| format!("{}/{}", mount, p)) + .expect("origin from path") + .into_normalized(); + + let path = full_uri.path().as_str(); + return quote!(#_uri::UriArgumentsKind::Static(#path)); + } else { + let path = route_uri.path().as_str(); + return quote!(#_uri::UriArgumentsKind::Static(#path)); + } } - let uri_display = quote!(#uri_mod::UriDisplay<#uri_mod::Path>); - let dyn_exprs = >::parse(uri).map(|segment| { - let segment = segment.expect("segment okay; prechecked on parse"); - match segment.kind { - Kind::Static => { - let string = &segment.string; - quote!(&#string as &dyn #uri_display) - } - Kind::Single | Kind::Multi => { - let (ident, ty, expr) = items.next().expect("one item for each dyn"); - add_binding(bindings, &ident, &ty, &expr, Source::Path); + let uri_display = quote!(#_uri::UriDisplay<#_uri::Path>); + let all_path_params = internal.mount_params.iter().chain(internal.path_params.iter()); + let dyn_exprs = all_path_params.map(|param| { + match param { + Parameter::Static(name) => { + quote!(&#name as &dyn #uri_display) + }, + Parameter::Dynamic(_) | Parameter::Guard(_) => { + let (ident, ty) = args.next().expect("ident/ty for non-ignored"); + let expr = exprs.next().expect("one expr per dynamic arg"); + add_binding::(bindings, &ident, &ty, &expr); quote_spanned!(expr.span() => &#ident as &dyn #uri_display) } + Parameter::Ignored(_) => { + let expr = exprs.next().expect("one expr per dynamic arg"); + quote_spanned!(expr.span() => &#expr as &dyn #uri_display) + } } }); - quote!(#uri_mod::UriArgumentsKind::Dynamic(&[#(#dyn_exprs),*])) + quote!(#_uri::UriArgumentsKind::Dynamic(&[#(#dyn_exprs),*])) } -fn explode_query<'a, I: Iterator>( - uri: &Origin<'_>, +fn explode_query<'a>( + internal: &InternalUriParams, bindings: &mut Vec, - mut items: I + mut arg_exprs: impl Iterator, + mut args: impl Iterator, ) -> Option { - let (uri_mod, query) = (quote!(rocket::http::uri), uri.query()?.as_str()); - if !query.contains('<') { - return Some(quote!(#uri_mod::UriArgumentsKind::Static(#query))); + let query = internal.route_uri.query()?.as_str(); + if internal.dynamic_query_params().count() == 0 { + return Some(quote!(#_uri::UriArgumentsKind::Static(#query))); } - let query_arg = quote!(#uri_mod::UriQueryArgument); - let uri_display = quote!(#uri_mod::UriDisplay<#uri_mod::Query>); - let dyn_exprs = >::parse(uri)?.filter_map(|segment| { - let segment = segment.expect("segment okay; prechecked on parse"); - if segment.kind == Kind::Static { - let string = &segment.string; - return Some(quote!(#query_arg::Raw(#string))); + let query_arg = quote!(#_uri::UriQueryArgument); + let uri_display = quote!(#_uri::UriDisplay<#_uri::Query>); + let dyn_exprs = internal.query_params.iter().filter_map(|param| { + if let Parameter::Static(source) = param { + return Some(quote!(#query_arg::Raw(#source))); } - let (ident, ty, arg_expr) = items.next().expect("one item for each dyn"); + let dynamic = match param { + Parameter::Static(source) => { + return Some(quote!(#query_arg::Raw(#source))); + }, + Parameter::Dynamic(ref seg) => seg, + Parameter::Guard(ref seg) => seg, + Parameter::Ignored(_) => unreachable!("invariant: unignorable q") + }; + + let (ident, ty) = args.next().expect("ident/ty for query"); + let arg_expr = arg_exprs.next().expect("one expr per query"); let expr = match arg_expr.as_expr() { Some(expr) => expr, None => { // Force a typecheck for the `Ignoreable` trait. Note that write // out the path to `is_ignorable` to get the right span. bindings.push(quote_respanned! { arg_expr.span() => - rocket::http::uri::assert_ignorable::<#uri_mod::Query, #ty>(); + rocket::http::uri::assert_ignorable::<#_uri::Query, #ty>(); }); return None; } }; - let name = &segment.name; - add_binding(bindings, &ident, &ty, &expr, Source::Query); - Some(match segment.kind { - Kind::Single => quote_spanned! { expr.span() => + let name = &dynamic.name; + add_binding::(bindings, &ident, &ty, &expr); + Some(match dynamic.trailing { + false => quote_spanned! { expr.span() => #query_arg::NameValue(#name, &#ident as &dyn #uri_display) }, - Kind::Multi => quote_spanned! { expr.span() => + true => quote_spanned! { expr.span() => #query_arg::Value(&#ident as &dyn #uri_display) }, - Kind::Static => unreachable!("Kind::Static returns early") }) }); - Some(quote!(#uri_mod::UriArgumentsKind::Dynamic(&[#(#dyn_exprs),*]))) -} - -// Returns an Origin URI with the mount point and route path concatenated. The -// query string is mangled by replacing single dynamic parameters in query parts -// (``) with `param=`. -fn build_origin(internal: &InternalUriParams) -> Origin<'static> { - let mount_point = internal.uri_params.mount_point.as_ref() - .map(|origin| origin.path().as_str()) - .unwrap_or(""); - - let path = format!("{}/{}", mount_point, internal.route_uri.path()); - let query = internal.route_uri.query().map(|q| q.as_str()); - Origin::new(path, query).into_normalized().into_owned() + Some(quote!(#_uri::UriArgumentsKind::Dynamic(&[#(#dyn_exprs),*]))) } pub fn _uri_internal_macro(input: TokenStream) -> Result { // Parse the internal invocation and the user's URI param expressions. let internal = syn::parse2::(input)?; - let (path_params, query_params) = extract_exprs(&internal)?; + let (path_exprs, query_exprs, mut fn_args) = extract_exprs(&internal)?; let mut bindings = vec![]; - let uri = build_origin(&internal); let uri_mod = quote!(rocket::http::uri); - let path = explode_path(&uri, &mut bindings, path_params); - let query = Optional(explode_query(&uri, &mut bindings, query_params)); + let path = explode_path(&internal, &mut bindings, path_exprs, &mut fn_args); + let query = explode_query(&internal, &mut bindings, query_exprs, fn_args); + let query = Optional(query); Ok(quote!({ #(#bindings)* diff --git a/core/codegen/src/bang/uri_parsing.rs b/core/codegen/src/bang/uri_parsing.rs index aa1d4329..020a6f92 100644 --- a/core/codegen/src/bang/uri_parsing.rs +++ b/core/codegen/src/bang/uri_parsing.rs @@ -6,9 +6,11 @@ use crate::syn::{self, Expr, Ident, LitStr, Path, Token, Type}; use crate::syn::parse::{self, Parse, ParseStream}; use crate::syn::punctuated::Punctuated; -use crate::http::{uri::Origin, ext::IntoOwned}; +use crate::http::{uri, uri::Origin, ext::IntoOwned}; use crate::proc_macro2::{TokenStream, Span}; -use crate::syn_ext::NameSource; +use crate::proc_macro_ext::StringLit; +use crate::attribute::param::{Parameter, Dynamic}; +use crate::name::Name; // TODO(diag): Use 'Diagnostic' in place of syn::Error. @@ -21,7 +23,7 @@ pub enum ArgExpr { #[derive(Debug)] pub enum Arg { Unnamed(ArgExpr), - Named(NameSource, Token![=], ArgExpr), + Named(Name, Ident, Token![=], ArgExpr), } #[derive(Debug)] @@ -49,31 +51,37 @@ pub struct FnArg { } pub enum Validation<'a> { + // Parameters that were ignored in a named argument setting. + NamedIgnored(Vec<&'a Dynamic>), // Number expected, what we actually got. Unnamed(usize, usize), // (Missing, Extra, Duplicate) - Named(Vec, Vec<&'a Ident>, Vec<&'a Ident>), + Named(Vec<&'a Name>, Vec<&'a Ident>, Vec<&'a Ident>), // Everything is okay; here are the expressions in the route decl order. Ok(Vec<&'a ArgExpr>) } // This is invoked by Rocket itself. The `uri!` macro expands to a call to a // route-specific macro which in-turn expands to a call to `internal_uri!`, -// passing along the user's parameters from the original `uri!` call. This is -// necessary so that we can converge the type information in the route (from the -// route-specific macro) with the user's parameters (by forwarding them to the -// internal_uri! call). +// passing along the user's parameters (`uri_params`) from the original `uri!` +// call. This is necessary so that we can converge the type information in the +// route (from the route-specific macro) with the user's parameters (by +// forwarding them to the internal_uri! call). // -// `fn_args` are the URI arguments (excluding guards) from the original route's -// handler in the order they were declared in the URI (`/`). -// `uri` is the full URI used in the origin route's attribute. +// `fn_args` are the URI arguments (excluding request guards and ignored path +// parts) from the original handler in the order they were declared in the URI +// (`/`). `route_uri` is the URI itself. // -// internal_uri!("//", (first: ty, second: ty), $($tt)*); -// ^--------|--------- ^-----------|---------| ^-----| -// route_uri fn_args uri_params +// internal_uri!("//<_>?lang=en&", (one: ty, two: ty), $($tt)*); +// ^----/----^ ^-----\-----^ ^-------/------^ ^-----| +// path_params query_params fn_args uri_params +// ^------ route_uri ------^ #[derive(Debug)] pub struct InternalUriParams { pub route_uri: Origin<'static>, + pub mount_params: Vec, + pub path_params: Vec, + pub query_params: Vec, pub fn_args: Vec, pub uri_params: UriParams, } @@ -95,7 +103,7 @@ impl Parse for Arg { let ident = input.parse::()?; let eq_token = input.parse::()?; let expr = input.parse::()?; - Ok(Arg::Named(ident.into(), eq_token, expr)) + Ok(Arg::Named(Name::from(&ident), ident, eq_token, expr)) } else { let expr = input.parse::()?; Ok(Arg::Unnamed(expr)) @@ -117,11 +125,13 @@ impl Parse for UriParams { // Parse the mount point and suffixing ',', if any. let mount_point = if input.peek(LitStr) { let string = input.parse::()?; - let mount_point = Origin::parse_owned(string.value()).map_err(|_| { - // TODO(proc_macro): use error, add example as a help - parse::Error::new(string.span(), "invalid mount point; \ + let mount_point = Origin::parse_owned(string.value()) + .map(|m| m.into_normalized()) + .map_err(|_| { + // TODO(proc_macro): use error, add example as a help + parse::Error::new(string.span(), "invalid mount point; \ mount points must be static, absolute URIs: `/example`") - })?; + })?; if !input.peek(Token![,]) && input.cursor().eof() { return err(string.span(), "unexpected end of input: \ @@ -192,7 +202,8 @@ impl Parse for InternalUriParams { // Validation should always succeed since this macro can only be called // if the route attribute succeeded, implying a valid route URI. - let route_uri = Origin::parse_route(&route_uri_str.value()) + let route_uri_str = StringLit::new(route_uri_str.value(), route_uri_str.span()); + let route_uri = Origin::parse_route(&route_uri_str) .map(|o| o.into_normalized().into_owned()) .map_err(|_| input.error("internal error: invalid route URI"))?; @@ -203,37 +214,85 @@ impl Parse for InternalUriParams { input.parse::()?; let uri_params = input.parse::()?; - Ok(InternalUriParams { route_uri, fn_args, uri_params }) + + // This span isn't right...we don't have the original span. + let span = route_uri_str.subspan(1..route_uri.path().len() + 1); + let mount_params = match uri_params.mount_point.as_ref() { + Some(mount) => Parameter::parse_many::(mount.path().as_str(), span) + .map(|p| p.expect("internal error: invalid path parameter")) + .collect::>(), + None => vec![] + }; + + let path_params = Parameter::parse_many::(route_uri.path().as_str(), span) + .map(|p| p.expect("internal error: invalid path parameter")) + .collect::>(); + + let query_params = match route_uri.query() { + Some(query) => { + let i = route_uri.path().len() + 2; + let span = route_uri_str.subspan(i..(i + query.len())); + Parameter::parse_many::(query.as_str(), span) + .map(|p| p.expect("internal error: invalid query parameter")) + .collect::>() + } + None => vec![] + }; + + Ok(InternalUriParams { route_uri, mount_params, path_params, query_params, fn_args, uri_params }) } } impl InternalUriParams { pub fn fn_args_str(&self) -> String { self.fn_args.iter() - .map(|FnArg { ident, ty }| format!("{}: {}", ident, quote!(#ty).to_string().trim())) + .map(|FnArg { ident, ty }| { + let ty = ty.with_stripped_lifetimes(); + let ty_str = quote!(#ty).to_string(); + let ty_str: String = ty_str.chars().filter(|c| !c.is_whitespace()).collect(); + format!("{}: {}", ident, ty_str) + }) .collect::>() .join(", ") } + pub fn dynamic_path_params(&self) -> impl Iterator + Clone { + self.path_params.iter() + .filter_map(|p| p.dynamic().or_else(|| p.ignored())) + } + + pub fn dynamic_query_params(&self) -> impl Iterator + Clone { + self.query_params.iter().filter_map(|p| p.dynamic()) + } + pub fn validate(&self) -> Validation<'_> { let args = &self.uri_params.arguments; + let all_params = self.dynamic_path_params().chain(self.dynamic_query_params()); match args { - Args::Unnamed(inner) => { - let (expected, actual) = (self.fn_args.len(), inner.len()); - if expected != actual { Validation::Unnamed(expected, actual) } - else { Validation::Ok(args.unnamed().unwrap().collect()) } + Args::Unnamed(args) => { + let (expected, actual) = (all_params.count(), args.len()); + let unnamed_args = args.iter().map(|arg| arg.unnamed()); + match expected == actual { + true => Validation::Ok(unnamed_args.collect()), + false => Validation::Unnamed(expected, actual) + } }, - Args::Named(_) => { - let mut params: IndexMap> = self.fn_args.iter() - .map(|FnArg { ident, .. }| (ident.clone().into(), None)) - .collect(); + Args::Named(args) => { + let ignored = all_params.clone().filter(|p| p.is_wild()); + if ignored.clone().count() > 0 { + return Validation::NamedIgnored(ignored.collect()); + } + + let mut params = all_params.map(|p| (&p.name, None)) + .collect::>>(); let (mut extra, mut dup) = (vec![], vec![]); - for (name, expr) in args.named().unwrap() { + let named_args = args.iter().map(|arg| arg.named()); + for (name, ident, expr) in named_args { match params.get_mut(name) { - Some(ref entry) if entry.is_some() => dup.push(name.ident()), + Some(ref entry) if entry.is_some() => dup.push(ident), Some(entry) => *entry = Some(expr), - None => extra.push(name.ident()), + None => extra.push(ident), } } @@ -280,9 +339,9 @@ impl Arg { } } - fn named(&self) -> (&NameSource, &ArgExpr) { + fn named(&self) -> (&Name, &Ident, &ArgExpr) { match self { - Arg::Named(name, _, expr) => (name, expr), + Arg::Named(name, ident, _, expr) => (name, ident, expr), _ => panic!("Called Arg::named() on an Arg::Unnamed!"), } } @@ -294,20 +353,6 @@ impl Args { Args::Named(inner) | Args::Unnamed(inner) => inner.len(), } } - - fn named(&self) -> Option> { - match self { - Args::Named(args) => Some(args.iter().map(|arg| arg.named())), - _ => None - } - } - - fn unnamed(&self) -> Option> { - match self { - Args::Unnamed(args) => Some(args.iter().map(|arg| arg.unnamed())), - _ => None - } - } } impl ArgExpr { @@ -339,9 +384,10 @@ impl ToTokens for Arg { fn to_tokens(&self, tokens: &mut TokenStream) { match self { Arg::Unnamed(e) => e.to_tokens(tokens), - Arg::Named(name, eq, expr) => { - let ident = name.ident(); - tokens.extend(quote!(#ident #eq #expr)) + Arg::Named(_, ident, eq, expr) => { + ident.to_tokens(tokens); + eq.to_tokens(tokens); + expr.to_tokens(tokens); } } } diff --git a/core/codegen/src/derive/form_field.rs b/core/codegen/src/derive/form_field.rs index 1919b471..d1698a63 100644 --- a/core/codegen/src/derive/form_field.rs +++ b/core/codegen/src/derive/form_field.rs @@ -2,11 +2,11 @@ use devise::{*, ext::{TypeExt, SpanDiagnosticExt}}; use crate::exports::*; use crate::proc_macro2::Span; -use crate::syn_ext::NameSource; +use crate::name::Name; pub struct FormField { pub span: Span, - pub name: NameSource, + pub name: Name, } #[derive(FromMeta)] @@ -41,8 +41,8 @@ impl FromMeta for FormField { s.chars().all(|c| c.is_ascii_graphic() && !CONTROL_CHARS.contains(&c)) } - let name = NameSource::from_meta(meta)?; - if !is_valid_field_name(name.name()) { + let name = Name::from_meta(meta)?; + if !is_valid_field_name(name.as_str()) { let chars = CONTROL_CHARS.iter() .map(|c| format!("{:?}", c)) .collect::>() @@ -69,7 +69,7 @@ impl FieldExt for Field<'_> { let name = fields.next() .map(|f| f.name) - .unwrap_or_else(|| NameSource::from(self.ident().clone())); + .unwrap_or_else(|| Name::from(self.ident().clone())); if let Some(field) = fields.next() { return Err(field.span diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index c11a5b6b..82283d4d 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -249,20 +249,5 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { }}) }) ) - // .inner_mapper(MapperBuild::new() - // .with_output(|_, output| quote! { - // fn default() -> #_Option { - // Some(Self { #output }) - // } - // }) - // .try_fields_map(|m, f| mapper::fields_null(m, f)) - // .field_map(|_, field| { - // let ident = field.ident.as_ref().expect("named"); - // let ty = field.ty.with_stripped_lifetimes(); - // quote_spanned!(ty.span() => - // #ident: <#ty as #_form::FromForm<'__f>>::default()?, - // ) - // }) - // ) .to_tokens() } diff --git a/core/codegen/src/derive/from_form_field.rs b/core/codegen/src/derive/from_form_field.rs index eb8a6e43..9c153ad4 100644 --- a/core/codegen/src/derive/from_form_field.rs +++ b/core/codegen/src/derive/from_form_field.rs @@ -2,11 +2,11 @@ use devise::{*, ext::SpanDiagnosticExt}; use crate::exports::*; use crate::proc_macro2::TokenStream; -use crate::syn_ext::NameSource; +use crate::name::Name; #[derive(FromMeta)] pub struct FieldAttr { - value: NameSource, + value: Name, } pub fn derive_from_form_field(input: proc_macro::TokenStream) -> TokenStream { @@ -36,10 +36,10 @@ pub fn derive_from_form_field(input: proc_macro::TokenStream) -> TokenStream { .map(|v| FieldAttr::one_from_attrs("field", &v.attrs).map(|o| { o.map(|f| f.value).unwrap_or_else(|| v.ident.clone().into()) })) - .collect::>>()?; + .collect::>>()?; let variant_name = variant_name_sources.iter() - .map(|n| n.name()) + .map(|n| n.as_str()) .collect::>(); let builder = data.variants() diff --git a/core/codegen/src/exports.rs b/core/codegen/src/exports.rs index aab38e36..05f08fb4 100644 --- a/core/codegen/src/exports.rs +++ b/core/codegen/src/exports.rs @@ -86,7 +86,10 @@ define_exported_paths! { BorrowMut => ::std::borrow::BorrowMut, Outcome => rocket::outcome::Outcome, FromForm => rocket::form::FromForm, + FromRequest => rocket::request::FromRequest, FromData => rocket::data::FromData, + FromSegments => rocket::request::FromSegments, + FromParam => rocket::request::FromParam, Request => rocket::request::Request, Response => rocket::response::Response, Data => rocket::data::Data, diff --git a/core/codegen/src/http_codegen.rs b/core/codegen/src/http_codegen.rs index 674651a4..675accd5 100644 --- a/core/codegen/src/http_codegen.rs +++ b/core/codegen/src/http_codegen.rs @@ -2,11 +2,7 @@ use quote::ToTokens; use devise::{FromMeta, MetaItem, Result, ext::{Split2, PathExt, SpanDiagnosticExt}}; use crate::proc_macro2::TokenStream; -use crate::http::{self, ext::IntoOwned}; -use crate::http::uri::{Path, Query}; -use crate::attribute::segments::{parse_segments, parse_data_segment, Segment, Kind}; - -use crate::proc_macro_ext::StringLit; +use crate::http; #[derive(Debug)] pub struct ContentType(pub http::ContentType); @@ -17,31 +13,12 @@ pub struct Status(pub http::Status); #[derive(Debug)] pub struct MediaType(pub http::MediaType); -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct Method(pub http::Method); -#[derive(Debug)] -pub struct Origin(pub http::uri::Origin<'static>); - -#[derive(Clone, Debug)] -pub struct DataSegment(pub Segment); - #[derive(Clone, Debug)] pub struct Optional(pub Option); -impl FromMeta for StringLit { - fn from_meta(meta: &MetaItem) -> Result { - Ok(StringLit::new(String::from_meta(meta)?, meta.value_span())) - } -} - -#[derive(Debug)] -pub struct RoutePath { - pub origin: Origin, - pub path: Vec, - pub query: Option>, -} - impl FromMeta for Status { fn from_meta(meta: &MetaItem) -> Result { let num = usize::from_meta(meta)?; @@ -155,69 +132,6 @@ impl ToTokens for Method { } } -impl FromMeta for Origin { - fn from_meta(meta: &MetaItem) -> Result { - let string = StringLit::from_meta(meta)?; - - let uri = http::uri::Origin::parse_route(&string) - .map_err(|e| { - let span = string.subspan(e.index() + 1..(e.index() + 2)); - span.error(format!("invalid route URI: {}", e)) - .help("expected path in origin form: \"/path/\"") - })?; - - if !uri.is_normalized() { - let normalized = uri.clone().into_normalized(); - return Err(string.span().error("paths cannot contain empty segments") - .note(format!("expected '{}', found '{}'", normalized, uri))); - } - - Ok(Origin(uri.into_owned())) - } -} - -impl FromMeta for DataSegment { - fn from_meta(meta: &MetaItem) -> Result { - let string = StringLit::from_meta(meta)?; - let span = string.subspan(1..(string.len() + 1)); - - let segment = parse_data_segment(&string, span)?; - if segment.kind != Kind::Single { - return Err(span.error("malformed parameter") - .help("parameter must be of the form ''")); - } - - Ok(DataSegment(segment)) - } -} - -impl FromMeta for RoutePath { - fn from_meta(meta: &MetaItem) -> Result { - let (origin, string) = (Origin::from_meta(meta)?, StringLit::from_meta(meta)?); - let path_span = string.subspan(1..origin.0.path().len() + 1); - let path = parse_segments::(origin.0.path(), path_span); - - let query = origin.0.query() - .map(|q| { - let len_to_q = 1 + origin.0.path().len() + 1; - let end_of_q = len_to_q + q.len(); - let query_span = string.subspan(len_to_q..end_of_q); - if q.starts_with('&') || q.contains("&&") || q.ends_with('&') { - // TODO: Show a help message with what's expected. - Err(query_span.error("query cannot contain empty segments").into()) - } else { - parse_segments::(q, query_span) - } - }).transpose(); - - match (path, query) { - (Ok(path), Ok(query)) => Ok(RoutePath { origin, path, query }), - (Err(diag), Ok(_)) | (Ok(_), Err(diag)) => Err(diag.emit_head()), - (Err(d1), Err(d2)) => Err(d1.join(d2).emit_head()) - } - } -} - impl ToTokens for Optional { fn to_tokens(&self, tokens: &mut TokenStream) { use crate::exports::{_Some, _None}; diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index e50d5537..dc7cdc3b 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -66,13 +66,14 @@ mod attribute; mod bang; mod http_codegen; mod syn_ext; +mod name; use crate::http::Method; use proc_macro::TokenStream; use devise::{proc_macro2, syn}; static URI_MACRO_PREFIX: &str = "rocket_uri_macro_"; -static ROCKET_PARAM_PREFIX: &str = "__rocket_param_"; +static ROCKET_IDENT_PREFIX: &str = "__rocket_"; macro_rules! emit { ($tokens:expr) => ({ @@ -118,13 +119,13 @@ macro_rules! route_attribute { /// * [`options`] - `OPTIONS` specific route /// * [`patch`] - `PATCH` specific route /// - /// Additionally, [`route`] allows the method and path to be explicitly + /// Additionally, [`route`] allows the method and uri to be explicitly /// specified: /// /// ```rust /// # #[macro_use] extern crate rocket; /// # - /// #[route(GET, path = "/")] + /// #[route(GET, uri = "/")] /// fn index() -> &'static str { /// "Hello, world!" /// } @@ -144,22 +145,22 @@ macro_rules! route_attribute { /// The grammar for all method-specific route attributes is defined as: /// /// ```text - /// route := '"' path ('?' query)? '"' (',' parameter)* + /// route := '"' uri ('?' query)? '"' (',' parameter)* /// - /// path := ('/' segment)* + /// uri := ('/' segment)* /// /// query := segment ('&' segment)* /// /// segment := URI_SEG /// | SINGLE_PARAM - /// | MULTI_PARAM + /// | TRAILING_PARAM /// /// parameter := 'rank' '=' INTEGER /// | 'format' '=' '"' MEDIA_TYPE '"' /// | 'data' '=' '"' SINGLE_PARAM '"' /// /// SINGLE_PARAM := '<' IDENT '>' - /// MULTI_PARAM := '<' IDENT '..>' + /// TRAILING_PARAM := '<' IDENT '..>' /// /// URI_SEG := valid, non-percent-encoded HTTP URI segment /// MEDIA_TYPE := valid HTTP media type or known shorthand @@ -171,13 +172,13 @@ macro_rules! route_attribute { /// The generic route attribute is defined as: /// /// ```text - /// generic-route := METHOD ',' 'path' '=' route + /// generic-route := METHOD ',' 'uri' '=' route /// ``` /// /// # Typing Requirements /// /// Every identifier that appears in a dynamic parameter (`SINGLE_PARAM` - /// or `MULTI_PARAM`) must appear as an argument to the function. For + /// or `TRAILING_PARAM`) must appear as an argument to the function. For /// example, the following route requires the decorated function to have /// the arguments `foo`, `baz`, `msg`, `rest`, and `form`: /// @@ -193,7 +194,7 @@ macro_rules! route_attribute { /// The type of each function argument corresponding to a dynamic /// parameter is required to implement one of Rocket's guard traits. The /// exact trait that is required to be implemented depends on the kind - /// of dynamic parameter (`SINGLE` or `MULTI`) and where in the route + /// of dynamic parameter (`SINGLE` or `TRAILING`) and where in the route /// attribute the parameter appears. The table below summarizes trait /// requirements: /// @@ -201,7 +202,7 @@ macro_rules! route_attribute { /// |----------|-------------|-------------------| /// | path | `` | [`FromParam`] | /// | path | `` | [`FromSegments`] | - /// | query | `` | [`FromFormField`] | + /// | query | `` | [`FromForm`] | /// | query | `` | [`FromFrom`] | /// | data | `` | [`FromData`] | /// @@ -238,13 +239,10 @@ macro_rules! route_attribute { /// `Failure`. See [`FromRequest` Outcomes] for further /// detail. /// - /// 2. Path and query parameters from left to right as declared - /// in the function argument list. + /// 2. Path and query guards in an unspecified order. If a path + /// or query guard fails, the request is forwarded. /// - /// If a path or query parameter guard fails, the request is - /// forwarded. - /// - /// 3. Data parameter, if any. + /// 3. Data guard, if any. /// /// If a data guard fails, the request is forwarded if the /// [`Outcome`] is `Forward` or failed if the [`Outcome`] is @@ -359,17 +357,17 @@ pub fn catch(args: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn async_test(args: TokenStream, input: TokenStream) -> TokenStream { - emit!(attribute::async_entry::async_test_attribute(args, input)) + emit!(attribute::entry::async_test_attribute(args, input)) } #[proc_macro_attribute] pub fn main(args: TokenStream, input: TokenStream) -> TokenStream { - emit!(attribute::async_entry::main_attribute(args, input)) + emit!(attribute::entry::main_attribute(args, input)) } #[proc_macro_attribute] pub fn launch(args: TokenStream, input: TokenStream) -> TokenStream { - emit!(attribute::async_entry::launch_attribute(args, input)) + emit!(attribute::entry::launch_attribute(args, input)) } /// Derive for the [`FromFormField`] trait. diff --git a/core/codegen/src/name.rs b/core/codegen/src/name.rs new file mode 100644 index 00000000..55ec0837 --- /dev/null +++ b/core/codegen/src/name.rs @@ -0,0 +1,101 @@ +use crate::syn::{self, Ident, ext::IdentExt}; +use crate::proc_macro2::Span; + +/// A "name" read by codegen, which may or may not be a valid identifier. A +/// `Name` is typically constructed indirectly via FromMeta, or From or +/// directly from a string via `Name::new()`. +/// +/// Some "names" in Rocket include: +/// * Dynamic parameter: `name` in `` +/// * Renamed fields: `foo` in #[field(name = "foo")]. +/// +/// `Name` implements Hash, PartialEq, and Eq, and additionally PartialEq for +/// all types `S: AsStr`. These implementations all compare the value of +/// `name()` only. +#[derive(Debug, Clone)] +pub struct Name { + value: String, + span: Span, +} + +impl Name { + /// Creates a new `Name` from the string `name` and span `span`. If + /// `name` is a valid ident, the ident is stored as well. + pub fn new>(name: S, span: Span) -> Self { + Name { value: name.into(), span } + } + + /// Returns the name as a string. Notably, if `self` was constructed from an + /// Ident this method returns a name *without* an `r#` prefix. + pub fn as_str(&self) -> &str { + &self.value + } + + pub fn span(&self) -> Span { + self.span + } +} + +impl devise::FromMeta for Name { + fn from_meta(meta: &devise::MetaItem) -> devise::Result { + use devise::ext::SpanDiagnosticExt; + + if let syn::Lit::Str(s) = meta.lit()? { + return Ok(Name::new(s.value(), s.span())); + } + + Err(meta.value_span().error("invalid value: expected string literal")) + } +} + +impl quote::ToTokens for Name { + fn to_tokens(&self, tokens: &mut devise::proc_macro2::TokenStream) { + self.as_str().to_tokens(tokens) + } +} + +impl From<&Ident> for Name { + fn from(ident: &Ident) -> Self { + Name::new(ident.unraw().to_string(), ident.span()) + } +} + +impl From for Name { + fn from(ident: Ident) -> Self { + Name::new(ident.unraw().to_string(), ident.span()) + } +} + +impl AsRef for Name { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl std::hash::Hash for Name { + fn hash(&self, hasher: &mut H) { + self.as_str().hash(hasher) + } +} + +impl std::ops::Deref for Name { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl Eq for Name { } + +impl + ?Sized> PartialEq for Name { + fn eq(&self, other: &S) -> bool { + self.as_str() == other.as_ref() + } +} + +impl std::fmt::Display for Name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_str().fmt(f) + } +} diff --git a/core/codegen/src/proc_macro_ext.rs b/core/codegen/src/proc_macro_ext.rs index c02b970b..26967256 100644 --- a/core/codegen/src/proc_macro_ext.rs +++ b/core/codegen/src/proc_macro_ext.rs @@ -4,10 +4,6 @@ use devise::Diagnostic; use crate::proc_macro2::{Span, Literal}; -pub type PResult = std::result::Result; - -pub type DResult = std::result::Result; - // An experiment. pub struct Diagnostics(Vec); @@ -20,11 +16,6 @@ impl Diagnostics { self.0.push(diag); } - pub fn join(mut self, mut diags: Diagnostics) -> Self { - self.0.append(&mut diags.0); - self - } - pub fn emit_head(self) -> Diagnostic { let mut iter = self.0.into_iter(); let mut last = iter.next().expect("Diagnostic::emit_head empty"); @@ -37,19 +28,12 @@ impl Diagnostics { last } - pub fn head_err_or(self, ok: T) -> PResult { + pub fn head_err_or(self, ok: T) -> devise::Result { match self.0.is_empty() { true => Ok(ok), false => Err(self.emit_head()) } } - - pub fn err_or(self, ok: T) -> DResult { - match self.0.is_empty() { - true => Ok(ok), - false => Err(self) - } - } } impl From for Diagnostics { @@ -64,18 +48,8 @@ impl From> for Diagnostics { } } -use std::ops::Deref; - pub struct StringLit(pub String, pub Literal); -impl Deref for StringLit { - type Target = str; - - fn deref(&self) -> &str { - &self.0 - } -} - impl StringLit { pub fn new>(string: S, span: Span) -> Self { let string = string.into(); @@ -95,3 +69,17 @@ impl StringLit { self.1.subspan(range).unwrap_or_else(|| self.span()) } } + +impl devise::FromMeta for StringLit { + fn from_meta(meta: &devise::MetaItem) -> devise::Result { + Ok(StringLit::new(String::from_meta(meta)?, meta.value_span())) + } +} + +impl std::ops::Deref for StringLit { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} diff --git a/core/codegen/src/syn_ext.rs b/core/codegen/src/syn_ext.rs index 6ebabef3..7528db3e 100644 --- a/core/codegen/src/syn_ext.rs +++ b/core/codegen/src/syn_ext.rs @@ -1,7 +1,5 @@ //! Extensions to `syn` types. -use devise::ext::SpanDiagnosticExt; - use crate::syn::{self, Ident, ext::IdentExt as _}; use crate::proc_macro2::Span; @@ -9,6 +7,20 @@ pub trait IdentExt { fn prepend(&self, string: &str) -> syn::Ident; fn append(&self, string: &str) -> syn::Ident; fn with_span(self, span: Span) -> syn::Ident; + fn rocketized(&self) -> syn::Ident; +} + +pub trait ReturnTypeExt { + fn ty(&self) -> Option<&syn::Type>; +} + +pub trait TokenStreamExt { + fn respanned(&self, span: crate::proc_macro2::Span) -> Self; +} + +pub trait FnArgExt { + fn typed(&self) -> Option<(&syn::Ident, &syn::Type)>; + fn wild(&self) -> Option<&syn::PatWild>; } impl IdentExt for syn::Ident { @@ -24,10 +36,10 @@ impl IdentExt for syn::Ident { self.set_span(span); self } -} -pub trait ReturnTypeExt { - fn ty(&self) -> Option<&syn::Type>; + fn rocketized(&self) -> syn::Ident { + self.prepend(crate::ROCKET_IDENT_PREFIX) + } } impl ReturnTypeExt for syn::ReturnType { @@ -39,10 +51,6 @@ impl ReturnTypeExt for syn::ReturnType { } } -pub trait TokenStreamExt { - fn respanned(&self, span: crate::proc_macro2::Span) -> Self; -} - impl TokenStreamExt for crate::proc_macro2::TokenStream { fn respanned(&self, span: crate::proc_macro2::Span) -> Self { self.clone().into_iter().map(|mut token| { @@ -52,82 +60,24 @@ impl TokenStreamExt for crate::proc_macro2::TokenStream { } } -/// Represents the source of a name read by codegen, which may or may not be a -/// valid identifier. A `NameSource` is typically constructed indirectly via -/// FromMeta, or From or directly from a string via `NameSource::new()`. -/// -/// NameSource implements Hash, PartialEq, and Eq, and additionally PartialEq -/// for all types `S: AsStr`. These implementations all compare the value -/// of `name()` only. -#[derive(Debug, Clone)] -pub struct NameSource { - name: String, - ident: Option, -} - -impl NameSource { - /// Creates a new `NameSource` from the string `name` and span `span`. If - /// `name` is a valid ident, the ident is stored as well. - pub fn new>(name: S, span: crate::proc_macro2::Span) -> Self { - let name = name.as_ref(); - syn::parse_str::(name) - .map(|mut ident| { ident.set_span(span); ident }) - .map(|ident| NameSource::from(ident)) - .unwrap_or_else(|_| NameSource { name: name.into(), ident: None }) - } - - /// Returns the name as a string. Notably, if `self` was constructed from an - /// Ident this method returns a name *without* an `r#` prefix. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the Ident corresponding to `self`, if any, otherwise panics. If - /// `self` was constructed from an `Ident`, this never panics. Otherwise, - /// panics if the string `self` was constructed from was not a valid ident. - pub fn ident(&self) -> &Ident { - self.ident.as_ref().expect("ident from namesource") - } -} - -impl devise::FromMeta for NameSource { - fn from_meta(meta: &devise::MetaItem) -> devise::Result { - if let syn::Lit::Str(s) = meta.lit()? { - return Ok(NameSource::new(s.value(), s.span())); +impl FnArgExt for syn::FnArg { + fn typed(&self) -> Option<(&Ident, &syn::Type)> { + match self { + syn::FnArg::Typed(arg) => match *arg.pat { + syn::Pat::Ident(ref pat) => Some((&pat.ident, &arg.ty)), + _ => None + } + _ => None, } + } - Err(meta.value_span().error("invalid value: expected string literal")) - } -} - -impl From for NameSource { - fn from(ident: Ident) -> Self { - Self { name: ident.unraw().to_string(), ident: Some(ident), } - } -} - -impl std::hash::Hash for NameSource { - fn hash(&self, hasher: &mut H) { - self.name().hash(hasher) - } -} - -impl AsRef for NameSource { - fn as_ref(&self) -> &str { - self.name() - } -} - -impl Eq for NameSource { } - -impl> PartialEq for NameSource { - fn eq(&self, other: &S) -> bool { - self.name == other.as_ref() - } -} - -impl std::fmt::Display for NameSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.name().fmt(f) + fn wild(&self) -> Option<&syn::PatWild> { + match self { + syn::FnArg::Typed(arg) => match *arg.pat { + syn::Pat::Wild(ref pat) => Some(pat), + _ => None + } + _ => None, + } } } diff --git a/core/codegen/tests/route.rs b/core/codegen/tests/route.rs index 1bf50bf4..9b8fb945 100644 --- a/core/codegen/tests/route.rs +++ b/core/codegen/tests/route.rs @@ -55,7 +55,7 @@ fn post1( #[route( POST, - path = "///name/?sky=blue&&", + uri = "///name/?sky=blue&&", format = "json", data = "", rank = 138 diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs index 584e6241..b510129a 100644 --- a/core/codegen/tests/typed-uris.rs +++ b/core/codegen/tests/typed-uris.rs @@ -8,6 +8,18 @@ use rocket::http::CookieJar; use rocket::http::uri::{FromUriParam, Query}; use rocket::form::{Form, error::{Errors, ErrorKind}}; +macro_rules! assert_uri_eq { + ($($uri:expr => $expected:expr,)+) => { + $( + let actual = $uri; + let expected = rocket::http::uri::Origin::parse($expected).expect("valid origin URI"); + if actual != expected { + panic!("URI mismatch: got {}, expected {}", actual, expected); + } + )+ + }; +} + #[derive(FromForm, UriDisplayQuery)] struct User<'a> { name: &'a str, @@ -62,13 +74,13 @@ fn guard_3(id: i32, name: String, cookies: &CookieJar<'_>) { } #[post("/", data = "

")] fn no_uri_display_okay(id: i32, form: Form) { } -#[post("/name/?&bar=10&&", data = "", rank = 2)] +#[post("/name/?&type=10&&", data = "", rank = 2)] fn complex<'r>( foo: usize, name: &str, query: User<'r>, user: Form>, - bar: &str, + r#type: &str, cookies: &CookieJar<'_> ) { } @@ -81,12 +93,6 @@ fn param_and_segments(path: PathBuf, id: usize) { } #[post("/a//then/")] fn guarded_segments(cookies: &CookieJar<'_>, path: PathBuf, id: usize) { } -macro_rules! assert_uri_eq { - ($($uri:expr => $expected:expr,)+) => { - $(assert_eq!($uri, rocket::http::uri::Origin::parse($expected).expect("valid origin URI"));)+ - }; -} - #[test] fn check_simple_unnamed() { assert_uri_eq! { @@ -236,42 +242,42 @@ fn check_with_segments() { fn check_complex() { assert_uri_eq! { uri!(complex: "no idea", 10, "high", ("A B C", "a c")) => - "/name/no%20idea?foo=10&bar=10&bar=high&name=A%20B%20C&nickname=a%20c", + "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", uri!(complex: "Bob", 248, "?", User { name: "Robert".into(), nickname: "Bob".into() }) => - "/name/Bob?foo=248&bar=10&bar=%3F&name=Robert&nickname=Bob", + "/name/Bob?foo=248&type=10&type=%3F&name=Robert&nickname=Bob", uri!(complex: "Bob", 248, "a a", &User { name: "Robert".into(), nickname: "B".into() }) => - "/name/Bob?foo=248&bar=10&bar=a%20a&name=Robert&nickname=B", + "/name/Bob?foo=248&type=10&type=a%20a&name=Robert&nickname=B", uri!(complex: "no idea", 248, "", &User { name: "A B".into(), nickname: "A".into() }) => - "/name/no%20idea?foo=248&bar=10&bar=&name=A%20B&nickname=A", + "/name/no%20idea?foo=248&type=10&type=&name=A%20B&nickname=A", uri!(complex: "hi", 3, "b", &User { name: "A B C".into(), nickname: "a b".into() }) => - "/name/hi?foo=3&bar=10&bar=b&name=A%20B%20C&nickname=a%20b", - uri!(complex: name = "no idea", foo = 10, bar = "high", query = ("A B C", "a c")) => - "/name/no%20idea?foo=10&bar=10&bar=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: foo = 10, name = "no idea", bar = "high", query = ("A B C", "a c")) => - "/name/no%20idea?foo=10&bar=10&bar=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: query = ("A B C", "a c"), foo = 10, name = "no idea", bar = "high", ) => - "/name/no%20idea?foo=10&bar=10&bar=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: query = ("A B C", "a c"), foo = 10, name = "no idea", bar = "high") => - "/name/no%20idea?foo=10&bar=10&bar=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: query = *&("A B C", "a c"), foo = 10, name = "no idea", bar = "high") => - "/name/no%20idea?foo=10&bar=10&bar=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: foo = 3, name = "hi", bar = "b", + "/name/hi?foo=3&type=10&type=b&name=A%20B%20C&nickname=a%20b", + uri!(complex: name = "no idea", foo = 10, r#type = "high", query = ("A B C", "a c")) => + "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", + uri!(complex: foo = 10, name = "no idea", r#type = "high", query = ("A B C", "a c")) => + "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", + uri!(complex: query = ("A B C", "a c"), foo = 10, name = "no idea", r#type = "high", ) => + "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", + uri!(complex: query = ("A B C", "a c"), foo = 10, name = "no idea", r#type = "high") => + "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", + uri!(complex: query = *&("A B C", "a c"), foo = 10, name = "no idea", r#type = "high") => + "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", + uri!(complex: foo = 3, name = "hi", r#type = "b", query = &User { name: "A B C".into(), nickname: "a b".into() }) => - "/name/hi?foo=3&bar=10&bar=b&name=A%20B%20C&nickname=a%20b", + "/name/hi?foo=3&type=10&type=b&name=A%20B%20C&nickname=a%20b", uri!(complex: query = &User { name: "A B C".into(), nickname: "a b".into() }, - foo = 3, name = "hi", bar = "b") => - "/name/hi?foo=3&bar=10&bar=b&name=A%20B%20C&nickname=a%20b", + foo = 3, name = "hi", r#type = "b") => + "/name/hi?foo=3&type=10&type=b&name=A%20B%20C&nickname=a%20b", } // Ensure variables are correctly processed. let user = User { name: "Robert".into(), nickname: "Bob".into() }; assert_uri_eq! { uri!(complex: "complex", 0, "high", &user) => - "/name/complex?foo=0&bar=10&bar=high&name=Robert&nickname=Bob", + "/name/complex?foo=0&type=10&type=high&name=Robert&nickname=Bob", uri!(complex: "complex", 0, "high", &user) => - "/name/complex?foo=0&bar=10&bar=high&name=Robert&nickname=Bob", + "/name/complex?foo=0&type=10&type=high&name=Robert&nickname=Bob", uri!(complex: "complex", 0, "high", user) => - "/name/complex?foo=0&bar=10&bar=high&name=Robert&nickname=Bob", + "/name/complex?foo=0&type=10&type=high&name=Robert&nickname=Bob", } } @@ -434,3 +440,39 @@ fn test_optional_uri_parameters() { ) => "/10/hi%20there", } } + +#[test] +fn test_simple_ignored() { + #[get("/<_>")] fn ignore_one() { } + assert_uri_eq! { + uri!(ignore_one: 100) => "/100", + uri!(ignore_one: "hello") => "/hello", + uri!(ignore_one: "cats r us") => "/cats%20r%20us", + } + + #[get("/<_>/<_>")] fn ignore_two() { } + assert_uri_eq! { + uri!(ignore_two: 100, "boop") => "/100/boop", + uri!(ignore_two: &"hi", "bop") => "/hi/bop", + } + + #[get("/<_>/foo/<_>")] fn ignore_inner_two() { } + #[get("/hi/<_>/foo")] fn ignore_inner_one_a() { } + #[get("/hey/hi/<_>/foo/<_>")] fn ignore_inner_two_b() { } + + assert_uri_eq! { + uri!(ignore_inner_two: 100, "boop") => "/100/foo/boop", + uri!(ignore_inner_one_a: "!?") => "/hi/!%3F/foo", + uri!(ignore_inner_two_b: &mut 5, "boo") => "/hey/hi/5/foo/boo", + } + + #[get("/<_>/foo/<_>?hi")] fn ignore_with_q() { } + #[get("/hi/<_>/foo/<_>?hi&")] fn ignore_with_q2(hey: Option) { } + #[get("/hi/<_>/foo/<_>?&")] fn ignore_with_q3(hi: &str, hey: &str) { } + + assert_uri_eq! { + uri!(ignore_with_q: 100, "boop") => "/100/foo/boop?hi", + uri!(ignore_with_q2: "!?", "bop", Some(3usize)) => "/hi/!%3F/foo/bop?hi&hey=3", + uri!(ignore_with_q3: &mut 5, "boo", "hi b", "ho") => "/hi/5/foo/boo?hi=hi%20b&hey=ho", + } +} diff --git a/core/codegen/tests/ui-fail-nightly/bad-ignored-segments.stderr b/core/codegen/tests/ui-fail-nightly/bad-ignored-segments.stderr index 29929953..ce8b42f6 100644 --- a/core/codegen/tests/ui-fail-nightly/bad-ignored-segments.stderr +++ b/core/codegen/tests/ui-fail-nightly/bad-ignored-segments.stderr @@ -1,8 +1,8 @@ error: parameter must be named - --> $DIR/bad-ignored-segments.rs:6:11 + --> $DIR/bad-ignored-segments.rs:6:12 | 6 | #[get("/c?<_>")] - | ^^^ + | ^ | = help: use a name such as `_guard` or `_param` diff --git a/core/codegen/tests/ui-fail-nightly/catch.stderr b/core/codegen/tests/ui-fail-nightly/catch.stderr index 90eb691c..14a830f5 100644 --- a/core/codegen/tests/ui-fail-nightly/catch.stderr +++ b/core/codegen/tests/ui-fail-nightly/catch.stderr @@ -14,7 +14,7 @@ error: expected `fn` | = help: `#[catch]` can only be used on functions -error: expected integer or identifier, found string literal +error: expected integer or `default`, found string literal --> $DIR/catch.rs:11:9 | 11 | #[catch("404")] diff --git a/core/codegen/tests/ui-fail-nightly/route-attribute-general-syntax.stderr b/core/codegen/tests/ui-fail-nightly/route-attribute-general-syntax.stderr index 0eb4ae5a..5604af79 100644 --- a/core/codegen/tests/ui-fail-nightly/route-attribute-general-syntax.stderr +++ b/core/codegen/tests/ui-fail-nightly/route-attribute-general-syntax.stderr @@ -1,4 +1,4 @@ -error: missing expected parameter: `path` +error: missing expected parameter: `uri` --> $DIR/route-attribute-general-syntax.rs:4:1 | 4 | #[get()] @@ -74,7 +74,7 @@ error: handler arguments cannot be ignored 39 | fn c1(_: usize) {} | ^^^^^^^^ | - = help: all handler arguments must be of the form: `ident: Type` + = note: handler arguments must be of the form: `ident: Type` error: invalid value: expected string literal --> $DIR/route-attribute-general-syntax.rs:43:7 diff --git a/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr b/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr index 29b9abe3..d1db6a6e 100644 --- a/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr +++ b/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr @@ -4,7 +4,7 @@ error: invalid route URI: expected token '/' but found 'a' at index 0 5 | #[get("a")] | ^ | - = help: expected path in origin form: "/path/" + = help: expected URI in origin form: "/path/" error: invalid route URI: unexpected EOF: expected token '/' at index 0 --> $DIR/route-path-bad-syntax.rs:8:8 @@ -12,7 +12,7 @@ error: invalid route URI: unexpected EOF: expected token '/' at index 0 8 | #[get("")] | ^ | - = help: expected path in origin form: "/path/" + = help: expected URI in origin form: "/path/" error: invalid route URI: expected token '/' but found 'a' at index 0 --> $DIR/route-path-bad-syntax.rs:11:8 @@ -20,41 +20,45 @@ error: invalid route URI: expected token '/' but found 'a' at index 0 11 | #[get("a/b/c")] | ^ | - = help: expected path in origin form: "/path/" + = help: expected URI in origin form: "/path/" -error: paths cannot contain empty segments - --> $DIR/route-path-bad-syntax.rs:14:7 +error: route URIs cannot contain empty segments + --> $DIR/route-path-bad-syntax.rs:14:10 | 14 | #[get("/a///b")] - | ^^^^^^^^ + | ^^ | - = note: expected '/a/b', found '/a///b' + = note: expected "/a/b", found "/a///b" -error: query cannot contain empty segments - --> $DIR/route-path-bad-syntax.rs:17:10 +error: route URIs cannot contain empty segments + --> $DIR/route-path-bad-syntax.rs:17:13 | 17 | #[get("/?bat&&")] - | ^^^^^ + | ^^ + | + = note: expected "/?bat", found "/?bat&&" -error: query cannot contain empty segments - --> $DIR/route-path-bad-syntax.rs:20:10 +error: route URIs cannot contain empty segments + --> $DIR/route-path-bad-syntax.rs:20:13 | 20 | #[get("/?bat&&")] - | ^^^^^ + | ^^ + | + = note: expected "/?bat", found "/?bat&&" -error: paths cannot contain empty segments - --> $DIR/route-path-bad-syntax.rs:23:7 +error: route URIs cannot contain empty segments + --> $DIR/route-path-bad-syntax.rs:23:12 | 23 | #[get("/a/b//")] - | ^^^^^^^^ + | ^^ | - = note: expected '/a/b', found '/a/b//' + = note: expected "/a/b", found "/a/b//" -error: unused dynamic parameter - --> $DIR/route-path-bad-syntax.rs:42:9 +error: unused parameter + --> $DIR/route-path-bad-syntax.rs:42:10 | 42 | #[get("/")] - | ^^^^^^ + | ^^^^ | note: expected argument named `name` here --> $DIR/route-path-bad-syntax.rs:43:6 @@ -62,11 +66,11 @@ note: expected argument named `name` here 43 | fn h0(_name: usize) {} | ^^^^^^^^^^^^^^ -error: unused dynamic parameter - --> $DIR/route-path-bad-syntax.rs:45:11 +error: unused parameter + --> $DIR/route-path-bad-syntax.rs:45:12 | 45 | #[get("/a?")] - | ^^^ + | ^ | note: expected argument named `r` here --> $DIR/route-path-bad-syntax.rs:46:6 @@ -74,11 +78,11 @@ note: expected argument named `r` here 46 | fn h1() {} | ^^ -error: unused dynamic parameter - --> $DIR/route-path-bad-syntax.rs:48:22 +error: unused parameter + --> $DIR/route-path-bad-syntax.rs:48:23 | 48 | #[post("/a", data = "")] - | ^^^^^^ + | ^^^^ | note: expected argument named `test` here --> $DIR/route-path-bad-syntax.rs:49:6 @@ -86,11 +90,11 @@ note: expected argument named `test` here 49 | fn h2() {} | ^^ -error: unused dynamic parameter - --> $DIR/route-path-bad-syntax.rs:51:9 +error: unused parameter + --> $DIR/route-path-bad-syntax.rs:51:10 | 51 | #[get("/<_r>")] - | ^^^^ + | ^^ | note: expected argument named `_r` here --> $DIR/route-path-bad-syntax.rs:52:6 @@ -98,11 +102,11 @@ note: expected argument named `_r` here 52 | fn h3() {} | ^^ -error: unused dynamic parameter - --> $DIR/route-path-bad-syntax.rs:54:9 +error: unused parameter + --> $DIR/route-path-bad-syntax.rs:54:10 | 54 | #[get("/<_r>/")] - | ^^^^ + | ^^ | note: expected argument named `_r` here --> $DIR/route-path-bad-syntax.rs:55:6 @@ -110,11 +114,11 @@ note: expected argument named `_r` here 55 | fn h4() {} | ^^ -error: unused dynamic parameter - --> $DIR/route-path-bad-syntax.rs:54:14 +error: unused parameter + --> $DIR/route-path-bad-syntax.rs:54:15 | 54 | #[get("/<_r>/")] - | ^^^ + | ^ | note: expected argument named `b` here --> $DIR/route-path-bad-syntax.rs:55:6 @@ -122,69 +126,82 @@ note: expected argument named `b` here 55 | fn h4() {} | ^^ -error: `foo_.` is not a valid identifier - --> $DIR/route-path-bad-syntax.rs:60:9 +error: invalid identifier: `foo_.` + --> $DIR/route-path-bad-syntax.rs:60:10 | 60 | #[get("/")] - | ^^^^^^^ + | ^^^^^ | - = help: parameter names must be valid identifiers + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? -error: `foo*` is not a valid identifier - --> $DIR/route-path-bad-syntax.rs:63:9 +error: invalid identifier: `foo*` + --> $DIR/route-path-bad-syntax.rs:63:10 | 63 | #[get("/")] - | ^^^^^^ + | ^^^^ | - = help: parameter names must be valid identifiers + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? -error: `!` is not a valid identifier - --> $DIR/route-path-bad-syntax.rs:66:9 +error: invalid identifier: `!` + --> $DIR/route-path-bad-syntax.rs:66:10 | 66 | #[get("/")] - | ^^^ + | ^ | - = help: parameter names must be valid identifiers + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? -error: `name>: $DIR/route-path-bad-syntax.rs:69:9 +error: invalid identifier: `name>: $DIR/route-path-bad-syntax.rs:69:10 | 69 | #[get("/:")] - | ^^^^^^^^^^^ + | ^^^^^^^^^ | - = help: parameter names must be valid identifiers + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? -error: malformed parameter +error: unexpected static parameter --> $DIR/route-path-bad-syntax.rs:74:20 | 74 | #[get("/", data = "foo")] | ^^^ | - = help: parameter must be of the form '' + = help: parameter must be dynamic: `` -error: malformed parameter +error: parameter cannot be trailing --> $DIR/route-path-bad-syntax.rs:77:20 | 77 | #[get("/", data = "")] | ^^^^^^^ | - = help: parameter must be of the form '' + = help: did you mean ``? -error: parameter is missing a closing bracket +warning: `segment` starts with `<` but does not end with `>` --> $DIR/route-path-bad-syntax.rs:80:20 | 80 | #[get("/", data = "'? + = help: perhaps you meant the dynamic parameter ``? -error: `test ` is not a valid identifier - --> $DIR/route-path-bad-syntax.rs:83:20 +error: unexpected static parameter + --> $DIR/route-path-bad-syntax.rs:80:20 + | +80 | #[get("/", data = "` + +error: invalid identifier: `test ` + --> $DIR/route-path-bad-syntax.rs:83:21 | 83 | #[get("/", data = "")] - | ^^^^^^^ + | ^^^^^ | - = help: parameter names must be valid identifiers + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? error: handler arguments cannot be ignored --> $DIR/route-path-bad-syntax.rs:89:7 @@ -192,37 +209,34 @@ error: handler arguments cannot be ignored 89 | fn k0(_: usize) {} | ^^^^^^^^ | - = help: all handler arguments must be of the form: `ident: Type` + = note: handler arguments must be of the form: `ident: Type` -error: parameter names cannot be empty +error: parameters cannot be empty --> $DIR/route-path-bad-syntax.rs:93:9 | 93 | #[get("/<>")] | ^^ -error: malformed parameter or identifier +warning: `segment` starts with `<` but does not end with `>` --> $DIR/route-path-bad-syntax.rs:96:9 | 96 | #[get("/<")] | ^^^^^ | - = help: parameters must be of the form '' - = help: identifiers cannot contain '<' or '>' + = help: perhaps you meant the dynamic parameter ``? -error: malformed parameter or identifier +warning: `segment` starts with `<` but does not end with `>` --> $DIR/route-path-bad-syntax.rs:99:9 | 99 | #[get("/<<<<")] | ^^^^^^^^ | - = help: parameters must be of the form '' - = help: identifiers cannot contain '<' or '>' + = help: perhaps you meant the dynamic parameter ``? -error: malformed parameter or identifier +warning: `segment` starts with `<` but does not end with `>` --> $DIR/route-path-bad-syntax.rs:102:9 | 102 | #[get("/<>name><")] | ^^^^^^^^ | - = help: parameters must be of the form '' - = help: identifiers cannot contain '<' or '>' + = help: perhaps you meant the dynamic parameter ``? diff --git a/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr b/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr index 6694a1bc..9cfd1fe7 100644 --- a/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr +++ b/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr @@ -1,16 +1,16 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied - --> $DIR/route-type-errors.rs:6:7 + --> $DIR/route-type-errors.rs:6:12 | 6 | fn f0(foo: Q) {} - | ^^^^^^ the trait `FromParam<'_>` is not implemented for `Q` + | ^ the trait `FromParam<'_>` is not implemented for `Q` | = note: required by `from_param` error[E0277]: the trait bound `Q: FromSegments<'_>` is not satisfied - --> $DIR/route-type-errors.rs:9:7 + --> $DIR/route-type-errors.rs:9:12 | 9 | fn f1(foo: Q) {} - | ^^^^^^ the trait `FromSegments<'_>` is not implemented for `Q` + | ^ the trait `FromSegments<'_>` is not implemented for `Q` | = note: required by `from_segments` @@ -31,10 +31,10 @@ error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied = note: required because of the requirements on the impl of `FromForm<'_>` for `Q` error[E0277]: the trait bound `Q: FromData<'_>` is not satisfied - --> $DIR/route-type-errors.rs:18:7 + --> $DIR/route-type-errors.rs:18:12 | 18 | fn f4(foo: Q) {} - | ^^^^^^ the trait `FromData<'_>` is not implemented for `Q` + | ^ the trait `FromData<'_>` is not implemented for `Q` | ::: $WORKSPACE/core/lib/src/data/from_data.rs | @@ -42,10 +42,10 @@ error[E0277]: the trait bound `Q: FromData<'_>` is not satisfied | -- required by this bound in `rocket::data::FromData::from_data` error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied - --> $DIR/route-type-errors.rs:21:7 + --> $DIR/route-type-errors.rs:21:10 | 21 | fn f5(a: Q, foo: Q) {} - | ^^^^ the trait `FromRequest<'_, '_>` is not implemented for `Q` + | ^ the trait `FromRequest<'_, '_>` is not implemented for `Q` | ::: $WORKSPACE/core/lib/src/request/from_request.rs | @@ -53,18 +53,18 @@ error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied | -- required by this bound in `from_request` error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied - --> $DIR/route-type-errors.rs:21:13 + --> $DIR/route-type-errors.rs:21:18 | 21 | fn f5(a: Q, foo: Q) {} - | ^^^^^^ the trait `FromParam<'_>` is not implemented for `Q` + | ^ the trait `FromParam<'_>` is not implemented for `Q` | = note: required by `from_param` error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied - --> $DIR/route-type-errors.rs:24:7 + --> $DIR/route-type-errors.rs:24:10 | 24 | fn f6(a: Q, foo: Q, good: usize, bar: Q) {} - | ^^^^ the trait `FromRequest<'_, '_>` is not implemented for `Q` + | ^ the trait `FromRequest<'_, '_>` is not implemented for `Q` | ::: $WORKSPACE/core/lib/src/request/from_request.rs | @@ -72,17 +72,17 @@ error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied | -- required by this bound in `from_request` error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied - --> $DIR/route-type-errors.rs:24:13 + --> $DIR/route-type-errors.rs:24:18 | 24 | fn f6(a: Q, foo: Q, good: usize, bar: Q) {} - | ^^^^^^ the trait `FromParam<'_>` is not implemented for `Q` + | ^ the trait `FromParam<'_>` is not implemented for `Q` | = note: required by `from_param` error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied - --> $DIR/route-type-errors.rs:24:34 + --> $DIR/route-type-errors.rs:24:39 | 24 | fn f6(a: Q, foo: Q, good: usize, bar: Q) {} - | ^^^^^^ the trait `FromParam<'_>` is not implemented for `Q` + | ^ the trait `FromParam<'_>` is not implemented for `Q` | = note: required by `from_param` diff --git a/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr b/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr index c4bd50fc..19ebe0e8 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr @@ -70,11 +70,11 @@ error[E0277]: the trait bound `std::string::String: FromUriParam>` for `Result` = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:58:20 | 58 | uri!(simple_q: "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` + | ^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following implementations were found: > @@ -82,11 +82,11 @@ error[E0277]: the trait bound `isize: FromUriParam` is not satisfie > = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:60:25 | 60 | uri!(simple_q: id = "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` + | ^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following implementations were found: > @@ -94,48 +94,48 @@ error[E0277]: the trait bound `isize: FromUriParam` is not satisfie > = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied +error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:62:24 | 62 | uri!(other_q: 100, S); - | ^ the trait `FromUriParam` is not implemented for `S` + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied +error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:64:26 | 64 | uri!(other_q: rest = S, id = 100); - | ^ the trait `FromUriParam` is not implemented for `S` + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `S: Ignorable` is not satisfied +error[E0277]: the trait bound `S: Ignorable` is not satisfied --> $DIR/typed-uri-bad-type.rs:66:26 | 66 | uri!(other_q: rest = _, id = 100); - | ^ the trait `Ignorable` is not implemented for `S` + | ^ the trait `Ignorable` is not implemented for `S` | ::: $WORKSPACE/core/http/src/uri/uri_display.rs | | pub fn assert_ignorable>() { } | ------------ required by this bound in `assert_ignorable` -error[E0277]: the trait bound `usize: Ignorable` is not satisfied +error[E0277]: the trait bound `usize: Ignorable` is not satisfied --> $DIR/typed-uri-bad-type.rs:68:34 | 68 | uri!(other_q: rest = S, id = _); - | ^ the trait `Ignorable` is not implemented for `usize` + | ^ the trait `Ignorable` is not implemented for `usize` | ::: $WORKSPACE/core/http/src/uri/uri_display.rs | | pub fn assert_ignorable>() { } | ------------ required by this bound in `assert_ignorable` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied +error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:68:26 | 68 | uri!(other_q: rest = S, id = _); - | ^ the trait `FromUriParam` is not implemented for `S` + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` diff --git a/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr index 24819efe..238e58ee 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr @@ -1,238 +1,274 @@ -error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:55:37 +error: expected identifier + --> $DIR/typed-uris-bad-params.rs:62:19 | -55 | uri!(optionals: id = 10, name = _); +62 | uri!(ignored: _ = 10); + | ^ + +error: expected 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:68:19 + | +68 | uri!(ignored: 10, "10"); + | ^^^^^^^^ + | + = note: route `ignored` has uri "/<_>" + +error: expected unnamed arguments due to ignored parameters + --> $DIR/typed-uris-bad-params.rs:66:19 + | +66 | uri!(ignored: num = 10); + | ^^^^^^^^ + | + = note: uri for route `ignored` ignores path parameters: "/<_>" + +error: expected 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:64:19 + | +64 | uri!(ignored: 10, 20); + | ^^^^^^ + | + = note: route `ignored` has uri "/<_>" + +error: path parameters cannot be ignored + --> $DIR/typed-uris-bad-params.rs:60:19 + | +60 | uri!(ignored: _); + | ^ + +error: path parameters cannot be ignored + --> $DIR/typed-uris-bad-params.rs:58:37 + | +58 | uri!(optionals: id = 10, name = _); | ^ error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:53:26 + --> $DIR/typed-uris-bad-params.rs:56:26 | -53 | uri!(optionals: id = _, name = "bob".into()); +56 | uri!(optionals: id = _, name = "bob".into()); | ^ error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:51:19 + --> $DIR/typed-uris-bad-params.rs:54:19 | -51 | uri!(has_two: id = 100, cookies = "hi"); +54 | uri!(has_two: id = 100, cookies = "hi"); | ^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32, name: String = help: missing parameter: `name` help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:51:29 + --> $DIR/typed-uris-bad-params.rs:54:29 | -51 | uri!(has_two: id = 100, cookies = "hi"); +54 | uri!(has_two: id = 100, cookies = "hi"); | ^^^^^^^ error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:49:19 + --> $DIR/typed-uris-bad-params.rs:52:19 | -49 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); +52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32, name: String = help: missing parameter: `name` help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:49:19 + --> $DIR/typed-uris-bad-params.rs:52:19 | -49 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); +52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); | ^^^^^^^ help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:49:45 + --> $DIR/typed-uris-bad-params.rs:52:45 | -49 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); +52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); | ^^ ^^ error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:47:19 + --> $DIR/typed-uris-bad-params.rs:50:19 | -47 | uri!(has_two: name = "hi"); +50 | uri!(has_two: name = "hi"); | ^^^^^^^^^^^ | = note: uri parameters are: id: i32, name: String = help: missing parameter: `id` error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:45:19 + --> $DIR/typed-uris-bad-params.rs:48:19 | -45 | uri!(has_two: id = 100, id = 100, ); +48 | uri!(has_two: id = 100, id = 100, ); | ^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32, name: String = help: missing parameter: `name` help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:45:29 + --> $DIR/typed-uris-bad-params.rs:48:29 | -45 | uri!(has_two: id = 100, id = 100, ); +48 | uri!(has_two: id = 100, id = 100, ); | ^^ error: invalid parameters for `has_one_guarded` route uri - --> $DIR/typed-uris-bad-params.rs:43:27 + --> $DIR/typed-uris-bad-params.rs:46:27 | -43 | uri!(has_one_guarded: id = 100, cookies = "hi"); +46 | uri!(has_one_guarded: id = 100, cookies = "hi"); | ^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:43:37 + --> $DIR/typed-uris-bad-params.rs:46:37 | -43 | uri!(has_one_guarded: id = 100, cookies = "hi"); +46 | uri!(has_one_guarded: id = 100, cookies = "hi"); | ^^^^^^^ error: invalid parameters for `has_one_guarded` route uri - --> $DIR/typed-uris-bad-params.rs:41:27 + --> $DIR/typed-uris-bad-params.rs:44:27 | -41 | uri!(has_one_guarded: cookies = "hi", id = 100); +44 | uri!(has_one_guarded: cookies = "hi", id = 100); | ^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:41:27 + --> $DIR/typed-uris-bad-params.rs:44:27 | -41 | uri!(has_one_guarded: cookies = "hi", id = 100); +44 | uri!(has_one_guarded: cookies = "hi", id = 100); | ^^^^^^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:39:19 + --> $DIR/typed-uris-bad-params.rs:42:19 | -39 | uri!(has_one: name = "hi"); +42 | uri!(has_one: name = "hi"); | ^^^^^^^^^^^ | = note: uri parameters are: id: i32 = help: missing parameter: `id` help: unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:39:19 + --> $DIR/typed-uris-bad-params.rs:42:19 | -39 | uri!(has_one: name = "hi"); +42 | uri!(has_one: name = "hi"); | ^^^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:37:19 + --> $DIR/typed-uris-bad-params.rs:40:19 | -37 | uri!(has_one: id = 100, id = 100, ); +40 | uri!(has_one: id = 100, id = 100, ); | ^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:37:29 + --> $DIR/typed-uris-bad-params.rs:40:29 | -37 | uri!(has_one: id = 100, id = 100, ); +40 | uri!(has_one: id = 100, id = 100, ); | ^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:35:19 + --> $DIR/typed-uris-bad-params.rs:38:19 | -35 | uri!(has_one: id = 100, id = 100); +38 | uri!(has_one: id = 100, id = 100); | ^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:35:29 + --> $DIR/typed-uris-bad-params.rs:38:29 | -35 | uri!(has_one: id = 100, id = 100); +38 | uri!(has_one: id = 100, id = 100); | ^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:33:19 + --> $DIR/typed-uris-bad-params.rs:36:19 | -33 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); +36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameters: `name`, `age` - --> $DIR/typed-uris-bad-params.rs:33:19 + --> $DIR/typed-uris-bad-params.rs:36:19 | -33 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); +36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); | ^^^^ ^^^ help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:33:51 + --> $DIR/typed-uris-bad-params.rs:36:51 | -33 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); +36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); | ^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:31:19 + --> $DIR/typed-uris-bad-params.rs:34:19 | -31 | uri!(has_one: name = 100, age = 50, id = 100); +34 | uri!(has_one: name = 100, age = 50, id = 100); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameters: `name`, `age` - --> $DIR/typed-uris-bad-params.rs:31:19 + --> $DIR/typed-uris-bad-params.rs:34:19 | -31 | uri!(has_one: name = 100, age = 50, id = 100); +34 | uri!(has_one: name = 100, age = 50, id = 100); | ^^^^ ^^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:29:19 + --> $DIR/typed-uris-bad-params.rs:32:19 | -29 | uri!(has_one: name = 100, id = 100); +32 | uri!(has_one: name = 100, id = 100); | ^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:29:19 + --> $DIR/typed-uris-bad-params.rs:32:19 | -29 | uri!(has_one: name = 100, id = 100); +32 | uri!(has_one: name = 100, id = 100); | ^^^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:27:19 + --> $DIR/typed-uris-bad-params.rs:30:19 | -27 | uri!(has_one: id = 100, name = "hi"); +30 | uri!(has_one: id = 100, name = "hi"); | ^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:27:29 + --> $DIR/typed-uris-bad-params.rs:30:29 | -27 | uri!(has_one: id = 100, name = "hi"); +30 | uri!(has_one: id = 100, name = "hi"); | ^^^^ -error: `has_two` route uri expects 2 parameters but 1 was supplied - --> $DIR/typed-uris-bad-params.rs:25:19 +error: expected 2 parameters but 1 was supplied + --> $DIR/typed-uris-bad-params.rs:28:19 | -25 | uri!(has_two: 10); +28 | uri!(has_two: 10); | ^^ | - = note: expected parameters: id: i32, name: String + = note: route `has_two` has uri "/?" -error: `has_two` route uri expects 2 parameters but 3 were supplied - --> $DIR/typed-uris-bad-params.rs:24:19 +error: expected 2 parameters but 3 were supplied + --> $DIR/typed-uris-bad-params.rs:27:19 | -24 | uri!(has_two: 10, "hi", "there"); +27 | uri!(has_two: 10, "hi", "there"); | ^^^^^^^^^^^^^^^^^ | - = note: expected parameters: id: i32, name: String + = note: route `has_two` has uri "/?" -error: `has_one_guarded` route uri expects 1 parameter but 2 were supplied - --> $DIR/typed-uris-bad-params.rs:22:27 +error: expected 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:25:27 | -22 | uri!(has_one_guarded: "hi", 100); +25 | uri!(has_one_guarded: "hi", 100); | ^^^^^^^^^ | - = note: expected parameter: id: i32 + = note: route `has_one_guarded` has uri "/" -error: `has_one` route uri expects 1 parameter but 2 were supplied - --> $DIR/typed-uris-bad-params.rs:21:19 +error: expected 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:24:19 | -21 | uri!(has_one: "Hello", 23, ); +24 | uri!(has_one: "Hello", 23, ); | ^^^^^^^^^^^^ | - = note: expected parameter: id: i32 + = note: route `has_one` has uri "/" -error: `has_one` route uri expects 1 parameter but 2 were supplied - --> $DIR/typed-uris-bad-params.rs:20:19 +error: expected 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:23:19 | -20 | uri!(has_one: 1, 23); +23 | uri!(has_one: 1, 23); | ^^^^^ | - = note: expected parameter: id: i32 + = note: route `has_one` has uri "/" -error: `has_one` route uri expects 1 parameter but 0 were supplied - --> $DIR/typed-uris-bad-params.rs:18:10 +error: expected 1 parameter but 0 were supplied + --> $DIR/typed-uris-bad-params.rs:21:10 | -18 | uri!(has_one); +21 | uri!(has_one); | ^^^^^^^ | - = note: expected parameter: id: i32 + = note: route `has_one` has uri "/" diff --git a/core/codegen/tests/ui-fail-nightly/uri_display_type_errors.stderr b/core/codegen/tests/ui-fail-nightly/uri_display_type_errors.stderr index 10085692..1114a845 100644 --- a/core/codegen/tests/ui-fail-nightly/uri_display_type_errors.stderr +++ b/core/codegen/tests/ui-fail-nightly/uri_display_type_errors.stderr @@ -1,56 +1,56 @@ -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:6:13 | 6 | struct Bar1(BadType); - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:10:5 | 10 | field: BadType, - | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:16:5 | 16 | bad: BadType, - | ^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:21:11 | 21 | Inner(BadType), - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:27:9 | 27 | field: BadType, - | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:35:9 | 35 | other: BadType, - | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:40:12 diff --git a/core/codegen/tests/ui-fail-stable/catch.stderr b/core/codegen/tests/ui-fail-stable/catch.stderr index 2b0e8ea3..b720f953 100644 --- a/core/codegen/tests/ui-fail-stable/catch.stderr +++ b/core/codegen/tests/ui-fail-stable/catch.stderr @@ -12,7 +12,7 @@ error: expected `fn` 9 | const CATCH: &str = "Catcher"; | ^^^^^ -error: expected integer or identifier, found string literal +error: expected integer or `default`, found string literal --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` --> $DIR/catch.rs:11:9 | diff --git a/core/codegen/tests/ui-fail-stable/route-attribute-general-syntax.stderr b/core/codegen/tests/ui-fail-stable/route-attribute-general-syntax.stderr index bd742b5d..ebd19470 100644 --- a/core/codegen/tests/ui-fail-stable/route-attribute-general-syntax.stderr +++ b/core/codegen/tests/ui-fail-stable/route-attribute-general-syntax.stderr @@ -1,4 +1,4 @@ -error: missing expected parameter: `path` +error: missing expected parameter: `uri` --> $DIR/route-attribute-general-syntax.rs:4:1 | 4 | #[get()] @@ -65,7 +65,7 @@ error: expected key/value `key = value` | ^^^ error: handler arguments cannot be ignored - --- help: all handler arguments must be of the form: `ident: Type` + --- note: handler arguments must be of the form: `ident: Type` --> $DIR/route-attribute-general-syntax.rs:39:7 | 39 | fn c1(_: usize) {} diff --git a/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr b/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr index dc254fa1..3ce980bd 100644 --- a/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr +++ b/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr @@ -1,51 +1,53 @@ error: invalid route URI: expected token '/' but found 'a' at index 0 - --- help: expected path in origin form: "/path/" + --- help: expected URI in origin form: "/path/" --> $DIR/route-path-bad-syntax.rs:5:7 | 5 | #[get("a")] | ^^^ error: invalid route URI: unexpected EOF: expected token '/' at index 0 - --- help: expected path in origin form: "/path/" + --- help: expected URI in origin form: "/path/" --> $DIR/route-path-bad-syntax.rs:8:7 | 8 | #[get("")] | ^^ error: invalid route URI: expected token '/' but found 'a' at index 0 - --- help: expected path in origin form: "/path/" + --- help: expected URI in origin form: "/path/" --> $DIR/route-path-bad-syntax.rs:11:7 | 11 | #[get("a/b/c")] | ^^^^^^^ -error: paths cannot contain empty segments - --- note: expected '/a/b', found '/a///b' +error: route URIs cannot contain empty segments + --- note: expected "/a/b", found "/a///b" --> $DIR/route-path-bad-syntax.rs:14:7 | 14 | #[get("/a///b")] | ^^^^^^^^ -error: query cannot contain empty segments +error: route URIs cannot contain empty segments + --- note: expected "/?bat", found "/?bat&&" --> $DIR/route-path-bad-syntax.rs:17:7 | 17 | #[get("/?bat&&")] | ^^^^^^^^^ -error: query cannot contain empty segments +error: route URIs cannot contain empty segments + --- note: expected "/?bat", found "/?bat&&" --> $DIR/route-path-bad-syntax.rs:20:7 | 20 | #[get("/?bat&&")] | ^^^^^^^^^ -error: paths cannot contain empty segments - --- note: expected '/a/b', found '/a/b//' +error: route URIs cannot contain empty segments + --- note: expected "/a/b", found "/a/b//" --> $DIR/route-path-bad-syntax.rs:23:7 | 23 | #[get("/a/b//")] | ^^^^^^^^ -error: unused dynamic parameter +error: unused parameter --> $DIR/route-path-bad-syntax.rs:42:7 | 42 | #[get("/")] @@ -57,7 +59,7 @@ error: [note] expected argument named `name` here 43 | fn h0(_name: usize) {} | ^^^^^^^^^^^^^^ -error: unused dynamic parameter +error: unused parameter --> $DIR/route-path-bad-syntax.rs:45:7 | 45 | #[get("/a?")] @@ -69,7 +71,7 @@ error: [note] expected argument named `r` here 46 | fn h1() {} | ^^ -error: unused dynamic parameter +error: unused parameter --> $DIR/route-path-bad-syntax.rs:48:21 | 48 | #[post("/a", data = "")] @@ -81,7 +83,7 @@ error: [note] expected argument named `test` here 49 | fn h2() {} | ^^ -error: unused dynamic parameter +error: unused parameter --> $DIR/route-path-bad-syntax.rs:51:7 | 51 | #[get("/<_r>")] @@ -93,7 +95,7 @@ error: [note] expected argument named `_r` here 52 | fn h3() {} | ^^ -error: unused dynamic parameter +error: unused parameter --> $DIR/route-path-bad-syntax.rs:54:7 | 54 | #[get("/<_r>/")] @@ -105,95 +107,76 @@ error: [note] expected argument named `b` here 55 | fn h4() {} | ^^ -error: `foo_.` is not a valid identifier - --- help: parameter names must be valid identifiers +error: invalid identifier: `foo_.` + --- help: dynamic parameters must be valid identifiers + --- help: did you mean ``? --> $DIR/route-path-bad-syntax.rs:60:7 | 60 | #[get("/")] | ^^^^^^^^^^ -error: `foo*` is not a valid identifier - --- help: parameter names must be valid identifiers +error: invalid identifier: `foo*` + --- help: dynamic parameters must be valid identifiers + --- help: did you mean ``? --> $DIR/route-path-bad-syntax.rs:63:7 | 63 | #[get("/")] | ^^^^^^^^^ -error: `!` is not a valid identifier - --- help: parameter names must be valid identifiers +error: invalid identifier: `!` + --- help: dynamic parameters must be valid identifiers + --- help: did you mean ``? --> $DIR/route-path-bad-syntax.rs:66:7 | 66 | #[get("/")] | ^^^^^^ -error: `name>::`? --> $DIR/route-path-bad-syntax.rs:69:7 | 69 | #[get("/:")] | ^^^^^^^^^^^^^^ -error: malformed parameter - --- help: parameter must be of the form '' +error: unexpected static parameter + --- help: parameter must be dynamic: `` --> $DIR/route-path-bad-syntax.rs:74:19 | 74 | #[get("/", data = "foo")] | ^^^^^ -error: malformed parameter - --- help: parameter must be of the form '' +error: parameter cannot be trailing + --- help: did you mean ``? --> $DIR/route-path-bad-syntax.rs:77:19 | 77 | #[get("/", data = "")] | ^^^^^^^^^ -error: parameter is missing a closing bracket - --- help: did you mean ''? +error: unexpected static parameter + --- help: parameter must be dynamic: `` --> $DIR/route-path-bad-syntax.rs:80:19 | 80 | #[get("/", data = "`? --> $DIR/route-path-bad-syntax.rs:83:19 | 83 | #[get("/", data = "")] | ^^^^^^^^^ error: handler arguments cannot be ignored - --- help: all handler arguments must be of the form: `ident: Type` + --- note: handler arguments must be of the form: `ident: Type` --> $DIR/route-path-bad-syntax.rs:89:7 | 89 | fn k0(_: usize) {} | ^ -error: parameter names cannot be empty +error: parameters cannot be empty --> $DIR/route-path-bad-syntax.rs:93:7 | 93 | #[get("/<>")] | ^^^^^ - -error: malformed parameter or identifier - --- help: parameters must be of the form '' - --- help: identifiers cannot contain '<' or '>' - --> $DIR/route-path-bad-syntax.rs:96:7 - | -96 | #[get("/<")] - | ^^^^^^^^ - -error: malformed parameter or identifier - --- help: parameters must be of the form '' - --- help: identifiers cannot contain '<' or '>' - --> $DIR/route-path-bad-syntax.rs:99:7 - | -99 | #[get("/<<<<")] - | ^^^^^^^^^^^ - -error: malformed parameter or identifier - --- help: parameters must be of the form '' - --- help: identifiers cannot contain '<' or '>' - --> $DIR/route-path-bad-syntax.rs:102:7 - | -102 | #[get("/<>name><")] - | ^^^^^^^^^^^ diff --git a/core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr b/core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr index 686693d1..7af18958 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr @@ -70,11 +70,11 @@ error[E0277]: the trait bound `std::string::String: FromUriParam>` for `std::result::Result` = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:58:20 | 58 | uri!(simple_q: "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` + | ^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following implementations were found: > @@ -82,11 +82,11 @@ error[E0277]: the trait bound `isize: FromUriParam` is not satisfie > = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:60:25 | 60 | uri!(simple_q: id = "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` + | ^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following implementations were found: > @@ -94,48 +94,48 @@ error[E0277]: the trait bound `isize: FromUriParam` is not satisfie > = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied +error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:62:24 | 62 | uri!(other_q: 100, S); - | ^ the trait `FromUriParam` is not implemented for `S` + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied +error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:64:26 | 64 | uri!(other_q: rest = S, id = 100); - | ^ the trait `FromUriParam` is not implemented for `S` + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `S: Ignorable` is not satisfied +error[E0277]: the trait bound `S: Ignorable` is not satisfied --> $DIR/typed-uri-bad-type.rs:66:26 | 66 | uri!(other_q: rest = _, id = 100); - | ^ the trait `Ignorable` is not implemented for `S` + | ^ the trait `Ignorable` is not implemented for `S` | ::: $WORKSPACE/core/http/src/uri/uri_display.rs | | pub fn assert_ignorable>() { } | ------------ required by this bound in `assert_ignorable` -error[E0277]: the trait bound `usize: Ignorable` is not satisfied +error[E0277]: the trait bound `usize: Ignorable` is not satisfied --> $DIR/typed-uri-bad-type.rs:68:34 | 68 | uri!(other_q: rest = S, id = _); - | ^ the trait `Ignorable` is not implemented for `usize` + | ^ the trait `Ignorable` is not implemented for `usize` | ::: $WORKSPACE/core/http/src/uri/uri_display.rs | | pub fn assert_ignorable>() { } | ------------ required by this bound in `assert_ignorable` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied +error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> $DIR/typed-uri-bad-type.rs:68:26 | 68 | uri!(other_q: rest = S, id = _); - | ^ the trait `FromUriParam` is not implemented for `S` + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` diff --git a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr index 0d00572f..83c203a0 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr @@ -1,233 +1,266 @@ -error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:55:37 +error: expected identifier + --> $DIR/typed-uris-bad-params.rs:62:19 | -55 | uri!(optionals: id = 10, name = _); +62 | uri!(ignored: _ = 10); + | ^ + +error: expected 1 parameter but 2 were supplied + --- note: route `ignored` has uri "/<_>" + --> $DIR/typed-uris-bad-params.rs:68:19 + | +68 | uri!(ignored: 10, "10"); + | ^^ + +error: expected unnamed arguments due to ignored parameters + --- note: uri for route `ignored` ignores path parameters: "/<_>" + --> $DIR/typed-uris-bad-params.rs:66:19 + | +66 | uri!(ignored: num = 10); + | ^^^ + +error: expected 1 parameter but 2 were supplied + --- note: route `ignored` has uri "/<_>" + --> $DIR/typed-uris-bad-params.rs:64:19 + | +64 | uri!(ignored: 10, 20); + | ^^ + +error: path parameters cannot be ignored + --> $DIR/typed-uris-bad-params.rs:60:19 + | +60 | uri!(ignored: _); + | ^ + +error: path parameters cannot be ignored + --> $DIR/typed-uris-bad-params.rs:58:37 + | +58 | uri!(optionals: id = 10, name = _); | ^ error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:53:26 + --> $DIR/typed-uris-bad-params.rs:56:26 | -53 | uri!(optionals: id = _, name = "bob".into()); +56 | uri!(optionals: id = _, name = "bob".into()); | ^ error: invalid parameters for `has_two` route uri --- note: uri parameters are: id: i32, name: String --- help: missing parameter: `name` - --> $DIR/typed-uris-bad-params.rs:51:19 + --> $DIR/typed-uris-bad-params.rs:54:19 | -51 | uri!(has_two: id = 100, cookies = "hi"); +54 | uri!(has_two: id = 100, cookies = "hi"); | ^^ error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:51:29 + --> $DIR/typed-uris-bad-params.rs:54:29 | -51 | uri!(has_two: id = 100, cookies = "hi"); +54 | uri!(has_two: id = 100, cookies = "hi"); | ^^^^^^^ error: invalid parameters for `has_two` route uri --- note: uri parameters are: id: i32, name: String --- help: missing parameter: `name` - --> $DIR/typed-uris-bad-params.rs:49:19 + --> $DIR/typed-uris-bad-params.rs:52:19 | -49 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); +52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); | ^^^^^^^ error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:49:19 + --> $DIR/typed-uris-bad-params.rs:52:19 | -49 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); +52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); | ^^^^^^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:49:45 + --> $DIR/typed-uris-bad-params.rs:52:45 | -49 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); +52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); | ^^ error: invalid parameters for `has_two` route uri --- note: uri parameters are: id: i32, name: String --- help: missing parameter: `id` - --> $DIR/typed-uris-bad-params.rs:47:19 + --> $DIR/typed-uris-bad-params.rs:50:19 | -47 | uri!(has_two: name = "hi"); +50 | uri!(has_two: name = "hi"); | ^^^^ error: invalid parameters for `has_two` route uri --- note: uri parameters are: id: i32, name: String --- help: missing parameter: `name` - --> $DIR/typed-uris-bad-params.rs:45:19 + --> $DIR/typed-uris-bad-params.rs:48:19 | -45 | uri!(has_two: id = 100, id = 100, ); +48 | uri!(has_two: id = 100, id = 100, ); | ^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:45:29 + --> $DIR/typed-uris-bad-params.rs:48:29 | -45 | uri!(has_two: id = 100, id = 100, ); +48 | uri!(has_two: id = 100, id = 100, ); | ^^ error: invalid parameters for `has_one_guarded` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:43:27 + --> $DIR/typed-uris-bad-params.rs:46:27 | -43 | uri!(has_one_guarded: id = 100, cookies = "hi"); +46 | uri!(has_one_guarded: id = 100, cookies = "hi"); | ^^ error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:43:37 + --> $DIR/typed-uris-bad-params.rs:46:37 | -43 | uri!(has_one_guarded: id = 100, cookies = "hi"); +46 | uri!(has_one_guarded: id = 100, cookies = "hi"); | ^^^^^^^ error: invalid parameters for `has_one_guarded` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:41:27 + --> $DIR/typed-uris-bad-params.rs:44:27 | -41 | uri!(has_one_guarded: cookies = "hi", id = 100); +44 | uri!(has_one_guarded: cookies = "hi", id = 100); | ^^^^^^^ error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:41:27 + --> $DIR/typed-uris-bad-params.rs:44:27 | -41 | uri!(has_one_guarded: cookies = "hi", id = 100); +44 | uri!(has_one_guarded: cookies = "hi", id = 100); | ^^^^^^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 --- help: missing parameter: `id` - --> $DIR/typed-uris-bad-params.rs:39:19 + --> $DIR/typed-uris-bad-params.rs:42:19 | -39 | uri!(has_one: name = "hi"); +42 | uri!(has_one: name = "hi"); | ^^^^ error: [help] unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:39:19 + --> $DIR/typed-uris-bad-params.rs:42:19 | -39 | uri!(has_one: name = "hi"); +42 | uri!(has_one: name = "hi"); | ^^^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:37:19 + --> $DIR/typed-uris-bad-params.rs:40:19 | -37 | uri!(has_one: id = 100, id = 100, ); +40 | uri!(has_one: id = 100, id = 100, ); | ^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:37:29 + --> $DIR/typed-uris-bad-params.rs:40:29 | -37 | uri!(has_one: id = 100, id = 100, ); +40 | uri!(has_one: id = 100, id = 100, ); | ^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:35:19 + --> $DIR/typed-uris-bad-params.rs:38:19 | -35 | uri!(has_one: id = 100, id = 100); +38 | uri!(has_one: id = 100, id = 100); | ^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:35:29 + --> $DIR/typed-uris-bad-params.rs:38:29 | -35 | uri!(has_one: id = 100, id = 100); +38 | uri!(has_one: id = 100, id = 100); | ^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:33:19 + --> $DIR/typed-uris-bad-params.rs:36:19 | -33 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); +36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); | ^^^^ error: [help] unknown parameters: `name`, `age` - --> $DIR/typed-uris-bad-params.rs:33:19 + --> $DIR/typed-uris-bad-params.rs:36:19 | -33 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); +36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); | ^^^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:33:51 + --> $DIR/typed-uris-bad-params.rs:36:51 | -33 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); +36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); | ^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:31:19 + --> $DIR/typed-uris-bad-params.rs:34:19 | -31 | uri!(has_one: name = 100, age = 50, id = 100); +34 | uri!(has_one: name = 100, age = 50, id = 100); | ^^^^ error: [help] unknown parameters: `name`, `age` - --> $DIR/typed-uris-bad-params.rs:31:19 + --> $DIR/typed-uris-bad-params.rs:34:19 | -31 | uri!(has_one: name = 100, age = 50, id = 100); +34 | uri!(has_one: name = 100, age = 50, id = 100); | ^^^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:29:19 + --> $DIR/typed-uris-bad-params.rs:32:19 | -29 | uri!(has_one: name = 100, id = 100); +32 | uri!(has_one: name = 100, id = 100); | ^^^^ error: [help] unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:29:19 + --> $DIR/typed-uris-bad-params.rs:32:19 | -29 | uri!(has_one: name = 100, id = 100); +32 | uri!(has_one: name = 100, id = 100); | ^^^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:27:19 + --> $DIR/typed-uris-bad-params.rs:30:19 | -27 | uri!(has_one: id = 100, name = "hi"); +30 | uri!(has_one: id = 100, name = "hi"); | ^^ error: [help] unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:27:29 + --> $DIR/typed-uris-bad-params.rs:30:29 | -27 | uri!(has_one: id = 100, name = "hi"); +30 | uri!(has_one: id = 100, name = "hi"); | ^^^^ -error: `has_two` route uri expects 2 parameters but 1 was supplied - --- note: expected parameters: id: i32, name: String - --> $DIR/typed-uris-bad-params.rs:25:19 +error: expected 2 parameters but 1 was supplied + --- note: route `has_two` has uri "/?" + --> $DIR/typed-uris-bad-params.rs:28:19 | -25 | uri!(has_two: 10); +28 | uri!(has_two: 10); | ^^ -error: `has_two` route uri expects 2 parameters but 3 were supplied - --- note: expected parameters: id: i32, name: String - --> $DIR/typed-uris-bad-params.rs:24:19 +error: expected 2 parameters but 3 were supplied + --- note: route `has_two` has uri "/?" + --> $DIR/typed-uris-bad-params.rs:27:19 | -24 | uri!(has_two: 10, "hi", "there"); +27 | uri!(has_two: 10, "hi", "there"); | ^^ -error: `has_one_guarded` route uri expects 1 parameter but 2 were supplied - --- note: expected parameter: id: i32 - --> $DIR/typed-uris-bad-params.rs:22:27 +error: expected 1 parameter but 2 were supplied + --- note: route `has_one_guarded` has uri "/" + --> $DIR/typed-uris-bad-params.rs:25:27 | -22 | uri!(has_one_guarded: "hi", 100); +25 | uri!(has_one_guarded: "hi", 100); | ^^^^ -error: `has_one` route uri expects 1 parameter but 2 were supplied - --- note: expected parameter: id: i32 - --> $DIR/typed-uris-bad-params.rs:21:19 +error: expected 1 parameter but 2 were supplied + --- note: route `has_one` has uri "/" + --> $DIR/typed-uris-bad-params.rs:24:19 | -21 | uri!(has_one: "Hello", 23, ); +24 | uri!(has_one: "Hello", 23, ); | ^^^^^^^ -error: `has_one` route uri expects 1 parameter but 2 were supplied - --- note: expected parameter: id: i32 - --> $DIR/typed-uris-bad-params.rs:20:19 +error: expected 1 parameter but 2 were supplied + --- note: route `has_one` has uri "/" + --> $DIR/typed-uris-bad-params.rs:23:19 | -20 | uri!(has_one: 1, 23); +23 | uri!(has_one: 1, 23); | ^ -error: `has_one` route uri expects 1 parameter but 0 were supplied - --- note: expected parameter: id: i32 - --> $DIR/typed-uris-bad-params.rs:18:10 +error: expected 1 parameter but 0 were supplied + --- note: route `has_one` has uri "/" + --> $DIR/typed-uris-bad-params.rs:21:10 | -18 | uri!(has_one); +21 | uri!(has_one); | ^^^^^^^ diff --git a/core/codegen/tests/ui-fail-stable/uri_display_type_errors.stderr b/core/codegen/tests/ui-fail-stable/uri_display_type_errors.stderr index 1a473fa0..45f0b8f9 100644 --- a/core/codegen/tests/ui-fail-stable/uri_display_type_errors.stderr +++ b/core/codegen/tests/ui-fail-stable/uri_display_type_errors.stderr @@ -1,56 +1,56 @@ -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:6:13 | 6 | struct Bar1(BadType); - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:10:5 | 10 | field: BadType, - | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:16:5 | 16 | bad: BadType, - | ^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:21:11 | 21 | Inner(BadType), - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:27:9 | 27 | field: BadType, - | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:35:9 | 35 | other: BadType, - | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:40:12 diff --git a/core/codegen/tests/ui-fail/typed-uris-bad-params.rs b/core/codegen/tests/ui-fail/typed-uris-bad-params.rs index 9f18a27e..8ffe0f97 100644 --- a/core/codegen/tests/ui-fail/typed-uris-bad-params.rs +++ b/core/codegen/tests/ui-fail/typed-uris-bad-params.rs @@ -14,6 +14,9 @@ fn has_two(cookies: &CookieJar<'_>, id: i32, name: String) { } #[post("//")] fn optionals(id: Option, name: Result) { } +#[post("/<_>")] +fn ignored() { } + fn main() { uri!(has_one); @@ -53,4 +56,14 @@ fn main() { uri!(optionals: id = _, name = "bob".into()); uri!(optionals: id = 10, name = _); + + uri!(ignored: _); + + uri!(ignored: _ = 10); + + uri!(ignored: 10, 20); + + uri!(ignored: num = 10); + + uri!(ignored: 10, "10"); } diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 011a3150..2ef8b2d0 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -30,7 +30,6 @@ time = "0.2.11" indexmap = { version = "1.5.2", features = ["std"] } tokio-rustls = { version = "0.22.0", optional = true } tokio = { version = "1.0", features = ["net", "sync", "time"] } -unicode-xid = "0.2" log = "0.4" ref-cast = "1.0" uncased = "0.9.4" diff --git a/core/http/src/lib.rs b/core/http/src/lib.rs index 888fdce1..7e22b173 100644 --- a/core/http/src/lib.rs +++ b/core/http/src/lib.rs @@ -27,9 +27,6 @@ mod docify; #[cfg(feature = "tls")] pub mod tls; -#[doc(hidden)] -pub mod route; - #[macro_use] mod header; mod cookies; diff --git a/core/http/src/raw_str.rs b/core/http/src/raw_str.rs index 75799b2a..fb0f6b88 100644 --- a/core/http/src/raw_str.rs +++ b/core/http/src/raw_str.rs @@ -5,7 +5,7 @@ use std::str::Utf8Error; use std::fmt; use ref_cast::RefCast; -use stable_pattern::{Pattern, ReverseSearcher, Split, SplitInternal}; +use stable_pattern::{Pattern, Searcher, ReverseSearcher, Split, SplitInternal}; use crate::uncased::UncasedStr; @@ -540,6 +540,34 @@ impl RawStr { pat.is_suffix_of(self.as_str()) } + + /// Returns the byte index of the first character of this string slice that + /// matches the pattern. + /// + /// Returns [`None`] if the pattern doesn't match. + /// + /// The pattern can be a `&str`, [`char`], a slice of [`char`]s, or a + /// function or closure that determines if a character matches. + /// + /// [`char`]: prim@char + /// + /// # Example + /// + /// ``` + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let s = RawStr::new("Löwe 老虎 Léopard Gepardi"); + /// + /// assert_eq!(s.find('L'), Some(0)); + /// assert_eq!(s.find('é'), Some(14)); + /// assert_eq!(s.find("pard"), Some(17)); + /// ``` + #[inline] + pub fn find<'a, P: Pattern<'a>>(&'a self, pat: P) -> Option { + pat.into_searcher(self.as_str()).next_match().map(|(i, _)| i) + } + /// An iterator over substrings of this string slice, separated by /// characters matched by a pattern. /// diff --git a/core/http/src/route.rs b/core/http/src/route.rs deleted file mode 100644 index 30176a8e..00000000 --- a/core/http/src/route.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::borrow::Cow; -use std::marker::PhantomData; - -use unicode_xid::UnicodeXID; - -use crate::ext::IntoOwned; -use crate::uri::{Origin, UriPart, Path, Query}; - -use self::Error::*; - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum Kind { - Static, - Single, - Multi, -} - -#[derive(Debug, Clone)] -pub struct RouteSegment<'a, P: UriPart> { - pub string: Cow<'a, str>, - pub kind: Kind, - pub name: Cow<'a, str>, - pub index: Option, - _part: PhantomData

, -} - -impl IntoOwned for RouteSegment<'_, P> { - type Owned = RouteSegment<'static, P>; - - #[inline] - fn into_owned(self) -> Self::Owned { - RouteSegment { - string: IntoOwned::into_owned(self.string), - kind: self.kind, - name: IntoOwned::into_owned(self.name), - index: self.index, - _part: PhantomData - } - } -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum Error<'a> { - Empty, - Ident(&'a str), - Ignored, - MissingClose, - Malformed, - Uri, - Trailing(&'a str) -} - -impl std::fmt::Display for Error<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Empty => "parameter names cannot be empty".fmt(f), - Ident(name) => write!(f, "`{}` is not a valid identifier", name), - Ignored => "parameter must be named".fmt(f), - MissingClose => "parameter is missing a closing bracket".fmt(f), - Malformed => "malformed parameter or identifier".fmt(f), - Uri => "segment contains invalid URI characters".fmt(f), - Trailing(i) => write!(f, "unexpected trailing text after `{}`", i) - } - } -} - -pub type SResult<'a, P> = Result, (&'a str, Error<'a>)>; - -#[inline] -fn is_ident_start(c: char) -> bool { - c.is_ascii_alphabetic() - || c == '_' - || (c > '\x7f' && UnicodeXID::is_xid_start(c)) -} - -#[inline] -fn is_ident_continue(c: char) -> bool { - c.is_ascii_alphanumeric() - || c == '_' - || (c > '\x7f' && UnicodeXID::is_xid_continue(c)) -} - -fn is_valid_ident(string: &str) -> bool { - let mut chars = string.chars(); - match chars.next() { - Some(c) => is_ident_start(c) && chars.all(is_ident_continue), - None => false - } -} - -impl<'a, P: UriPart> RouteSegment<'a, P> { - pub fn parse_one(segment: &'a str) -> Result> { - let (string, index) = (segment.into(), None); - - // Check if this is a dynamic param. If so, check its well-formedness. - if segment.starts_with('<') && segment.ends_with('>') { - let mut kind = Kind::Single; - let mut name = &segment[1..(segment.len() - 1)]; - if name.ends_with("..") { - kind = Kind::Multi; - name = &name[..(name.len() - 2)]; - } - - if name.is_empty() { - return Err(Empty); - } else if !is_valid_ident(name) { - return Err(Ident(name)); - } else if name == "_" && P::DELIMITER != '/' { - // Only path segments may be ignored. - return Err(Ignored); - } - - let name = name.into(); - return Ok(RouteSegment { string, name, kind, index, _part: PhantomData }); - } else if segment.is_empty() { - return Err(Empty); - } else if segment.starts_with('<') && segment.len() > 1 - && !segment[1..].contains('<') && !segment[1..].contains('>') { - return Err(MissingClose); - } else if segment.contains('>') || segment.contains('<') { - return Err(Malformed); - } - - Ok(RouteSegment { - string, index, - name: segment.into(), - kind: Kind::Static, - _part: PhantomData - }) - } - - pub fn parse_many + ?Sized> ( - string: &'a S, - ) -> impl Iterator> { - let mut last_multi_seg: Option<&str> = None; - // We check for empty segments when we parse an `Origin` in `FromMeta`. - string.as_ref() - .split(P::DELIMITER) - .filter(|s| !s.is_empty()) - .enumerate() - .map(move |(i, seg)| { - if let Some(multi_seg) = last_multi_seg { - return Err((seg, Trailing(multi_seg))); - } - - let mut parsed = Self::parse_one(seg).map_err(|e| (seg, e))?; - if parsed.kind == Kind::Multi { - last_multi_seg = Some(seg); - } - - parsed.index = Some(i); - Ok(parsed) - }) - } -} - -impl<'a> RouteSegment<'a, Path> { - pub fn parse(uri: &'a Origin<'_>) -> impl Iterator> { - Self::parse_many(uri.path().as_str()) - } -} - -impl<'a> RouteSegment<'a, Query> { - pub fn parse(uri: &'a Origin<'_>) -> Option>> { - uri.query().map(|q| Self::parse_many(q.as_str())) - } -} diff --git a/core/http/src/uri/mod.rs b/core/http/src/uri/mod.rs index 94b14bfa..f7e786a0 100644 --- a/core/http/src/uri/mod.rs +++ b/core/http/src/uri/mod.rs @@ -53,12 +53,21 @@ mod private { /// [`UriDisplay`]: crate::uri::UriDisplay /// [`Formatter`]: crate::uri::Formatter pub trait UriPart: private::Sealed { + /// The dynamic version of `Self`. + #[doc(hidden)] + const KIND: Kind; + /// The delimiter used to separate components of this URI part. /// Specifically, `/` for `Path` and `&` for `Query`. #[doc(hidden)] const DELIMITER: char; } +/// Dynamic version of the `Path` and `Query` parts. +#[doc(hidden)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Kind { Path, Query } + /// Marker type indicating use of a type for the path [`UriPart`] of a URI. /// /// In route URIs, this corresponds to all of the text before a `?`, if any, or @@ -87,9 +96,11 @@ pub enum Path { } pub enum Query { } impl UriPart for Path { + const KIND: Kind = Kind::Path; const DELIMITER: char = '/'; } impl UriPart for Query { + const KIND: Kind = Kind::Query; const DELIMITER: char = '&'; } diff --git a/core/http/src/uri/origin.rs b/core/http/src/uri/origin.rs index 6fbc6343..580a9e5e 100644 --- a/core/http/src/uri/origin.rs +++ b/core/http/src/uri/origin.rs @@ -42,7 +42,8 @@ use state::Storage; /// "/", /// "/a/b/c", /// "/a/b/c?q", -/// "/some%20thing" +/// "/hello?lang=en", +/// "/some%20thing?q=foo&lang=fr", /// # ]; /// # for uri in &valid_uris { /// # assert!(Origin::parse(uri).unwrap().is_normalized()); @@ -57,7 +58,9 @@ use state::Storage; /// # let invalid = [ /// "//", // one empty segment /// "/a/b/", // trailing empty segment -/// "/a/ab//c//d" // two empty segments +/// "/a/ab//c//d", // two empty segments +/// "/?a&&b", // empty query segment +/// "/?foo&", // trailing empty query segment /// # ]; /// # for uri in &invalid { /// # assert!(!Origin::parse(uri).unwrap().is_normalized()); @@ -72,11 +75,10 @@ use state::Storage; /// # use rocket::http::uri::Origin; /// # let invalid = [ /// // abnormal versions -/// "//", "/a/b/", "/a/ab//c//d" -/// # , +/// "//", "/a/b/", "/a/ab//c//d", "/a?a&&b&", /// /// // normalized versions -/// "/", "/a/b", "/a/ab/c/d" +/// "/", "/a/b", "/a/ab/c/d", "/a?a&b", /// # ]; /// # for i in 0..(invalid.len() / 2) { /// # let abnormal = Origin::parse(invalid[i]).unwrap(); @@ -285,14 +287,19 @@ impl<'a> Origin<'a> { /// let normal = Origin::parse("/a/b/c").unwrap(); /// assert!(normal.is_normalized()); /// + /// let normal = Origin::parse("/a/b/c?a=b&c").unwrap(); + /// assert!(normal.is_normalized()); + /// /// let abnormal = Origin::parse("/a/b/c//d").unwrap(); /// assert!(!abnormal.is_normalized()); + /// + /// let abnormal = Origin::parse("/a?q&&b").unwrap(); + /// assert!(!abnormal.is_normalized()); /// ``` pub fn is_normalized(&self) -> bool { - let path_str = self.path().as_str(); - path_str.starts_with('/') && - !path_str.contains("//") && - !(path_str.len() > 1 && path_str.ends_with('/')) + self.path().starts_with('/') + && self.raw_path_segments().all(|s| !s.is_empty()) + && self.raw_query_segments().all(|s| !s.is_empty()) } /// Normalizes `self`. @@ -314,21 +321,37 @@ impl<'a> Origin<'a> { /// assert_eq!(normalized, Origin::parse("/a/b/c/d").unwrap()); /// ``` pub fn into_normalized(mut self) -> Self { + use std::fmt::Write; + if self.is_normalized() { self } else { let mut new_path = String::with_capacity(self.path().len()); - for segment in self.raw_path_segments() { - use std::fmt::Write; - let _ = write!(new_path, "/{}", segment); + for seg in self.raw_path_segments().filter(|s| !s.is_empty()) { + let _ = write!(new_path, "/{}", seg); } if new_path.is_empty() { new_path.push('/'); } - // Note: normalization preserves segmments! self.path = Indexed::from(Cow::Owned(new_path)); + + if let Some(q) = self.query() { + let mut new_query = String::with_capacity(q.len()); + let raw_segments = self.raw_query_segments() + .filter(|s| !s.is_empty()) + .enumerate(); + + for (i, seg) in raw_segments { + if i != 0 { new_query.push('&'); } + let _ = write!(new_query, "{}", seg); + } + + self.query = Some(Indexed::from(Cow::Owned(new_query))); + } + + // Note: normalization preserves segments! self } } @@ -467,6 +490,7 @@ impl<'a> Origin<'a> { let cached = self.decoded_path_segs.get_or_set(|| { let (indexed, path) = (&self.path, self.path()); self.raw_path_segments() + .filter(|r| !r.is_empty()) .map(|s| decode_to_indexed_str::(s, (indexed, path))) .collect() }); @@ -500,6 +524,8 @@ impl<'a> Origin<'a> { }; self.raw_query_segments() + .filter(|s| !s.is_empty()) + .map(|s| s.split_at_byte(b'=')) .map(|(name, val)| { let name = decode_to_indexed_str::(name, (indexed, query)); let val = decode_to_indexed_str::(val, (indexed, query)); @@ -512,58 +538,46 @@ impl<'a> Origin<'a> { } /// Returns an iterator over the raw, undecoded segments of the path in this - /// URI. + /// URI. Segments may be empty. /// - /// ### Examples - /// - /// A valid URI with only non-empty segments: + /// ### Example /// /// ```rust /// # extern crate rocket; /// use rocket::http::uri::Origin; /// - /// let uri = Origin::parse("/a/b/c?a=true").unwrap(); - /// # let segments: Vec<_> = uri.raw_path_segments().collect(); - /// # assert_eq!(segments, &["a", "b", "c"]); + /// let uri = Origin::parse("/").unwrap(); + /// let segments: Vec<_> = uri.raw_path_segments().collect(); + /// assert!(segments.is_empty()); /// - /// for (i, segment) in uri.raw_path_segments().enumerate() { - /// match i { - /// 0 => assert_eq!(segment, "a"), - /// 1 => assert_eq!(segment, "b"), - /// 2 => assert_eq!(segment, "c"), - /// _ => unreachable!("only three segments") - /// } - /// } - /// ``` + /// let uri = Origin::parse("//").unwrap(); + /// let segments: Vec<_> = uri.raw_path_segments().collect(); + /// assert_eq!(segments, &["", ""]); /// - /// A URI with empty segments: + /// let uri = Origin::parse("/a").unwrap(); + /// let segments: Vec<_> = uri.raw_path_segments().collect(); + /// assert_eq!(segments, &["a"]); /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Origin; - /// - /// let uri = Origin::parse("///a//b///c////d?query¶m").unwrap(); - /// # let segments: Vec<_> = uri.raw_path_segments().collect(); - /// # assert_eq!(segments, &["a", "b", "c", "d"]); - /// - /// for (i, segment) in uri.raw_path_segments().enumerate() { - /// match i { - /// 0 => assert_eq!(segment, "a"), - /// 1 => assert_eq!(segment, "b"), - /// 2 => assert_eq!(segment, "c"), - /// 3 => assert_eq!(segment, "d"), - /// _ => unreachable!("only four segments") - /// } - /// } + /// let uri = Origin::parse("/a//b///c/d?query¶m").unwrap(); + /// let segments: Vec<_> = uri.raw_path_segments().collect(); + /// assert_eq!(segments, &["a", "", "b", "", "", "c", "d"]); /// ``` #[inline(always)] pub fn raw_path_segments(&self) -> impl Iterator { - self.path().split(Path::DELIMITER).filter(|s| !s.is_empty()) + let path = match self.path() { + p if p == "/" => None, + p if p.starts_with('/') => Some(&p[1..]), + p => Some(p) + }; + + path.map(|p| p.split(Path::DELIMITER)) + .into_iter() + .flatten() } - /// Returns a (smart) iterator over the non-empty, non-url-decoded `(name, - /// value)` pairs of the query of this URI. If there is no query, the - /// iterator is empty. + /// Returns an iterator over the non-empty, non-url-decoded `(name, value)` + /// pairs of the query of this URI. If there is no query, the iterator is + /// empty. Segments may be empty. /// /// # Example /// @@ -572,23 +586,25 @@ impl<'a> Origin<'a> { /// use rocket::http::uri::Origin; /// /// let uri = Origin::parse("/").unwrap(); + /// assert!(uri.raw_query_segments().next().is_none()); + /// + /// let uri = Origin::parse("/?a=b&dog").unwrap(); /// let query_segs: Vec<_> = uri.raw_query_segments().collect(); - /// assert!(query_segs.is_empty()); + /// assert_eq!(query_segs, &["a=b", "dog"]); + /// + /// let uri = Origin::parse("/?&").unwrap(); + /// let query_segs: Vec<_> = uri.raw_query_segments().collect(); + /// assert_eq!(query_segs, &["", ""]); /// /// let uri = Origin::parse("/foo/bar?a+b%2F=some+one%40gmail.com&&%26%3D2").unwrap(); - /// let query_segs: Vec<_> = uri.raw_query_segments() - /// .map(|(name, val)| (name.as_str(), val.as_str())) - /// .collect(); - /// - /// assert_eq!(query_segs, &[("a+b%2F", "some+one%40gmail.com"), ("%26%3D2", "")]); + /// let query_segs: Vec<_> = uri.raw_query_segments().collect(); + /// assert_eq!(query_segs, &["a+b%2F=some+one%40gmail.com", "", "%26%3D2"]); /// ``` - #[inline(always)] - pub fn raw_query_segments(&self) -> impl Iterator { - self.query().into_iter().flat_map(|q| { - q.split(Query::DELIMITER) - .filter(|s| !s.is_empty()) - .map(|q| q.split_at_byte(b'=')) - }) + #[inline] + pub fn raw_query_segments(&self) -> impl Iterator { + self.query() + .into_iter() + .flat_map(|q| q.split(Query::DELIMITER)) } } diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index c5a71673..0e82935e 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -205,7 +205,6 @@ impl Drop for Error { use crate::http::uri; use crate::http::ext::IntoOwned; -use crate::http::route::Error as SegmentError; /// Error returned by [`Route::map_base()`] on invalid URIs. #[derive(Debug)] @@ -218,12 +217,6 @@ pub enum RouteUriError { DynamicBase, } -impl<'a> From<(&'a str, SegmentError<'a>)> for RouteUriError { - fn from((seg, err): (&'a str, SegmentError<'a>)) -> Self { - RouteUriError::Segment(seg.into(), err.to_string()) - } -} - impl<'a> From> for RouteUriError { fn from(error: uri::Error<'a>) -> Self { RouteUriError::Uri(error.into_owned()) diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index aec4b3fe..0dce75c9 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -1,8 +1,6 @@ use super::Route; use crate::http::MediaType; -use crate::http::route::Kind; -use crate::form::ValueField; use crate::request::Request; impl Route { @@ -48,15 +46,15 @@ impl Route { } fn paths_collide(route: &Route, other: &Route) -> bool { - let a_segments = &route.metadata.path_segments; - let b_segments = &other.metadata.path_segments; + let a_segments = &route.metadata.path_segs; + let b_segments = &other.metadata.path_segs; for (seg_a, seg_b) in a_segments.iter().zip(b_segments.iter()) { - if seg_a.kind == Kind::Multi || seg_b.kind == Kind::Multi { + if seg_a.trailing || seg_b.trailing { return true; } - if seg_a.kind == Kind::Static && seg_b.kind == Kind::Static { - if seg_a.string != seg_b.string { + if !seg_a.dynamic && !seg_b.dynamic { + if seg_a.value != seg_b.value { return false; } } @@ -66,17 +64,23 @@ fn paths_collide(route: &Route, other: &Route) -> bool { } fn paths_match(route: &Route, req: &Request<'_>) -> bool { - let route_segments = &route.metadata.path_segments; + let route_segments = &route.metadata.path_segs; let req_segments = req.routed_segments(0..); if route_segments.len() > req_segments.len() { return false; } + if route.metadata.wild_path { + return true; + } + for (route_seg, req_seg) in route_segments.iter().zip(req_segments) { - match route_seg.kind { - Kind::Multi => return true, - Kind::Static if route_seg.string != req_seg => return false, - _ => continue, + if route_seg.trailing { + return true; + } + + if !route_seg.dynamic && route_seg.value != req_seg { + return false; } } @@ -84,21 +88,16 @@ fn paths_match(route: &Route, req: &Request<'_>) -> bool { } fn queries_match(route: &Route, req: &Request<'_>) -> bool { - if route.metadata.fully_dynamic_query { + if route.metadata.wild_query { return true; } - let route_segments = match route.metadata.query_segments { - Some(ref segments) => segments.iter(), - None => return true - }; + let route_query_fields = route.metadata.static_query_fields.iter() + .map(|(k, v)| (k.as_str(), v.as_str())); - for seg in route_segments.filter(|s| s.kind == Kind::Static) { - if !req.query_fields().any(|f| f == ValueField::parse(&seg.string)) { - trace_!("route {} missing static query {}", route, seg.string); - for f in req.query_fields() { - trace_!("field: {:?}", f); - } + for route_seg in route_query_fields { + if !req.uri().query_segments().any(|req_seg| req_seg == route_seg) { + trace_!("request {} missing static query {:?}", req, route_seg); return false; } } diff --git a/core/lib/src/router/mod.rs b/core/lib/src/router/mod.rs index 85383943..12fe220a 100644 --- a/core/lib/src/router/mod.rs +++ b/core/lib/src/router/mod.rs @@ -1,5 +1,6 @@ mod collider; mod route; +mod segment; use std::collections::HashMap; @@ -8,6 +9,7 @@ use crate::http::Method; use crate::handler::dummy; pub use self::route::Route; +pub use self::segment::Segment; // type Selector = (Method, usize); type Selector = Method; diff --git a/core/lib/src/router/route.rs b/core/lib/src/router/route.rs index a25125ef..7cacb858 100644 --- a/core/lib/src/router/route.rs +++ b/core/lib/src/router/route.rs @@ -6,10 +6,11 @@ use yansi::Paint; use crate::codegen::StaticRouteInfo; use crate::handler::Handler; use crate::http::{Method, MediaType}; -use crate::http::route::{RouteSegment, Kind}; use crate::error::RouteUriError; use crate::http::ext::IntoOwned; -use crate::http::uri::{Origin, Path, Query}; +use crate::http::uri::Origin; +use crate::router::Segment; +use crate::form::ValueField; /// A route: a method, its handler, path, rank, and format/media type. #[derive(Clone)] @@ -37,43 +38,24 @@ pub struct Route { #[derive(Debug, Default, Clone)] pub(crate) struct Metadata { - pub path_segments: Vec>, - pub query_segments: Option>>, - pub fully_dynamic_query: bool, -} - -impl Metadata { - fn from(route: &Route) -> Result { - let path_segments = >::parse(&route.uri) - .map(|res| res.map(|s| s.into_owned())) - .collect::, _>>()?; - - let (query_segments, is_dyn) = match >::parse(&route.uri) { - Some(results) => { - let segments = results.map(|res| res.map(|s| s.into_owned())) - .collect::, _>>()?; - - let dynamic = !segments.iter().any(|s| s.kind == Kind::Static); - - (Some(segments), dynamic) - } - None => (None, true) - }; - - Ok(Metadata { path_segments, query_segments, fully_dynamic_query: is_dyn }) - } + pub path_segs: Vec, + pub query_segs: Vec, + pub static_query_fields: Vec<(String, String)>, + pub static_path: bool, + pub wild_path: bool, + pub wild_query: bool, } #[inline(always)] fn default_rank(route: &Route) -> isize { - let static_path = route.metadata.path_segments.iter().all(|s| s.kind == Kind::Static); - let partly_static_query = route.uri.query().map(|_| !route.metadata.fully_dynamic_query); - match (static_path, partly_static_query) { - (true, Some(true)) => -6, // static path, partly static query - (true, Some(false)) => -5, // static path, fully dynamic query + let static_path = route.metadata.static_path; + let wild_query = route.uri.query().map(|_| route.metadata.wild_query); + match (static_path, wild_query) { + (true, Some(false)) => -6, // static path, partly static query + (true, Some(true)) => -5, // static path, fully dynamic query (true, None) => -4, // static path, no query - (false, Some(true)) => -3, // dynamic path, partly static query - (false, Some(false)) => -2, // dynamic path, fully dynamic query + (false, Some(false)) => -3, // dynamic path, partly static query + (false, Some(true)) => -2, // dynamic path, fully dynamic query (false, None) => -1, // dynamic path, no query } } @@ -186,16 +168,37 @@ impl Route { method, rank, }; - route.update_metadata().unwrap_or_else(|e| panic(path, e)); + route.update_metadata(); route } + fn metadata(&self) -> Metadata { + let path_segs = self.uri.raw_path_segments() + .map(Segment::from) + .collect::>(); + + let query_segs = self.uri.raw_query_segments() + .map(Segment::from) + .collect::>(); + + Metadata { + static_path: path_segs.iter().all(|s| !s.dynamic), + wild_path: path_segs.iter().all(|s| s.dynamic) + && path_segs.last().map_or(false, |p| p.trailing), + wild_query: query_segs.iter().all(|s| s.dynamic), + static_query_fields: query_segs.iter().filter(|s| !s.dynamic) + .map(|s| ValueField::parse(&s.value)) + .map(|f| (f.name.source().to_string(), f.value.to_string())) + .collect(), + path_segs, + query_segs, + } + } + /// Updates the cached routing metadata. MUST be called whenver the route's /// URI is set or changes. - fn update_metadata(&mut self) -> Result<(), RouteUriError> { - let new_metadata = Metadata::from(&*self)?; - self.metadata = new_metadata; - Ok(()) + fn update_metadata(&mut self) { + self.metadata = self.metadata(); } /// Retrieves the path of the base mount point of this route as an `&str`. @@ -272,7 +275,7 @@ impl Route { let new_uri = format!("{}{}", self.base, self.path); self.uri = Origin::parse_route(&new_uri)?.into_owned().into_normalized(); - self.update_metadata()?; + self.update_metadata(); Ok(self) } } diff --git a/core/lib/src/router/segment.rs b/core/lib/src/router/segment.rs new file mode 100644 index 00000000..3e394410 --- /dev/null +++ b/core/lib/src/router/segment.rs @@ -0,0 +1,28 @@ +use crate::http::RawStr; + +#[derive(Debug, Clone)] +pub struct Segment { + pub value: String, + pub dynamic: bool, + pub trailing: bool, +} + +impl Segment { + pub fn from(segment: &RawStr) -> Self { + let mut value = segment; + let mut dynamic = false; + let mut trailing = false; + + if segment.starts_with('<') && segment.ends_with('>') { + dynamic = true; + value = &segment[1..(segment.len() - 1)]; + + if value.ends_with("..") { + trailing = true; + value = &value[..(value.len() - 2)]; + } + } + + Segment { value: value.to_string(), dynamic, trailing } + } +} diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index a6d198a1..79c8b680 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -1515,8 +1515,7 @@ segment in its query string. For example, the route below will match requests with path `/` and _at least_ the query segments `hello` and `cat=♥`: -```rust,ignore -# FIXME: https://github.com/rust-lang/rust/issues/82583 +```rust # #[macro_use] extern crate rocket; #[get("/?hello&cat=♥")] @@ -1524,17 +1523,17 @@ fn cats() -> &'static str { "Hello, kittens!" } -// The following GET requests match `cats`. +// The following GET requests match `cats`. `%E2%99%A5` is encoded `♥`. # let status = rocket_guide_tests::client(routes![cats]).get( -"/?cat%3D%E2%99%A5%26hello" +"/?cat=%E2%99%A5&hello" # ).dispatch().status(); # assert_eq!(status, rocket::http::Status::Ok); # let status = rocket_guide_tests::client(routes![cats]).get( -"/?hello&cat%3D%E2%99%A5%26" +"/?hello&cat=%E2%99%A5" # ).dispatch().status(); # assert_eq!(status, rocket::http::Status::Ok); # let status = rocket_guide_tests::client(routes![cats]).get( -"/?dogs=amazing&hello&there&cat%3D%E2%99%A5%26" +"/?dogs=amazing&hello&there&cat=%E2%99%A5" # ).dispatch().status(); # assert_eq!(status, rocket::http::Status::Ok); ``` diff --git a/site/tests/src/lib.rs b/site/tests/src/lib.rs index a9a7350c..c888f34d 100644 --- a/site/tests/src/lib.rs +++ b/site/tests/src/lib.rs @@ -50,6 +50,8 @@ macro_rules! assert_form_parses_ok { } pub fn client(routes: Vec) -> rocket::local::blocking::Client { - let rocket = rocket::custom(rocket::Config::debug_default()).mount("/", routes); + let mut config = rocket::Config::debug_default(); + config.log_level = rocket::config::LogLevel::Debug; + let rocket = rocket::custom(config).mount("/", routes); rocket::local::blocking::Client::tracked(rocket).unwrap() }