diff --git a/core/codegen/src/attribute/route.rs b/core/codegen/src/attribute/route.rs index 8a33b126..de73995f 100644 --- a/core/codegen/src/attribute/route.rs +++ b/core/codegen/src/attribute/route.rs @@ -66,23 +66,23 @@ fn parse_route(attr: RouteAttribute, function: syn::ItemFn) -> Result { } } - // Collect all of the dynamic segments in an `IndexSet`, checking for dups. + // Collect non-wild dynamic segments in an `IndexSet`, checking for dups. let mut segments: IndexSet = IndexSet::new(); - fn dup_check(set: &mut IndexSet, iter: I, diags: &mut Diagnostics) - where I: Iterator + fn dup_check<'a, I>(set: &mut IndexSet, iter: I, diags: &mut Diagnostics) + where I: Iterator { - for segment in iter.filter(|s| s.kind != Kind::Static) { + for segment in iter.filter(|s| s.is_dynamic()) { let span = segment.span; - if let Some(previous) = set.replace(segment) { + 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().cloned(), &mut diags); - attr.path.query.as_ref().map(|q| dup_check(&mut segments, q.iter().cloned(), &mut diags)); - dup_check(&mut segments, attr.data.clone().map(|s| s.value.0).into_iter(), &mut diags); + 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![]; diff --git a/core/codegen/src/attribute/segments.rs b/core/codegen/src/attribute/segments.rs index e306db1f..3d4b9f82 100644 --- a/core/codegen/src/attribute/segments.rs +++ b/core/codegen/src/attribute/segments.rs @@ -3,11 +3,11 @@ use std::hash::{Hash, Hasher}; use devise::{syn, Diagnostic, ext::SpanDiagnosticExt}; use crate::proc_macro2::Span; -use crate::http::uri::{UriPart, Path}; +use crate::http::uri::{self, UriPart}; use crate::http::route::RouteSegment; use crate::proc_macro_ext::{Diagnostics, StringLit, PResult, DResult}; -pub use crate::http::route::{Error, Kind, Source}; +pub use crate::http::route::{Error, Kind}; #[derive(Debug, Clone)] pub struct Segment { @@ -18,6 +18,14 @@ pub struct Segment { 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 { @@ -29,6 +37,17 @@ impl Segment { let (kind, index) = (segment.kind, segment.index); Segment { span, kind, source, index, name: segment.name.into_owned() } } + + 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 { @@ -80,35 +99,33 @@ fn into_diagnostic( ) -> Diagnostic { let seg_span = subspan(segment, source, span); match error { - Error::Empty => { - seg_span.error("parameter names cannot be empty") - } - Error::Ident(name) => { - seg_span.error(format!("`{}` is not a valid identifier", name)) + 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("parameters must be named") + seg_span.error(error.to_string()) .help("use a name such as `_guard` or `_param`") } Error::MissingClose => { - seg_span.error("parameter is missing a closing bracket") + seg_span.error(error.to_string()) .help(format!("did you mean '{}>'?", segment)) } Error::Malformed => { - seg_span.error("malformed parameter or identifier") + seg_span.error(error.to_string()) .help("parameters must be of the form ''") .help("identifiers cannot contain '<' or '>'") } Error::Uri => { - seg_span.error("component contains invalid URI characters") + 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("unexpected trailing text after a '..' param") + .error(error.to_string()) .help("a multi-segment param must be the final component") .span_note(multi_span, "multi-segment param is here") } @@ -116,7 +133,7 @@ fn into_diagnostic( } pub fn parse_data_segment(segment: &str, span: Span) -> PResult { - >::parse_one(segment) + >::parse_one(segment) .map(|segment| { let mut seg = Segment::from(segment, span); seg.source = Source::Data; @@ -134,14 +151,17 @@ pub fn parse_segments( let mut diags = Diagnostics::new(); for result in >::parse_many(string) { - if let Err((segment_string, error)) = result { - diags.push(into_diagnostic(segment_string, string, span, &error)); - if let Error::Trailing(..) = error { - break; + match result { + Ok(segment) => { + let seg_span = subspan(&segment.string, string, span); + segments.push(Segment::from(segment, seg_span)); + }, + Err((segment_string, error)) => { + diags.push(into_diagnostic(segment_string, string, span, &error)); + if let Error::Trailing(..) = error { + break; + } } - } else if let Ok(segment) = result { - let seg_span = subspan(&segment.string, string, span); - segments.push(Segment::from(segment, seg_span)); } } diff --git a/core/codegen/src/bang/uri.rs b/core/codegen/src/bang/uri.rs index b9125a62..b63b6e98 100644 --- a/core/codegen/src/bang/uri.rs +++ b/core/codegen/src/bang/uri.rs @@ -3,7 +3,8 @@ use std::fmt::Display; use devise::{syn, Result, ext::SpanDiagnosticExt}; use crate::http::{uri::{Origin, Path, Query}, ext::IntoOwned}; -use crate::http::route::{RouteSegment, Kind, Source}; +use crate::http::route::{RouteSegment, Kind}; +use crate::attribute::segments::Source; use crate::syn::{Expr, Ident, Type, spanned::Spanned}; use crate::http_codegen::Optional; 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 0864618c..06f691d2 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 @@ -58,7 +58,7 @@ error: invalid path URI: expected EOF but found # at index 3 | = help: expected path in origin form: "/path/" -error: component contains invalid URI characters +error: segment contains invalid URI characters --> $DIR/route-path-bad-syntax.rs:31:9 | 31 | #[get("/a%20b")] @@ -67,7 +67,7 @@ error: component contains invalid URI characters = note: components cannot contain reserved characters = help: reserved characters include: '%', '+', '&', etc. -error: component contains invalid URI characters +error: segment contains invalid URI characters --> $DIR/route-path-bad-syntax.rs:34:11 | 34 | #[get("/a?a%20b")] @@ -76,7 +76,7 @@ error: component contains invalid URI characters = note: components cannot contain reserved characters = help: reserved characters include: '%', '+', '&', etc. -error: component contains invalid URI characters +error: segment contains invalid URI characters --> $DIR/route-path-bad-syntax.rs:37:11 | 37 | #[get("/a?a+b")] @@ -221,13 +221,13 @@ error: `test ` is not a valid identifier | = help: parameter names must be valid identifiers -error: parameters must be named - --> $DIR/route-path-bad-syntax.rs:88:9 +error: handler arguments cannot be ignored + --> $DIR/route-path-bad-syntax.rs:89:7 | -88 | #[get("/<_>")] - | ^^^ +89 | fn k0(_: usize) {} + | ^^^^^^^^ | - = help: use a name such as `_guard` or `_param` + = help: all handler arguments must be of the form: `ident: Type` error: parameter names cannot be empty --> $DIR/route-path-bad-syntax.rs:93:9 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 f3eaf2e5..505f7092 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 @@ -52,7 +52,7 @@ error: invalid path URI: expected EOF but found # at index 3 28 | #[get("/!@#$%^&*()")] | ^^^^^^^^^^^^^ -error: component contains invalid URI characters +error: segment contains invalid URI characters --- note: components cannot contain reserved characters --- help: reserved characters include: '%', '+', '&', etc. --> $DIR/route-path-bad-syntax.rs:31:7 @@ -60,7 +60,7 @@ error: component contains invalid URI characters 31 | #[get("/a%20b")] | ^^^^^^^^ -error: component contains invalid URI characters +error: segment contains invalid URI characters --- note: components cannot contain reserved characters --- help: reserved characters include: '%', '+', '&', etc. --> $DIR/route-path-bad-syntax.rs:34:7 @@ -68,7 +68,7 @@ error: component contains invalid URI characters 34 | #[get("/a?a%20b")] | ^^^^^^^^^^ -error: component contains invalid URI characters +error: segment contains invalid URI characters --- note: components cannot contain reserved characters --- help: reserved characters include: '%', '+', '&', etc. --> $DIR/route-path-bad-syntax.rs:37:7 @@ -192,12 +192,12 @@ error: `test ` is not a valid identifier 83 | #[get("/", data = "")] | ^^^^^^^^^ -error: parameters must be named - --- help: use a name such as `_guard` or `_param` - --> $DIR/route-path-bad-syntax.rs:88:7 +error: handler arguments cannot be ignored + --- help: all handler arguments must be of the form: `ident: Type` + --> $DIR/route-path-bad-syntax.rs:89:7 | -88 | #[get("/<_>")] - | ^^^^^^ +89 | fn k0(_: usize) {} + | ^ error: parameter names cannot be empty --> $DIR/route-path-bad-syntax.rs:93:7 diff --git a/core/http/src/route.rs b/core/http/src/route.rs index f6362e3e..d4c357f2 100644 --- a/core/http/src/route.rs +++ b/core/http/src/route.rs @@ -16,14 +16,6 @@ pub enum Kind { Multi, } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum Source { - Path, - Query, - Data, - Unknown, -} - #[derive(Debug, Clone)] pub struct RouteSegment<'a, P: UriPart> { pub string: Cow<'a, str>, @@ -59,6 +51,20 @@ pub enum Error<'a> { 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] @@ -100,7 +106,8 @@ impl<'a, P: UriPart> RouteSegment<'a, P> { return Err(Empty); } else if !is_valid_ident(name) { return Err(Ident(name)); - } else if name == "_" { + } else if name == "_" && P::DELIMITER != '/' { + // Only path segments may be ignored. return Err(Ignored); } @@ -129,6 +136,7 @@ impl<'a, P: UriPart> RouteSegment<'a, P> { string: &'a str, ) -> impl Iterator> { let mut last_multi_seg: Option<&str> = None; + // We check for empty segments when we parse an `Origin` in `FromMeta`. string.split(P::DELIMITER) .filter(|s| !s.is_empty()) .enumerate() diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index 029618b8..66e60df1 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -211,7 +211,7 @@ use crate::http::route::Error as SegmentError; #[derive(Debug)] pub enum RouteUriError { /// The base (mount point) or route path contains invalid segments. - Segment, + Segment(String, String), /// The route URI is not a valid URI. Uri(uri::Error<'static>), /// The base (mount point) contains dynamic segments. @@ -219,8 +219,8 @@ pub enum RouteUriError { } impl<'a> From<(&'a str, SegmentError<'a>)> for RouteUriError { - fn from(_: (&'a str, SegmentError<'a>)) -> Self { - RouteUriError::Segment + fn from((seg, err): (&'a str, SegmentError<'a>)) -> Self { + RouteUriError::Segment(seg.into(), err.to_string()) } } @@ -233,8 +233,8 @@ impl<'a> From> for RouteUriError { impl fmt::Display for RouteUriError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - RouteUriError::Segment => { - write!(f, "The URI contains malformed dynamic route path segments.") + RouteUriError::Segment(seg, err) => { + write!(f, "Malformed segment '{}': {}", Paint::white(seg), err) } RouteUriError::DynamicBase => { write!(f, "The mount point contains dynamic parameters.")