|
|
@@ -1,5 +1,3 @@
|
|
|
-use std::{collections::HashMap, fmt::Display};
|
|
|
-
|
|
|
use nom::{
|
|
|
bytes::complete::{tag, take_while, take_while1},
|
|
|
character::complete::char,
|
|
|
@@ -8,6 +6,7 @@ use nom::{
|
|
|
Parser,
|
|
|
};
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
+use std::{collections::HashMap, fmt::Display};
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
pub struct RequestUrlOwned {
|
|
|
@@ -21,8 +20,8 @@ pub struct RequestUrlOwned {
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
#[serde(tag = "type", content = "value")]
|
|
|
pub enum SegmentOwned {
|
|
|
- Static(String),
|
|
|
- Dynamic(String),
|
|
|
+ Static(String, usize),
|
|
|
+ Dynamic(String, usize),
|
|
|
}
|
|
|
|
|
|
/// A fully deconstructed URL from a workspace request.
|
|
|
@@ -56,7 +55,7 @@ pub struct RequestUrl<'a> {
|
|
|
type NomError<'a> = nom::Err<(&'a str, nom::error::ErrorKind)>;
|
|
|
|
|
|
impl<'a> RequestUrl<'a> {
|
|
|
- pub fn parse(input: &'a str) -> Result<Self, UrlParseError> {
|
|
|
+ pub fn parse(original_input: &'a str) -> Result<Self, UrlParseError> {
|
|
|
fn parse_query(query: &str) -> Result<(Vec<(&str, &str)>, bool), NomError<'_>> {
|
|
|
if query.is_empty() {
|
|
|
return Ok((vec![], false));
|
|
|
@@ -95,13 +94,17 @@ impl<'a> RequestUrl<'a> {
|
|
|
Ok((query_params, trailing_pair))
|
|
|
}
|
|
|
|
|
|
- let (input, scheme) = match take_while1(char::is_alphabetic)(input) {
|
|
|
+ let mut offset = 0;
|
|
|
+
|
|
|
+ let (input, scheme) = match take_while1(char::is_alphabetic)(original_input) {
|
|
|
Ok((i, s)) => (i, s),
|
|
|
- Err(e) => return Err(map_nom_err(input, None, e)),
|
|
|
+ Err(e) => return Err(map_nom_err(original_input, None, e)),
|
|
|
};
|
|
|
|
|
|
let (input, _) = tag("://")(input).map_err(|e| map_nom_err(input, Some("://"), e))?;
|
|
|
|
|
|
+ offset += scheme.len() + 3;
|
|
|
+
|
|
|
// Parse until first /
|
|
|
|
|
|
let (path, host) =
|
|
|
@@ -126,11 +129,16 @@ impl<'a> RequestUrl<'a> {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ offset += host.len();
|
|
|
+
|
|
|
// Parse until query
|
|
|
|
|
|
let (query, path) =
|
|
|
take_while(|c| c != '?')(path).map_err(|e| map_nom_err(path, None, e))?;
|
|
|
|
|
|
+ #[cfg(debug_assertions)]
|
|
|
+ debug_assert_eq!(&original_input[offset..], path.to_owned() + query);
|
|
|
+
|
|
|
let (query_params, trailing) =
|
|
|
parse_query(query).map_err(|e| map_nom_err(query, None, e))?;
|
|
|
|
|
|
@@ -138,26 +146,37 @@ impl<'a> RequestUrl<'a> {
|
|
|
.parse(path)
|
|
|
.map_err(|e| map_nom_err(path, None, e))?;
|
|
|
|
|
|
+ let mut path = vec![];
|
|
|
+
|
|
|
+ for segment in segments {
|
|
|
+ match preceded(
|
|
|
+ tag::<_, _, nom::error::Error<_>>(":"),
|
|
|
+ nom::combinator::rest,
|
|
|
+ )
|
|
|
+ .parse(segment)
|
|
|
+ {
|
|
|
+ Ok((remainder, segment)) => {
|
|
|
+ debug_assert_eq!("", remainder);
|
|
|
+ path.push(Segment::Dynamic(segment, offset));
|
|
|
+ // account for :
|
|
|
+ offset += segment.len() + 1;
|
|
|
+ }
|
|
|
+ Err(_) => {
|
|
|
+ path.push(Segment::Static(segment, offset));
|
|
|
+ offset += segment.len();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // account for the parsed /
|
|
|
+ offset += 1;
|
|
|
+ }
|
|
|
+
|
|
|
debug_assert!(remainder.is_empty());
|
|
|
|
|
|
Ok(RequestUrl {
|
|
|
scheme,
|
|
|
host,
|
|
|
- path: segments
|
|
|
- .into_iter()
|
|
|
- .map(|segment| {
|
|
|
- preceded(
|
|
|
- tag::<_, _, nom::error::Error<_>>(":"),
|
|
|
- nom::combinator::rest,
|
|
|
- )
|
|
|
- .parse(segment)
|
|
|
- .ok()
|
|
|
- .map_or(Segment::Static(segment), |(r, s)| {
|
|
|
- debug_assert_eq!("", r);
|
|
|
- Segment::Dynamic(s)
|
|
|
- })
|
|
|
- })
|
|
|
- .collect(),
|
|
|
+ path,
|
|
|
query_params,
|
|
|
trailing_query: query == "?",
|
|
|
trailing_query_pair: trailing,
|
|
|
@@ -165,10 +184,36 @@ impl<'a> RequestUrl<'a> {
|
|
|
}
|
|
|
|
|
|
pub fn populate(&mut self, path_params: HashMap<&'a str, &'a str>) {
|
|
|
+ let mut total_displaced = 0i64;
|
|
|
+
|
|
|
for path in self.path.iter_mut() {
|
|
|
- if let Segment::Dynamic(value) = path {
|
|
|
- let val = path_params.get(value).unwrap_or(&"");
|
|
|
- std::mem::swap(path, &mut Segment::Static(val));
|
|
|
+ match path {
|
|
|
+ Segment::Dynamic(value, offset) => {
|
|
|
+ dbg!(*offset, total_displaced);
|
|
|
+ let value_len = value.len();
|
|
|
+
|
|
|
+ let new = path_params.get(value).unwrap_or(&"");
|
|
|
+
|
|
|
+ let offset = if total_displaced < 0 {
|
|
|
+ *offset - total_displaced.abs() as usize
|
|
|
+ } else {
|
|
|
+ *offset + total_displaced as usize
|
|
|
+ };
|
|
|
+
|
|
|
+ *path = Segment::Static(new, offset);
|
|
|
+
|
|
|
+ total_displaced += new.len() as i64 - value_len as i64;
|
|
|
+
|
|
|
+ // Account for the :
|
|
|
+ total_displaced -= 1;
|
|
|
+ }
|
|
|
+ Segment::Static(_, offset) => {
|
|
|
+ if total_displaced < 0 {
|
|
|
+ *offset -= total_displaced.abs() as usize;
|
|
|
+ } else {
|
|
|
+ *offset += total_displaced as usize;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -238,32 +283,25 @@ impl<'a> Display for RequestUrl<'a> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/// First value is the parameter name, second is the position of the segment in the input
|
|
|
+/// (including the /).
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
|
pub enum Segment<'a> {
|
|
|
/// Path segments that do not change.
|
|
|
/// The value is the final path value.
|
|
|
- Static(&'a str),
|
|
|
+ Static(&'a str, usize),
|
|
|
|
|
|
/// Path segments that depend on request configuration.
|
|
|
/// The value is the name of the variable in the request configuration
|
|
|
/// that contains the final path value.
|
|
|
- Dynamic(&'a str),
|
|
|
-}
|
|
|
-
|
|
|
-impl<'a> Segment<'a> {
|
|
|
- pub fn value(&self) -> &'a str {
|
|
|
- match self {
|
|
|
- Segment::Static(s) => s,
|
|
|
- Segment::Dynamic(s) => s,
|
|
|
- }
|
|
|
- }
|
|
|
+ Dynamic(&'a str, usize),
|
|
|
}
|
|
|
|
|
|
impl<'a> From<Segment<'a>> for SegmentOwned {
|
|
|
fn from(value: Segment<'a>) -> Self {
|
|
|
match value {
|
|
|
- Segment::Static(s) => Self::Static(s.to_owned()),
|
|
|
- Segment::Dynamic(s) => Self::Dynamic(s.to_owned()),
|
|
|
+ Segment::Static(s, pos) => Self::Static(s.to_owned(), pos),
|
|
|
+ Segment::Dynamic(s, pos) => Self::Dynamic(s.to_owned(), pos),
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -271,8 +309,8 @@ impl<'a> From<Segment<'a>> for SegmentOwned {
|
|
|
impl<'a> Display for Segment<'a> {
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
match self {
|
|
|
- Segment::Static(p) => write!(f, "{p}"),
|
|
|
- Segment::Dynamic(p) => write!(f, ":{p}"),
|
|
|
+ Segment::Static(p, _) => write!(f, "{p}"),
|
|
|
+ Segment::Dynamic(p, _) => write!(f, ":{p}"),
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -344,6 +382,8 @@ where
|
|
|
|
|
|
#[cfg(test)]
|
|
|
mod tests {
|
|
|
+ use std::collections::HashMap;
|
|
|
+
|
|
|
use super::{RequestUrl, Segment};
|
|
|
|
|
|
#[test]
|
|
|
@@ -351,9 +391,9 @@ mod tests {
|
|
|
let input = "http://localhost:4000/foo/:bar/bax";
|
|
|
|
|
|
let expected_path = vec![
|
|
|
- Segment::Static("foo"),
|
|
|
- Segment::Dynamic("bar"),
|
|
|
- Segment::Static("bax"),
|
|
|
+ Segment::Static("foo", "http://localhost:4000".len()),
|
|
|
+ Segment::Dynamic("bar", "http://localhost:4000/foo".len()),
|
|
|
+ Segment::Static("bax", "http://localhost:4000/foo/:bar".len()),
|
|
|
];
|
|
|
|
|
|
let url = RequestUrl::parse(input).unwrap();
|
|
|
@@ -369,10 +409,10 @@ mod tests {
|
|
|
let input = "http://localhost:4000/foo/:bar/bax/";
|
|
|
|
|
|
let expected_path = vec![
|
|
|
- Segment::Static("foo"),
|
|
|
- Segment::Dynamic("bar"),
|
|
|
- Segment::Static("bax"),
|
|
|
- Segment::Static(""),
|
|
|
+ Segment::Static("foo", "http://localhost:4000".len()),
|
|
|
+ Segment::Dynamic("bar", "http://localhost:4000/foo".len()),
|
|
|
+ Segment::Static("bax", "http://localhost:4000/foo/:bar".len()),
|
|
|
+ Segment::Static("", "http://localhost:4000/foo/:bar/bax".len()),
|
|
|
];
|
|
|
|
|
|
let url = RequestUrl::parse(input).unwrap();
|
|
|
@@ -403,7 +443,13 @@ mod tests {
|
|
|
|
|
|
assert_eq!("http", url.scheme);
|
|
|
assert_eq!("localhost:4000", url.host);
|
|
|
- assert_eq!(vec![Segment::Static(""), Segment::Static("")], url.path);
|
|
|
+ assert_eq!(
|
|
|
+ vec![
|
|
|
+ Segment::Static("", "http://localhost:4000".len()),
|
|
|
+ Segment::Static("", "http://localhost:4000/".len())
|
|
|
+ ],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
assert!(url.query_params.is_empty());
|
|
|
}
|
|
|
|
|
|
@@ -415,7 +461,13 @@ mod tests {
|
|
|
|
|
|
assert_eq!("http", url.scheme);
|
|
|
assert_eq!("localhost:4000", url.host);
|
|
|
- assert_eq!(vec![Segment::Dynamic(""), Segment::Dynamic("")], url.path);
|
|
|
+ assert_eq!(
|
|
|
+ vec![
|
|
|
+ Segment::Dynamic("", "http://localhost:4000".len()),
|
|
|
+ Segment::Dynamic("", "http://localhost:4000/:".len())
|
|
|
+ ],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
assert!(url.query_params.is_empty());
|
|
|
}
|
|
|
|
|
|
@@ -427,7 +479,10 @@ mod tests {
|
|
|
|
|
|
assert_eq!("http", url.scheme);
|
|
|
assert_eq!("localhost:4000", url.host);
|
|
|
- assert_eq!(vec![Segment::Static("")], url.path);
|
|
|
+ assert_eq!(
|
|
|
+ vec![Segment::Static("", "http://localhost:4000".len())],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
assert!(url.query_params.is_empty());
|
|
|
}
|
|
|
|
|
|
@@ -453,9 +508,9 @@ mod tests {
|
|
|
assert_eq!("localhost:4000", url.host);
|
|
|
assert_eq!(
|
|
|
vec![
|
|
|
- Segment::Static("foo"),
|
|
|
- Segment::Dynamic("bar"),
|
|
|
- Segment::Dynamic("qux")
|
|
|
+ Segment::Static("foo", "http://localhost:4000".len()),
|
|
|
+ Segment::Dynamic("bar", "http://localhost:4000/foo".len()),
|
|
|
+ Segment::Dynamic("qux", "http://localhost:4000/foo/:bar".len())
|
|
|
],
|
|
|
url.path
|
|
|
);
|
|
|
@@ -472,13 +527,105 @@ mod tests {
|
|
|
assert_eq!("localhost:4000", url.host);
|
|
|
assert_eq!(
|
|
|
vec![
|
|
|
- Segment::Static("foo"),
|
|
|
- Segment::Dynamic("bar"),
|
|
|
- Segment::Dynamic("qux"),
|
|
|
- Segment::Static("")
|
|
|
+ Segment::Static("foo", "http://localhost:4000".len()),
|
|
|
+ Segment::Dynamic("bar", "http://localhost:4000/foo".len()),
|
|
|
+ Segment::Dynamic("qux", "http://localhost:4000/foo/:bar".len()),
|
|
|
+ Segment::Static("", "http://localhost:4000/foo/:bar/:qux".len())
|
|
|
],
|
|
|
url.path
|
|
|
);
|
|
|
assert_eq!(vec![("foo", "bar"), ("baz", "bax")], url.query_params);
|
|
|
}
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn populate_adjusts_positions() {
|
|
|
+ let input = "http://localhost:4000/foo/:bar/qux/:baz/final";
|
|
|
+
|
|
|
+ let mut url = RequestUrl::parse(input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!("http", url.scheme);
|
|
|
+ assert_eq!("localhost:4000", url.host);
|
|
|
+ assert_eq!(
|
|
|
+ vec![
|
|
|
+ Segment::Static("foo", "http://localhost:4000".len()),
|
|
|
+ Segment::Dynamic("bar", "http://localhost:4000/foo".len()),
|
|
|
+ Segment::Static("qux", "http://localhost:4000/foo/:bar".len()),
|
|
|
+ Segment::Dynamic("baz", "http://localhost:4000/foo/:bar/qux".len()),
|
|
|
+ Segment::Static("final", "http://localhost:4000/foo/:bar/qux/:baz".len()),
|
|
|
+ ],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
+
|
|
|
+ url.populate(HashMap::from([("bar", "VALUE"), ("baz", "EULAV")]));
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ "http://localhost:4000/foo/VALUE/qux/EULAV/final",
|
|
|
+ url.to_string()
|
|
|
+ );
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ vec![
|
|
|
+ Segment::Static("foo", "http://localhost:4000".len()),
|
|
|
+ Segment::Static("VALUE", "http://localhost:4000/foo".len()),
|
|
|
+ Segment::Static("qux", "http://localhost:4000/foo/VALUE".len()),
|
|
|
+ Segment::Static("EULAV", "http://localhost:4000/foo/VALUE/qux".len()),
|
|
|
+ Segment::Static("final", "http://localhost:4000/foo/VALUE/qux/EULAV".len()),
|
|
|
+ ],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn populate_adjusts_positions_empty() {
|
|
|
+ let input = "http://foo.com/:ID/:myID";
|
|
|
+
|
|
|
+ let mut url = RequestUrl::parse(input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ vec![
|
|
|
+ Segment::Dynamic("ID", "http://foo.com".len()),
|
|
|
+ Segment::Dynamic("myID", "http://foo.com/:ID".len()),
|
|
|
+ ],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
+
|
|
|
+ url.populate(HashMap::from([("ID", ""), ("myID", "")]));
|
|
|
+
|
|
|
+ assert_eq!("http://foo.com//", url.to_string());
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ vec![
|
|
|
+ Segment::Static("", "http://foo.com".len()),
|
|
|
+ Segment::Static("", "http://foo.com/".len())
|
|
|
+ ],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn populate_adjusts_positions_trailing() {
|
|
|
+ let input = "http://foo.com/:ID/";
|
|
|
+
|
|
|
+ let mut url = RequestUrl::parse(input).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ vec![
|
|
|
+ Segment::Dynamic("ID", "http://foo.com".len()),
|
|
|
+ Segment::Static("", "http://foo.com/:ID".len()),
|
|
|
+ ],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
+
|
|
|
+ url.populate(HashMap::from([("ID", "FOO")]));
|
|
|
+
|
|
|
+ assert_eq!("http://foo.com/FOO/", url.to_string());
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ vec![
|
|
|
+ Segment::Static("FOO", "http://foo.com".len()),
|
|
|
+ Segment::Static("", "http://foo.com/FOO".len())
|
|
|
+ ],
|
|
|
+ url.path
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|