From e3e7a238ec9c0ea7eeda98ddfa7d8ecd85e53751 Mon Sep 17 00:00:00 2001 From: Chrislearn Young Date: Mon, 6 Jan 2025 12:29:54 +0800 Subject: [PATCH] Get matched route path information (#1020) * wip * x * use {param} instead of * Format Rust code using rustfmt * update doc * fix ci * wip * wip * test * Format Rust code using rustfmt --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/core/Cargo.toml | 5 +- crates/core/src/http/request.rs | 22 ++- crates/core/src/routing/filters/path.rs | 61 ++++++ crates/core/src/routing/flow_ctrl.rs | 101 ++++++++++ crates/core/src/routing/mod.rs | 252 +----------------------- crates/core/src/routing/path_params.rs | 53 +++++ crates/core/src/routing/path_state.rs | 100 ++++++++++ crates/core/src/service.rs | 1 + 8 files changed, 348 insertions(+), 247 deletions(-) create mode 100644 crates/core/src/routing/flow_ctrl.rs create mode 100644 crates/core/src/routing/path_params.rs create mode 100644 crates/core/src/routing/path_state.rs diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index a8efb8b83..66344ec17 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -17,8 +17,8 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["cookie", "fix-http1-request-uri", "server", "server-handle", "http1", "http2", "test", "ring"] -full = ["cookie", "fix-http1-request-uri", "server", "http1", "http2", "http2-cleartext", "quinn", "rustls", "native-tls", "openssl", "unix", "test", "anyhow", "eyre", "ring", "socket2"] +default = ["cookie", "fix-http1-request-uri", "server", "server-handle", "http1", "http2", "test", "ring", "matched-path"] +full = ["cookie", "fix-http1-request-uri", "server", "http1", "http2", "http2-cleartext", "quinn", "rustls", "native-tls", "openssl", "unix", "test", "anyhow", "eyre", "ring", "matched-path", "socket2"] cookie = ["dep:cookie"] fix-http1-request-uri = ["http1"] server = [] @@ -36,6 +36,7 @@ acme = ["http1", "http2", "hyper-util/http1", "hyper-util/http2", "hyper-util/cl socket2 = ["dep:socket2"] # aws-lc-rs = ["hyper-rustls?/aws-lc-rs", "tokio-rustls?/aws-lc-rs"] ring = ["hyper-rustls?/ring", "tokio-rustls?/ring"] +matched-path = [] [dependencies] anyhow = { workspace = true, optional = true } diff --git a/crates/core/src/http/request.rs b/crates/core/src/http/request.rs index 663c8b1dd..65f75a4e1 100644 --- a/crates/core/src/http/request.rs +++ b/crates/core/src/http/request.rs @@ -101,7 +101,6 @@ pub struct Request { pub(crate) params: PathParams, - // accept: Option>, pub(crate) queries: OnceLock>, pub(crate) form_data: tokio::sync::OnceCell, pub(crate) payload: tokio::sync::OnceCell, @@ -113,6 +112,8 @@ pub struct Request { pub(crate) remote_addr: SocketAddr, pub(crate) secure_max_size: Option, + #[cfg(feature = "matched-path")] + pub(crate) matched_path: String, } impl Debug for Request { @@ -159,6 +160,8 @@ impl Request { local_addr: SocketAddr::Unknown, remote_addr: SocketAddr::Unknown, secure_max_size: None, + #[cfg(feature = "matched-path")] + matched_path: Default::default(), } } #[doc(hidden)] @@ -223,6 +226,8 @@ impl Request { version, scheme, secure_max_size: None, + #[cfg(feature = "matched-path")] + matched_path: Default::default(), } } @@ -386,6 +391,21 @@ impl Request { &mut self.local_addr } + cfg_feature! { + #![feature = "matched-path"] + + /// Get matched path. + #[inline] + pub fn matched_path(&self) -> &str { + &self.matched_path + } + /// Get mutable matched path. + #[inline] + pub fn matched_path_mut(&mut self) -> &mut String { + &mut self.matched_path + } + } + /// Returns a reference to the associated header field map. /// /// # Examples diff --git a/crates/core/src/routing/filters/path.rs b/crates/core/src/routing/filters/path.rs index 4c84f133d..6183558fb 100644 --- a/crates/core/src/routing/filters/path.rs +++ b/crates/core/src/routing/filters/path.rs @@ -265,12 +265,16 @@ impl PathWisp for CharsWisp { if chars.len() == max_width { state.forward(max_width); state.params.insert(&self.name, chars.into_iter().collect()); + #[cfg(feature = "matched-path")] + state.matched_parts.push(format!("{{{}}}", self.name)); return true; } } if chars.len() >= self.min_width { state.forward(chars.len()); state.params.insert(&self.name, chars.into_iter().collect()); + #[cfg(feature = "matched-path")] + state.matched_parts.push(format!("{{{}}}", self.name)); true } else { false @@ -285,6 +289,8 @@ impl PathWisp for CharsWisp { if chars.len() >= self.min_width { state.forward(chars.len()); state.params.insert(&self.name, chars.into_iter().collect()); + #[cfg(feature = "matched-path")] + state.matched_parts.push(format!("{{{}}}", self.name)); true } else { false @@ -417,16 +423,37 @@ impl PathWisp for CombWisp { } else { self.names.len() }; + #[cfg(feature = "matched-path")] + let mut start = 0; + #[cfg(feature = "matched-path")] + let mut matched_part = "".to_owned(); for name in self.names.iter().take(take_count) { if let Some(value) = caps.name(name) { state.params.insert(name, value.as_str().to_owned()); if self.wild_regex.is_some() { wild_path = wild_path.trim_start_matches(value.as_str()).to_string(); } + #[cfg(feature = "matched-path")] + { + if value.start() > start { + matched_part.push_str(&picked[start..value.start()]); + } + matched_part.push_str(&format!("{{{}}}", name)); + start = value.end(); + } } else { return false; } } + #[cfg(feature = "matched-path")] + { + if start < picked.len() { + matched_part.push_str(&picked[start..]); + } + if !matched_part.is_empty() { + state.matched_parts.push(matched_part); + } + } let len = if let Some(cap) = caps.get(0) { cap.as_str().len() } else { @@ -455,6 +482,8 @@ impl PathWisp for CombWisp { let cap = cap.as_str().to_owned(); state.forward(cap.len()); state.params.insert(wild_name, cap); + #[cfg(feature = "matched-path")] + state.matched_parts.push(format!("{{{}}}", wild_name)); true } else { false @@ -488,6 +517,8 @@ impl PathWisp for NamedWisp { let rest = rest.to_string(); state.params.insert(&self.0, rest); state.cursor.0 = state.parts.len(); + #[cfg(feature = "matched-path")] + state.matched_parts.push(format!("{{{}}}", self.0)); true } else { false @@ -500,6 +531,8 @@ impl PathWisp for NamedWisp { let picked = picked.expect("picked should not be `None`").to_owned(); state.forward(picked.len()); state.params.insert(&self.0, picked); + #[cfg(feature = "matched-path")] + state.matched_parts.push(format!("{{{}}}", self.0)); true } } @@ -559,6 +592,8 @@ impl PathWisp for RegexWisp { let cap = cap.as_str().to_owned(); state.forward(cap.len()); state.params.insert(&self.name, cap); + #[cfg(feature = "matched-path")] + state.matched_parts.push(format!("{{{}}}", self.name)); true } else { false @@ -575,6 +610,8 @@ impl PathWisp for RegexWisp { let cap = cap.as_str().to_owned(); state.forward(cap.len()); state.params.insert(&self.name, cap); + #[cfg(feature = "matched-path")] + state.matched_parts.push(format!("{{{}}}", self.name)); true } else { false @@ -594,6 +631,8 @@ impl PathWisp for ConstWisp { }; if picked.starts_with(&self.0) { state.forward(self.0.len()); + #[cfg(feature = "matched-path")] + state.matched_parts.push(self.0.clone()); true } else { false @@ -1025,15 +1064,21 @@ impl PathFilter { /// Detect is that path is match. pub fn detect(&self, state: &mut PathState) -> bool { let original_cursor = state.cursor; + #[cfg(feature = "matched-path")] + let original_matched_parts_len = state.matched_parts.len(); for ps in &self.path_wisps { let row = state.cursor.0; if ps.detect(state) { if row == state.cursor.0 && row != state.parts.len() { state.cursor = original_cursor; + #[cfg(feature = "matched-path")] + state.matched_parts.truncate(original_matched_parts_len); return false; } } else { state.cursor = original_cursor; + #[cfg(feature = "matched-path")] + state.matched_parts.truncate(original_matched_parts_len); return false; } } @@ -1319,16 +1364,28 @@ mod tests { let mut state = PathState::new("/users/123e4567-e89b-12d3-a456-9AC7CBDCEE52"); assert!(filter.detect(&mut state)); + assert_eq!( + state.matched_parts, + vec!["users".to_owned(), "{id}".to_owned()] + ); } #[test] fn test_detect_wildcard() { let filter = PathFilter::new("/users/{id}/{**rest}"); let mut state = PathState::new("/users/12/facebook/insights/23"); assert!(filter.detect(&mut state)); + assert_eq!( + state.matched_parts, + vec!["users".to_owned(), "{id}".to_owned(), "{**rest}".to_owned()] + ); let mut state = PathState::new("/users/12/"); assert!(filter.detect(&mut state)); let mut state = PathState::new("/users/12"); assert!(filter.detect(&mut state)); + assert_eq!( + state.matched_parts, + vec!["users".to_owned(), "{id}".to_owned(), "{**rest}".to_owned()] + ); let filter = PathFilter::new("/users/{id}/{*+rest}"); let mut state = PathState::new("/users/12/facebook/insights/23"); @@ -1347,5 +1404,9 @@ mod tests { assert!(filter.detect(&mut state)); let mut state = PathState::new("/users/12/abc"); assert!(filter.detect(&mut state)); + assert_eq!( + state.matched_parts, + vec!["users".to_owned(), "{id}".to_owned(), "{*?rest}".to_owned()] + ); } } diff --git a/crates/core/src/routing/flow_ctrl.rs b/crates/core/src/routing/flow_ctrl.rs new file mode 100644 index 000000000..2ca75a0f9 --- /dev/null +++ b/crates/core/src/routing/flow_ctrl.rs @@ -0,0 +1,101 @@ +use std::sync::Arc; + +use crate::http::{Request, Response}; +use crate::{Depot, Handler}; + +/// Control the flow of execute handlers. +/// +/// When a request is coming, [`Router`] will detect it and get the matched router. +/// And then salvo will collect all handlers (including added as middlewares) from the matched router tree. +/// All handlers in this list will executed one by one. +/// +/// Each handler can use `FlowCtrl` to control execute flow, let the flow call next handler or skip all rest handlers. +/// +/// **NOTE**: When `Response`'s status code is set, and the status code [`Response::is_stamped()`] is returns false, +/// all rest handlers will skipped. +/// +/// [`Router`]: crate::routing::Router +#[derive(Default)] +pub struct FlowCtrl { + catching: Option, + is_ceased: bool, + pub(crate) cursor: usize, + pub(crate) handlers: Vec>, +} + +impl FlowCtrl { + /// Create new `FlowCtrl`. + #[inline] + pub fn new(handlers: Vec>) -> Self { + FlowCtrl { + catching: None, + is_ceased: false, + cursor: 0, + handlers, + } + } + /// Has next handler. + #[inline] + pub fn has_next(&self) -> bool { + self.cursor < self.handlers.len() // && !self.handlers.is_empty() + } + + /// Call next handler. If get next handler and executed, returns `true``, otherwise returns `false`. + /// + /// **NOTE**: If response status code is error or is redirection, all reset handlers will be skipped. + #[inline] + pub async fn call_next( + &mut self, + req: &mut Request, + depot: &mut Depot, + res: &mut Response, + ) -> bool { + if self.catching.is_none() { + self.catching = Some(res.is_stamped()); + } + if !self.catching.unwrap_or_default() && res.is_stamped() { + self.skip_rest(); + return false; + } + let mut handler = self.handlers.get(self.cursor).cloned(); + if handler.is_none() { + false + } else { + while let Some(h) = handler.take() { + self.cursor += 1; + h.handle(req, depot, res, self).await; + if !self.catching.unwrap_or_default() && res.is_stamped() { + self.skip_rest(); + return true; + } else if self.has_next() { + handler = self.handlers.get(self.cursor).cloned(); + } + } + true + } + } + + /// Skip all reset handlers. + #[inline] + pub fn skip_rest(&mut self) { + self.cursor = self.handlers.len() + } + + /// Check is `FlowCtrl` ceased. + /// + /// **NOTE**: If handler is used as middleware, it should use `is_ceased` to check is flow ceased. + /// If `is_ceased` returns `true`, the handler should skip the following logic. + #[inline] + pub fn is_ceased(&self) -> bool { + self.is_ceased + } + /// Cease all following logic. + /// + /// **NOTE**: This function will mark is_ceased as `true`, but whether the subsequent logic can be skipped + /// depends on whether the middleware correctly checks is_ceased and skips the subsequent logic. + #[inline] + pub fn cease(&mut self) { + self.skip_rest(); + self.is_ceased = true; + } +} diff --git a/crates/core/src/routing/mod.rs b/crates/core/src/routing/mod.rs index d983974f0..fa7accc96 100644 --- a/crates/core/src/routing/mod.rs +++ b/crates/core/src/routing/mod.rs @@ -374,14 +374,16 @@ pub use filters::*; mod router; pub use router::Router; -use std::borrow::Cow; -use std::ops::Deref; -use std::sync::Arc; +mod path_params; +pub use path_params::PathParams; +mod path_state; +pub use path_state::PathState; +mod flow_ctrl; +pub use flow_ctrl::FlowCtrl; -use indexmap::IndexMap; +use std::sync::Arc; -use crate::http::{Request, Response}; -use crate::{Depot, Handler}; +use crate::Handler; #[doc(hidden)] pub struct DetectMatched { @@ -389,54 +391,6 @@ pub struct DetectMatched { pub goal: Arc, } -/// The path parameters. -#[derive(Clone, Default, Debug, Eq, PartialEq)] -pub struct PathParams { - inner: IndexMap, - greedy: bool, -} -impl Deref for PathParams { - type Target = IndexMap; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} -impl PathParams { - /// Create new `PathParams`. - pub fn new() -> Self { - PathParams::default() - } - /// If there is a wildcard param, it's value is `true`. - pub fn greedy(&self) -> bool { - self.greedy - } - /// Get the last param starts with '*', for example: {**rest}, {*?rest}. - pub fn tail(&self) -> Option<&str> { - if self.greedy { - self.inner.last().map(|(_, v)| &**v) - } else { - None - } - } - - /// Insert new param. - pub fn insert(&mut self, name: &str, value: String) { - #[cfg(debug_assertions)] - { - if self.greedy { - panic!("only one wildcard param is allowed and it must be the last one."); - } - } - if name.starts_with('*') { - self.inner.insert(split_wild_name(name).1.to_owned(), value); - self.greedy = true; - } else { - self.inner.insert(name.to_owned(), value); - } - } -} - pub(crate) fn split_wild_name(name: &str) -> (&str, &str) { if name.starts_with("*+") || name.starts_with("*?") || name.starts_with("**") { (&name[0..2], &name[2..]) @@ -447,99 +401,6 @@ pub(crate) fn split_wild_name(name: &str) -> (&str, &str) { } } -#[doc(hidden)] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PathState { - pub(crate) parts: Vec, - /// (row, col), row is the index of parts, col is the index of char in the part. - pub(crate) cursor: (usize, usize), - pub(crate) params: PathParams, - pub(crate) end_slash: bool, // For rest match, we want include the last slash. - pub(crate) once_ended: bool, // Once it has ended, used to determine whether the error code returned is 404 or 405. -} -impl PathState { - /// Create new `PathState`. - #[inline] - pub fn new(url_path: &str) -> Self { - let end_slash = url_path.ends_with('/'); - let parts = url_path - .trim_start_matches('/') - .trim_end_matches('/') - .split('/') - .filter_map(|p| { - if !p.is_empty() { - Some(decode_url_path_safely(p)) - } else { - None - } - }) - .collect::>(); - PathState { - parts, - cursor: (0, 0), - params: PathParams::new(), - end_slash, - once_ended: false, - } - } - - #[inline] - pub fn pick(&self) -> Option<&str> { - match self.parts.get(self.cursor.0) { - None => None, - Some(part) => { - if self.cursor.1 >= part.len() { - let row = self.cursor.0 + 1; - self.parts.get(row).map(|s| &**s) - } else { - Some(&part[self.cursor.1..]) - } - } - } - } - - #[inline] - pub fn all_rest(&self) -> Option> { - if let Some(picked) = self.pick() { - if self.cursor.0 >= self.parts.len() - 1 { - if self.end_slash { - Some(Cow::Owned(format!("{picked}/"))) - } else { - Some(Cow::Borrowed(picked)) - } - } else { - let last = self.parts[self.cursor.0 + 1..].join("/"); - if self.end_slash { - Some(Cow::Owned(format!("{picked}/{last}/"))) - } else { - Some(Cow::Owned(format!("{picked}/{last}"))) - } - } - } else { - None - } - } - - #[inline] - pub fn forward(&mut self, steps: usize) { - let mut steps = steps + self.cursor.1; - while let Some(part) = self.parts.get(self.cursor.0) { - if part.len() > steps { - self.cursor.1 = steps; - return; - } else { - steps -= part.len(); - self.cursor = (self.cursor.0 + 1, 0); - } - } - } - - #[inline] - pub fn is_ended(&self) -> bool { - self.cursor.0 >= self.parts.len() - } -} - #[inline] fn decode_url_path_safely(path: &str) -> String { percent_encoding::percent_decode_str(path) @@ -547,103 +408,6 @@ fn decode_url_path_safely(path: &str) -> String { .to_string() } -/// Control the flow of execute handlers. -/// -/// When a request is coming, [`Router`] will detect it and get the matched router. -/// And then salvo will collect all handlers (including added as middlewares) from the matched router tree. -/// All handlers in this list will executed one by one. -/// -/// Each handler can use `FlowCtrl` to control execute flow, let the flow call next handler or skip all rest handlers. -/// -/// **NOTE**: When `Response`'s status code is set, and the status code [`Response::is_stamped()`] is returns false, -/// all rest handlers will skipped. -/// -/// [`Router`]: crate::routing::Router -#[derive(Default)] -pub struct FlowCtrl { - catching: Option, - is_ceased: bool, - pub(crate) cursor: usize, - pub(crate) handlers: Vec>, -} - -impl FlowCtrl { - /// Create new `FlowCtrl`. - #[inline] - pub fn new(handlers: Vec>) -> Self { - FlowCtrl { - catching: None, - is_ceased: false, - cursor: 0, - handlers, - } - } - /// Has next handler. - #[inline] - pub fn has_next(&self) -> bool { - self.cursor < self.handlers.len() // && !self.handlers.is_empty() - } - - /// Call next handler. If get next handler and executed, returns `true``, otherwise returns `false`. - /// - /// **NOTE**: If response status code is error or is redirection, all reset handlers will be skipped. - #[inline] - pub async fn call_next( - &mut self, - req: &mut Request, - depot: &mut Depot, - res: &mut Response, - ) -> bool { - if self.catching.is_none() { - self.catching = Some(res.is_stamped()); - } - if !self.catching.unwrap_or_default() && res.is_stamped() { - self.skip_rest(); - return false; - } - let mut handler = self.handlers.get(self.cursor).cloned(); - if handler.is_none() { - false - } else { - while let Some(h) = handler.take() { - self.cursor += 1; - h.handle(req, depot, res, self).await; - if !self.catching.unwrap_or_default() && res.is_stamped() { - self.skip_rest(); - return true; - } else if self.has_next() { - handler = self.handlers.get(self.cursor).cloned(); - } - } - true - } - } - - /// Skip all reset handlers. - #[inline] - pub fn skip_rest(&mut self) { - self.cursor = self.handlers.len() - } - - /// Check is `FlowCtrl` ceased. - /// - /// **NOTE**: If handler is used as middleware, it should use `is_ceased` to check is flow ceased. - /// If `is_ceased` returns `true`, the handler should skip the following logic. - #[inline] - pub fn is_ceased(&self) -> bool { - self.is_ceased - } - /// Cease all following logic. - /// - /// **NOTE**: This function will mark is_ceased as `true`, but whether the subsequent logic can be skipped - /// depends on whether the middleware correctly checks is_ceased and skips the subsequent logic. - #[inline] - pub fn cease(&mut self) { - self.skip_rest(); - self.is_ceased = true; - } -} - #[cfg(test)] mod tests { use crate::prelude::*; diff --git a/crates/core/src/routing/path_params.rs b/crates/core/src/routing/path_params.rs new file mode 100644 index 000000000..135741481 --- /dev/null +++ b/crates/core/src/routing/path_params.rs @@ -0,0 +1,53 @@ +use std::ops::Deref; + +use indexmap::IndexMap; + +use super::split_wild_name; + +/// The path parameters. +#[derive(Clone, Default, Debug, Eq, PartialEq)] +pub struct PathParams { + inner: IndexMap, + greedy: bool, +} +impl Deref for PathParams { + type Target = IndexMap; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} +impl PathParams { + /// Create new `PathParams`. + pub fn new() -> Self { + PathParams::default() + } + /// If there is a wildcard param, it's value is `true`. + pub fn greedy(&self) -> bool { + self.greedy + } + /// Get the last param starts with '*', for example: <**rest>, <*?rest>. + pub fn tail(&self) -> Option<&str> { + if self.greedy { + self.inner.last().map(|(_, v)| &**v) + } else { + None + } + } + + /// Insert new param. + pub fn insert(&mut self, name: &str, value: String) { + #[cfg(debug_assertions)] + { + if self.greedy { + panic!("only one wildcard param is allowed and it must be the last one."); + } + } + if name.starts_with('*') { + self.inner.insert(split_wild_name(name).1.to_owned(), value); + self.greedy = true; + } else { + self.inner.insert(name.to_owned(), value); + } + } +} diff --git a/crates/core/src/routing/path_state.rs b/crates/core/src/routing/path_state.rs new file mode 100644 index 000000000..8d04fac3a --- /dev/null +++ b/crates/core/src/routing/path_state.rs @@ -0,0 +1,100 @@ +use std::borrow::Cow; + +use super::{decode_url_path_safely, PathParams}; + +#[doc(hidden)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PathState { + pub(crate) parts: Vec, + /// (row, col), row is the index of parts, col is the index of char in the part. + pub(crate) cursor: (usize, usize), + pub(crate) params: PathParams, + #[cfg(feature = "matched-path")] + pub(crate) matched_parts: Vec, + pub(crate) end_slash: bool, // For rest match, we want include the last slash. + pub(crate) once_ended: bool, // Once it has ended, used to determine whether the error code returned is 404 or 405. +} +impl PathState { + /// Create new `PathState`. + #[inline] + pub fn new(url_path: &str) -> Self { + let end_slash = url_path.ends_with('/'); + let parts = url_path + .trim_start_matches('/') + .trim_end_matches('/') + .split('/') + .filter_map(|p| { + if !p.is_empty() { + Some(decode_url_path_safely(p)) + } else { + None + } + }) + .collect::>(); + PathState { + parts, + cursor: (0, 0), + params: PathParams::new(), + end_slash, + once_ended: false, + #[cfg(feature = "matched-path")] + matched_parts: vec![], + } + } + + #[inline] + pub fn pick(&self) -> Option<&str> { + match self.parts.get(self.cursor.0) { + None => None, + Some(part) => { + if self.cursor.1 >= part.len() { + let row = self.cursor.0 + 1; + self.parts.get(row).map(|s| &**s) + } else { + Some(&part[self.cursor.1..]) + } + } + } + } + + #[inline] + pub fn all_rest(&self) -> Option> { + if let Some(picked) = self.pick() { + if self.cursor.0 >= self.parts.len() - 1 { + if self.end_slash { + Some(Cow::Owned(format!("{picked}/"))) + } else { + Some(Cow::Borrowed(picked)) + } + } else { + let last = self.parts[self.cursor.0 + 1..].join("/"); + if self.end_slash { + Some(Cow::Owned(format!("{picked}/{last}/"))) + } else { + Some(Cow::Owned(format!("{picked}/{last}"))) + } + } + } else { + None + } + } + + #[inline] + pub fn forward(&mut self, steps: usize) { + let mut steps = steps + self.cursor.1; + while let Some(part) = self.parts.get(self.cursor.0) { + if part.len() > steps { + self.cursor.1 = steps; + return; + } else { + steps -= part.len(); + self.cursor = (self.cursor.0 + 1, 0); + } + } + } + + #[inline] + pub fn is_ended(&self) -> bool { + self.cursor.0 >= self.parts.len() + } +} diff --git a/crates/core/src/service.rs b/crates/core/src/service.rs index cfebc1f85..c51e363a0 100644 --- a/crates/core/src/service.rs +++ b/crates/core/src/service.rs @@ -226,6 +226,7 @@ impl HyperHandler { async move { if let Some(dm) = router.detect(&mut req, &mut path_state).await { req.params = path_state.params; + req.matched_path = path_state.matched_parts.join("/"); // Set default status code before service hoops executed. // We hope all hoops in service can get the correct status code. let mut ctrl = FlowCtrl::new(