Allow ignored route path segments.

Co-authored-by: timokoesters <timo@koesters.xyz>
This commit is contained in:
Sergio Benitez 2020-07-28 21:35:07 -07:00
parent 23738446f0
commit 8e8fb4cae8
7 changed files with 88 additions and 59 deletions

View File

@ -66,23 +66,23 @@ fn parse_route(attr: RouteAttribute, function: syn::ItemFn) -> Result<Route> {
} }
} }
// 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<Segment> = IndexSet::new(); let mut segments: IndexSet<Segment> = IndexSet::new();
fn dup_check<I>(set: &mut IndexSet<Segment>, iter: I, diags: &mut Diagnostics) fn dup_check<'a, I>(set: &mut IndexSet<Segment>, iter: I, diags: &mut Diagnostics)
where I: Iterator<Item = Segment> where I: Iterator<Item = &'a Segment>
{ {
for segment in iter.filter(|s| s.kind != Kind::Static) { for segment in iter.filter(|s| s.is_dynamic()) {
let span = segment.span; 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)) diags.push(span.error(format!("duplicate parameter: `{}`", previous.name))
.span_note(previous.span, "previous parameter with the same name here")) .span_note(previous.span, "previous parameter with the same name here"))
} }
} }
} }
dup_check(&mut segments, attr.path.path.iter().cloned(), &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().cloned(), &mut diags)); attr.path.query.as_ref().map(|q| dup_check(&mut segments, q.iter(), &mut diags));
dup_check(&mut segments, attr.data.clone().map(|s| s.value.0).into_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. // Check the validity of function arguments.
let mut inputs = vec![]; let mut inputs = vec![];

View File

@ -3,11 +3,11 @@ use std::hash::{Hash, Hasher};
use devise::{syn, Diagnostic, ext::SpanDiagnosticExt}; use devise::{syn, Diagnostic, ext::SpanDiagnosticExt};
use crate::proc_macro2::Span; use crate::proc_macro2::Span;
use crate::http::uri::{UriPart, Path}; use crate::http::uri::{self, UriPart};
use crate::http::route::RouteSegment; use crate::http::route::RouteSegment;
use crate::proc_macro_ext::{Diagnostics, StringLit, PResult, DResult}; 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)] #[derive(Debug, Clone)]
pub struct Segment { pub struct Segment {
@ -18,6 +18,14 @@ pub struct Segment {
pub index: Option<usize>, pub index: Option<usize>,
} }
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Source {
Path,
Query,
Data,
Unknown,
}
impl Segment { impl Segment {
fn from<P: UriPart>(segment: RouteSegment<'_, P>, span: Span) -> Segment { fn from<P: UriPart>(segment: RouteSegment<'_, P>, span: Span) -> Segment {
let source = match P::DELIMITER { let source = match P::DELIMITER {
@ -29,6 +37,17 @@ impl Segment {
let (kind, index) = (segment.kind, segment.index); let (kind, index) = (segment.kind, segment.index);
Segment { span, kind, source, index, name: segment.name.into_owned() } 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 { impl From<&syn::Ident> for Segment {
@ -80,35 +99,33 @@ fn into_diagnostic(
) -> Diagnostic { ) -> Diagnostic {
let seg_span = subspan(segment, source, span); let seg_span = subspan(segment, source, span);
match error { match error {
Error::Empty => { Error::Empty => seg_span.error(error.to_string()),
seg_span.error("parameter names cannot be empty") Error::Ident(_) => {
} seg_span.error(error.to_string())
Error::Ident(name) => {
seg_span.error(format!("`{}` is not a valid identifier", name))
.help("parameter names must be valid identifiers") .help("parameter names must be valid identifiers")
} }
Error::Ignored => { Error::Ignored => {
seg_span.error("parameters must be named") seg_span.error(error.to_string())
.help("use a name such as `_guard` or `_param`") .help("use a name such as `_guard` or `_param`")
} }
Error::MissingClose => { Error::MissingClose => {
seg_span.error("parameter is missing a closing bracket") seg_span.error(error.to_string())
.help(format!("did you mean '{}>'?", segment)) .help(format!("did you mean '{}>'?", segment))
} }
Error::Malformed => { Error::Malformed => {
seg_span.error("malformed parameter or identifier") seg_span.error(error.to_string())
.help("parameters must be of the form '<param>'") .help("parameters must be of the form '<param>'")
.help("identifiers cannot contain '<' or '>'") .help("identifiers cannot contain '<' or '>'")
} }
Error::Uri => { Error::Uri => {
seg_span.error("component contains invalid URI characters") seg_span.error(error.to_string())
.note("components cannot contain reserved characters") .note("components cannot contain reserved characters")
.help("reserved characters include: '%', '+', '&', etc.") .help("reserved characters include: '%', '+', '&', etc.")
} }
Error::Trailing(multi) => { Error::Trailing(multi) => {
let multi_span = subspan(multi, source, span); let multi_span = subspan(multi, source, span);
trailspan(segment, 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") .help("a multi-segment param must be the final component")
.span_note(multi_span, "multi-segment param is here") .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<Segment> { pub fn parse_data_segment(segment: &str, span: Span) -> PResult<Segment> {
<RouteSegment<'_, Path>>::parse_one(segment) <RouteSegment<'_, uri::Query>>::parse_one(segment)
.map(|segment| { .map(|segment| {
let mut seg = Segment::from(segment, span); let mut seg = Segment::from(segment, span);
seg.source = Source::Data; seg.source = Source::Data;
@ -134,14 +151,17 @@ pub fn parse_segments<P: UriPart>(
let mut diags = Diagnostics::new(); let mut diags = Diagnostics::new();
for result in <RouteSegment<'_, P>>::parse_many(string) { for result in <RouteSegment<'_, P>>::parse_many(string) {
if let Err((segment_string, error)) = result { match result {
diags.push(into_diagnostic(segment_string, string, span, &error)); Ok(segment) => {
if let Error::Trailing(..) = error { let seg_span = subspan(&segment.string, string, span);
break; 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));
} }
} }

View File

@ -3,7 +3,8 @@ use std::fmt::Display;
use devise::{syn, Result, ext::SpanDiagnosticExt}; use devise::{syn, Result, ext::SpanDiagnosticExt};
use crate::http::{uri::{Origin, Path, Query}, ext::IntoOwned}; 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::syn::{Expr, Ident, Type, spanned::Spanned};
use crate::http_codegen::Optional; use crate::http_codegen::Optional;

View File

@ -58,7 +58,7 @@ error: invalid path URI: expected EOF but found # at index 3
| |
= help: expected path in origin form: "/path/<param>" = help: expected path in origin form: "/path/<param>"
error: component contains invalid URI characters error: segment contains invalid URI characters
--> $DIR/route-path-bad-syntax.rs:31:9 --> $DIR/route-path-bad-syntax.rs:31:9
| |
31 | #[get("/a%20b")] 31 | #[get("/a%20b")]
@ -67,7 +67,7 @@ error: component contains invalid URI characters
= note: components cannot contain reserved characters = note: components cannot contain reserved characters
= help: reserved characters include: '%', '+', '&', etc. = 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 --> $DIR/route-path-bad-syntax.rs:34:11
| |
34 | #[get("/a?a%20b")] 34 | #[get("/a?a%20b")]
@ -76,7 +76,7 @@ error: component contains invalid URI characters
= note: components cannot contain reserved characters = note: components cannot contain reserved characters
= help: reserved characters include: '%', '+', '&', etc. = 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 --> $DIR/route-path-bad-syntax.rs:37:11
| |
37 | #[get("/a?a+b")] 37 | #[get("/a?a+b")]
@ -221,13 +221,13 @@ error: `test ` is not a valid identifier
| |
= help: parameter names must be valid identifiers = help: parameter names must be valid identifiers
error: parameters must be named error: handler arguments cannot be ignored
--> $DIR/route-path-bad-syntax.rs:88:9 --> $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 error: parameter names cannot be empty
--> $DIR/route-path-bad-syntax.rs:93:9 --> $DIR/route-path-bad-syntax.rs:93:9

View File

@ -52,7 +52,7 @@ error: invalid path URI: expected EOF but found # at index 3
28 | #[get("/!@#$%^&*()")] 28 | #[get("/!@#$%^&*()")]
| ^^^^^^^^^^^^^ | ^^^^^^^^^^^^^
error: component contains invalid URI characters error: segment contains invalid URI characters
--- note: components cannot contain reserved characters --- note: components cannot contain reserved characters
--- help: reserved characters include: '%', '+', '&', etc. --- help: reserved characters include: '%', '+', '&', etc.
--> $DIR/route-path-bad-syntax.rs:31:7 --> $DIR/route-path-bad-syntax.rs:31:7
@ -60,7 +60,7 @@ error: component contains invalid URI characters
31 | #[get("/a%20b")] 31 | #[get("/a%20b")]
| ^^^^^^^^ | ^^^^^^^^
error: component contains invalid URI characters error: segment contains invalid URI characters
--- note: components cannot contain reserved characters --- note: components cannot contain reserved characters
--- help: reserved characters include: '%', '+', '&', etc. --- help: reserved characters include: '%', '+', '&', etc.
--> $DIR/route-path-bad-syntax.rs:34:7 --> $DIR/route-path-bad-syntax.rs:34:7
@ -68,7 +68,7 @@ error: component contains invalid URI characters
34 | #[get("/a?a%20b")] 34 | #[get("/a?a%20b")]
| ^^^^^^^^^^ | ^^^^^^^^^^
error: component contains invalid URI characters error: segment contains invalid URI characters
--- note: components cannot contain reserved characters --- note: components cannot contain reserved characters
--- help: reserved characters include: '%', '+', '&', etc. --- help: reserved characters include: '%', '+', '&', etc.
--> $DIR/route-path-bad-syntax.rs:37:7 --> $DIR/route-path-bad-syntax.rs:37:7
@ -192,12 +192,12 @@ error: `test ` is not a valid identifier
83 | #[get("/", data = "<test >")] 83 | #[get("/", data = "<test >")]
| ^^^^^^^^^ | ^^^^^^^^^
error: parameters must be named error: handler arguments cannot be ignored
--- help: use a name such as `_guard` or `_param` --- help: all handler arguments must be of the form: `ident: Type`
--> $DIR/route-path-bad-syntax.rs:88:7 --> $DIR/route-path-bad-syntax.rs:89:7
| |
88 | #[get("/<_>")] 89 | fn k0(_: usize) {}
| ^^^^^^ | ^
error: parameter names cannot be empty error: parameter names cannot be empty
--> $DIR/route-path-bad-syntax.rs:93:7 --> $DIR/route-path-bad-syntax.rs:93:7

View File

@ -16,14 +16,6 @@ pub enum Kind {
Multi, Multi,
} }
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Source {
Path,
Query,
Data,
Unknown,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RouteSegment<'a, P: UriPart> { pub struct RouteSegment<'a, P: UriPart> {
pub string: Cow<'a, str>, pub string: Cow<'a, str>,
@ -59,6 +51,20 @@ pub enum Error<'a> {
Trailing(&'a str) 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<RouteSegment<'a, P>, (&'a str, Error<'a>)>; pub type SResult<'a, P> = Result<RouteSegment<'a, P>, (&'a str, Error<'a>)>;
#[inline] #[inline]
@ -100,7 +106,8 @@ impl<'a, P: UriPart> RouteSegment<'a, P> {
return Err(Empty); return Err(Empty);
} else if !is_valid_ident(name) { } else if !is_valid_ident(name) {
return Err(Ident(name)); return Err(Ident(name));
} else if name == "_" { } else if name == "_" && P::DELIMITER != '/' {
// Only path segments may be ignored.
return Err(Ignored); return Err(Ignored);
} }
@ -129,6 +136,7 @@ impl<'a, P: UriPart> RouteSegment<'a, P> {
string: &'a str, string: &'a str,
) -> impl Iterator<Item = SResult<'_, P>> { ) -> impl Iterator<Item = SResult<'_, P>> {
let mut last_multi_seg: Option<&str> = None; let mut last_multi_seg: Option<&str> = None;
// We check for empty segments when we parse an `Origin` in `FromMeta`.
string.split(P::DELIMITER) string.split(P::DELIMITER)
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.enumerate() .enumerate()

View File

@ -211,7 +211,7 @@ use crate::http::route::Error as SegmentError;
#[derive(Debug)] #[derive(Debug)]
pub enum RouteUriError { pub enum RouteUriError {
/// The base (mount point) or route path contains invalid segments. /// The base (mount point) or route path contains invalid segments.
Segment, Segment(String, String),
/// The route URI is not a valid URI. /// The route URI is not a valid URI.
Uri(uri::Error<'static>), Uri(uri::Error<'static>),
/// The base (mount point) contains dynamic segments. /// The base (mount point) contains dynamic segments.
@ -219,8 +219,8 @@ pub enum RouteUriError {
} }
impl<'a> From<(&'a str, SegmentError<'a>)> for RouteUriError { impl<'a> From<(&'a str, SegmentError<'a>)> for RouteUriError {
fn from(_: (&'a str, SegmentError<'a>)) -> Self { fn from((seg, err): (&'a str, SegmentError<'a>)) -> Self {
RouteUriError::Segment RouteUriError::Segment(seg.into(), err.to_string())
} }
} }
@ -233,8 +233,8 @@ impl<'a> From<uri::Error<'a>> for RouteUriError {
impl fmt::Display for RouteUriError { impl fmt::Display for RouteUriError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
RouteUriError::Segment => { RouteUriError::Segment(seg, err) => {
write!(f, "The URI contains malformed dynamic route path segments.") write!(f, "Malformed segment '{}': {}", Paint::white(seg), err)
} }
RouteUriError::DynamicBase => { RouteUriError::DynamicBase => {
write!(f, "The mount point contains dynamic parameters.") write!(f, "The mount point contains dynamic parameters.")