mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-25 12:54:09 +02:00
Match path placeholders with a scan instead of a regex
This commit is contained in:
@@ -11,7 +11,6 @@ hex = { workspace = true }
|
||||
include_dir = "0.7"
|
||||
log = { workspace = true }
|
||||
nanoid = "0.4.0"
|
||||
regex-lite = "0.1"
|
||||
rusqlite = { version = "0.38", features = ["bundled", "chrono"] }
|
||||
sea-query = { version = "1.0", features = ["with-chrono", "attr"] }
|
||||
sea-query-rusqlite = { version = "0.8.0", features = ["with-chrono"] }
|
||||
|
||||
@@ -34,27 +34,41 @@ fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
// A path placeholder is terminated by `/`, `?`, `#`, end-of-string, or a literal `:`.
|
||||
// The `:` boundary is what lets `/:id:increment-importance` substitute the `:id`
|
||||
// placeholder while leaving `:increment-importance` as literal text.
|
||||
let re = regex_lite::Regex::new(format!("(/){}([/?#:]|$)", p.name).as_str()).unwrap();
|
||||
let result = re
|
||||
.replace_all(url, |cap: ®ex_lite::Captures| {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
cap[1].to_string(),
|
||||
urlencoding::encode(p.value.as_str()),
|
||||
cap[2].to_string()
|
||||
)
|
||||
})
|
||||
.into_owned();
|
||||
// A placeholder is `/` followed by the parameter's name (which starts with `:`), and it
|
||||
// ends at `/`, `?`, `#`, a literal `:`, or the end of the URL. The `:` boundary is what
|
||||
// lets `/:id:increment-importance` substitute the `:id` placeholder while leaving
|
||||
// `:increment-importance` as literal text. `/:foooo` is not a match for `:foo`.
|
||||
//
|
||||
// A plain scan rather than a regex: the name is matched literally, so a name containing
|
||||
// `.` or `+` means exactly that, and nothing else in the model layer needs a regex engine.
|
||||
let name = p.name.as_str();
|
||||
let value = urlencoding::encode(p.value.as_str());
|
||||
let mut result = String::with_capacity(url.len());
|
||||
let mut rest = url;
|
||||
while let Some(slash) = rest.find('/') {
|
||||
let after_slash = &rest[slash + 1..];
|
||||
let is_placeholder = after_slash.starts_with(name)
|
||||
&& after_slash[name.len()..]
|
||||
.chars()
|
||||
.next()
|
||||
.is_none_or(|c| matches!(c, '/' | '?' | '#' | ':'));
|
||||
if is_placeholder {
|
||||
result.push_str(&rest[..=slash]);
|
||||
result.push_str(&value);
|
||||
rest = &after_slash[name.len()..];
|
||||
} else {
|
||||
result.push_str(&rest[..=slash]);
|
||||
rest = after_slash;
|
||||
}
|
||||
}
|
||||
result.push_str(rest);
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod placeholder_tests {
|
||||
use crate::path_placeholders::{apply_path_placeholders, replace_path_placeholder};
|
||||
use crate::models::{HttpRequest, HttpUrlParameter};
|
||||
use crate::path_placeholders::{apply_path_placeholders, replace_path_placeholder};
|
||||
|
||||
#[test]
|
||||
fn placeholder_middle() {
|
||||
@@ -98,6 +112,30 @@ mod placeholder_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_name_is_matched_literally() {
|
||||
// `.` in a name is a dot, not "any character".
|
||||
let p = HttpUrlParameter {
|
||||
name: ":id.v2".into(),
|
||||
value: "xxx".into(),
|
||||
enabled: true,
|
||||
id: None,
|
||||
};
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:id.v2/:idXv2"),
|
||||
"https://example.com/xxx/:idXv2",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_repeated() {
|
||||
let p = HttpUrlParameter { name: ":id".into(), value: "7".into(), enabled: true, id: None };
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:id/:id"),
|
||||
"https://example.com/7/7",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_missing() {
|
||||
let p = HttpUrlParameter {
|
||||
|
||||
Reference in New Issue
Block a user