Skip to content

Commit

Permalink
Get matched route path information (#1020)
Browse files Browse the repository at this point in the history
* wip

* x

* use {param} instead of <param>

* 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>
  • Loading branch information
chrislearn and github-actions[bot] authored Jan 6, 2025
1 parent 306f67b commit e3e7a23
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 247 deletions.
5 changes: 3 additions & 2 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -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 }
Expand Down
22 changes: 21 additions & 1 deletion crates/core/src/http/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ pub struct Request {

pub(crate) params: PathParams,

// accept: Option<Vec<Mime>>,
pub(crate) queries: OnceLock<MultiMap<String, String>>,
pub(crate) form_data: tokio::sync::OnceCell<FormData>,
pub(crate) payload: tokio::sync::OnceCell<Bytes>,
Expand All @@ -113,6 +112,8 @@ pub struct Request {
pub(crate) remote_addr: SocketAddr,

pub(crate) secure_max_size: Option<usize>,
#[cfg(feature = "matched-path")]
pub(crate) matched_path: String,
}

impl Debug for Request {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -223,6 +226,8 @@ impl Request {
version,
scheme,
secure_max_size: None,
#[cfg(feature = "matched-path")]
matched_path: Default::default(),
}
}

Expand Down Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions crates/core/src/routing/filters/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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");
Expand All @@ -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()]
);
}
}
101 changes: 101 additions & 0 deletions crates/core/src/routing/flow_ctrl.rs
Original file line number Diff line number Diff line change
@@ -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<bool>,
is_ceased: bool,
pub(crate) cursor: usize,
pub(crate) handlers: Vec<Arc<dyn Handler>>,
}

impl FlowCtrl {
/// Create new `FlowCtrl`.
#[inline]
pub fn new(handlers: Vec<Arc<dyn Handler>>) -> 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;
}
}
Loading

0 comments on commit e3e7a23

Please sign in to comment.