Skip to content

Commit eab3bf4

Browse files
committed
feat: Support variables on resource paths
1 parent f9929bf commit eab3bf4

File tree

4 files changed

+194
-32
lines changed

4 files changed

+194
-32
lines changed

src/context.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ pub struct WebmachineRequest {
1717
pub request_path: String,
1818
/// Resource base path
1919
pub base_path: String,
20+
/// Path parts mapped to any variables (i.e. parts like /{id} will have id mapped)
21+
pub path_vars: HashMap<String, String>,
2022
/// Request method
2123
pub method: String,
2224
/// Request headers
@@ -33,6 +35,7 @@ impl Default for WebmachineRequest {
3335
WebmachineRequest {
3436
request_path: "/".to_string(),
3537
base_path: "/".to_string(),
38+
path_vars: Default::default(),
3639
method: "GET".to_string(),
3740
headers: HashMap::new(),
3841
body: None,

src/lib.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,12 @@ use tracing::{debug, error, trace};
134134

135135
use context::{WebmachineContext, WebmachineRequest, WebmachineResponse};
136136
use headers::HeaderValue;
137+
use crate::paths::map_path;
137138

138139
#[macro_use] pub mod headers;
139140
pub mod context;
140141
pub mod content_negotiation;
142+
pub mod paths;
141143

142144
/// Type of a Webmachine resource callback
143145
pub type WebmachineCallback<T> = Box<dyn Fn(&mut WebmachineContext, &WebmachineResource) -> T + Send + Sync>;
@@ -859,8 +861,15 @@ async fn execute_state_machine(context: &mut WebmachineContext, resource: &Webma
859861
}
860862
}
861863

862-
fn update_paths_for_resource(request: &mut WebmachineRequest, base_path: &str) {
864+
fn update_paths_for_resource(
865+
request: &mut WebmachineRequest,
866+
base_path: &str,
867+
mapped_parts: &Vec<(String, Option<String>)>
868+
) {
863869
request.base_path = base_path.into();
870+
request.path_vars = mapped_parts.iter()
871+
.filter_map(|(part, id)| id.as_ref().map(|id| (id.clone(), part.clone())))
872+
.collect();
864873
if request.request_path.len() > base_path.len() {
865874
let request_path = request.request_path.clone();
866875
let subpath = request_path.split_at(base_path.len()).1;
@@ -985,6 +994,7 @@ async fn request_from_http_request(req: Request<Incoming>) -> WebmachineRequest
985994
WebmachineRequest {
986995
request_path,
987996
base_path: "/".to_string(),
997+
path_vars: Default::default(),
988998
method,
989999
headers,
9901000
body,
@@ -1116,12 +1126,11 @@ impl WebmachineDispatcher {
11161126
}
11171127
}
11181128

1119-
fn match_paths(&self, request: &WebmachineRequest) -> Vec<String> {
1120-
let request_path = sanitise_path(&request.request_path);
1129+
fn match_paths(&self, request: &WebmachineRequest) -> Vec<(String, Vec<(String, Option<String>)>)> {
11211130
self.routes
11221131
.keys()
1123-
.filter(|k| request_path.starts_with(&sanitise_path(k)))
1124-
.map(|k| k.to_string())
1132+
.filter_map(|k| map_path(request.request_path.as_str(), k)
1133+
.map(|result| (k.to_string(), result)))
11251134
.collect()
11261135
}
11271136

@@ -1133,12 +1142,13 @@ impl WebmachineDispatcher {
11331142
/// 404 Not Found response
11341143
pub async fn dispatch_to_resource(&self, context: &mut WebmachineContext) {
11351144
let matching_paths = self.match_paths(&context.request);
1136-
let ordered_by_length: Vec<String> = matching_paths.iter()
1145+
let ordered_by_length = matching_paths.iter()
11371146
.cloned()
1138-
.sorted_by(|a, b| Ord::cmp(&b.len(), &a.len())).collect();
1147+
.sorted_by(|(a, _), (b, _)| Ord::cmp(&b.len(), &a.len()))
1148+
.collect_vec();
11391149
match ordered_by_length.first() {
1140-
Some(path) => {
1141-
update_paths_for_resource(&mut context.request, path);
1150+
Some((path, parts)) => {
1151+
update_paths_for_resource(&mut context.request, path, parts);
11421152
if let Some(resource) = self.lookup_resource(path) {
11431153
execute_state_machine(context, &resource).await;
11441154
finalise_response(context, &resource).await;

src/paths.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Utilities for matching URI paths
2+
3+
use itertools::EitherOrBoth::{Both, Left};
4+
use itertools::Itertools;
5+
6+
/// Maps a request path against a template path, populating any variables from the template.
7+
/// Returns None if the paths don't match.
8+
pub fn map_path(path: &str, path_template: &str) -> Option<Vec<(String, Option<String>)>> {
9+
if path.is_empty() || path_template.is_empty() {
10+
return None;
11+
}
12+
13+
let path_in = path.split('/').filter(|part| !part.is_empty()).collect_vec();
14+
let path_template = path_template.split('/').filter(|part| !part.is_empty()).collect_vec();
15+
if path_in.len() >= path_template.len() {
16+
let mut path_map = vec![];
17+
for item in path_in.iter().zip_longest(path_template) {
18+
if let Both(a, b) = item {
19+
if b.starts_with('{') && b.ends_with('}') {
20+
path_map.push((a.to_string(), Some(b[1..(b.len() - 1)].to_string())));
21+
} else if *a == b {
22+
path_map.push((a.to_string(), None));
23+
} else {
24+
return None
25+
}
26+
} else if let Left(a) = item {
27+
path_map.push((a.to_string(), None));
28+
} else {
29+
return None
30+
}
31+
}
32+
Some(path_map)
33+
} else {
34+
None
35+
}
36+
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use expectest::prelude::*;
41+
42+
use crate::paths::map_path;
43+
44+
#[test]
45+
fn map_path_simple_values() {
46+
expect!(map_path("", "")).to(be_none());
47+
expect!(map_path("/", "/")).to(be_equal_to(Some(vec![])));
48+
expect!(map_path("/a", "/a")).to(be_equal_to(Some(vec![("a".to_string(), None)])));
49+
expect!(map_path("/a", "/a")).to(be_equal_to(Some(vec![("a".to_string(), None)])));
50+
expect!(map_path("/a/", "/a")).to(be_equal_to(Some(vec![("a".to_string(), None)])));
51+
expect!(map_path("/a/b", "/a/b")).to(be_equal_to(Some(vec![("a".to_string(), None),
52+
("b".to_string(), None)])));
53+
expect!(map_path("/a/b/c", "/a/b/c")).to(be_equal_to(Some(vec![("a".to_string(), None),
54+
("b".to_string(), None), ("c".to_string(), None)])));
55+
56+
expect!(map_path("", "/")).to(be_none());
57+
expect!(map_path("/", "")).to(be_none());
58+
expect!(map_path("/", "/a")).to(be_none());
59+
expect!(map_path("/a", "/")).to(be_some().value(vec![("a".to_string(), None)]));
60+
expect!(map_path("/a/b", "/a")).to(be_some().value(vec![("a".to_string(), None), ("b".to_string(), None)]));
61+
expect!(map_path("/a/b", "/a/b/c")).to(be_none());
62+
}
63+
64+
#[test]
65+
fn map_path_with_variables() {
66+
expect!(map_path("/a", "/{id}")).to(be_equal_to(Some(vec![("a".to_string(), Some("id".to_string()))])));
67+
expect!(map_path("/a/", "/{id}")).to(be_equal_to(Some(vec![("a".to_string(), Some("id".to_string()))])));
68+
expect!(map_path("/a", "/{id}/")).to(be_equal_to(Some(vec![("a".to_string(), Some("id".to_string()))])));
69+
expect!(map_path("/a/b", "/a/{id}")).to(be_equal_to(Some(vec![("a".to_string(), None),
70+
("b".to_string(), Some("id".to_string()))])));
71+
expect!(map_path("/a/b", "/{id}/b")).to(be_equal_to(Some(vec![("a".to_string(), Some("id".to_string())),
72+
("b".to_string(), None)])));
73+
expect!(map_path("/a/b", "/{id}/{id}")).to(be_equal_to(Some(vec![("a".to_string(), Some("id".to_string())),
74+
("b".to_string(), Some("id".to_string()))])));
75+
expect!(map_path("/a/b/c", "/a/{b}/c")).to(be_equal_to(Some(vec![("a".to_string(), None),
76+
("b".to_string(), Some("b".to_string())), ("c".to_string(), None)])));
77+
78+
expect!(map_path("/", "/{id}")).to(be_none());
79+
expect!(map_path("/a/b", "/{id}")).to(be_some().value(vec![
80+
("a".to_string(), Some("id".to_string())),
81+
("b".to_string(), None)
82+
]));
83+
expect!(map_path("/a", "/{id}/b")).to(be_none());
84+
expect!(map_path("/a", "/{id}/{id}")).to(be_none());
85+
expect!(map_path("/a/b/c", "/{id}/{id}")).to(be_some().value(vec![
86+
("a".to_string(), Some("id".to_string())),
87+
("b".to_string(), Some("id".to_string())),
88+
("c".to_string(), None)
89+
]));
90+
}
91+
}

src/tests.rs

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ fn resource(path: &str) -> WebmachineRequest {
2020
WebmachineRequest {
2121
request_path: path.to_string(),
2222
base_path: "/".to_string(),
23+
path_vars: Default::default(),
2324
method: "GET".to_string(),
2425
headers: HashMap::new(),
2526
body: None,
@@ -34,16 +35,61 @@ fn path_matcher_test() {
3435
"/" => WebmachineResource::default(),
3536
"/path1" => WebmachineResource::default(),
3637
"/path2" => WebmachineResource::default(),
37-
"/path1/path3" => WebmachineResource::default()
38+
"/path1/path3" => WebmachineResource::default(),
39+
"/path2/{id}" => WebmachineResource::default(),
40+
"/path2/{id}/path3" => WebmachineResource::default()
3841
}
3942
};
40-
expect!(dispatcher.match_paths(&resource("/path1"))).to(be_equal_to(vec!["/", "/path1"]));
41-
expect!(dispatcher.match_paths(&resource("/path1/"))).to(be_equal_to(vec!["/", "/path1"]));
42-
expect!(dispatcher.match_paths(&resource("/path1/path3"))).to(be_equal_to(vec!["/", "/path1", "/path1/path3"]));
43-
expect!(dispatcher.match_paths(&resource("/path1/path3/path4"))).to(be_equal_to(vec!["/", "/path1", "/path1/path3"]));
44-
expect!(dispatcher.match_paths(&resource("/path1/other"))).to(be_equal_to(vec!["/", "/path1"]));
45-
expect!(dispatcher.match_paths(&resource("/path12"))).to(be_equal_to(vec!["/"]));
46-
expect!(dispatcher.match_paths(&resource("/"))).to(be_equal_to(vec!["/"]));
43+
44+
expect!(dispatcher.match_paths(&resource("/path1"))).to(be_equal_to(vec![
45+
("/".to_string(), vec![("path1".to_string(), None)]),
46+
("/path1".to_string(), vec![("path1".to_string(), None)]),
47+
]));
48+
expect!(dispatcher.match_paths(&resource("/path1/"))).to(be_equal_to(vec![
49+
("/".to_string(), vec![("path1".to_string(), None)]),
50+
("/path1".to_string(), vec![("path1".to_string(), None)])]
51+
));
52+
expect!(dispatcher.match_paths(&resource("/path1/path3"))).to(be_equal_to(vec![
53+
("/".to_string(), vec![("path1".to_string(), None), ("path3".to_string(), None)]),
54+
("/path1".to_string(), vec![("path1".to_string(), None), ("path3".to_string(), None)]),
55+
("/path1/path3".to_string(), vec![("path1".to_string(), None), ("path3".to_string(), None)])
56+
]));
57+
expect!(dispatcher.match_paths(&resource("/path1/path3/path4"))).to(be_equal_to(vec![
58+
("/".to_string(), vec![("path1".to_string(), None), ("path3".to_string(), None), ("path4".to_string(), None)]),
59+
("/path1".to_string(), vec![("path1".to_string(), None), ("path3".to_string(), None), ("path4".to_string(), None)]),
60+
("/path1/path3".to_string(), vec![("path1".to_string(), None), ("path3".to_string(), None), ("path4".to_string(), None)])
61+
]));
62+
expect!(dispatcher.match_paths(&resource("/path1/other"))).to(be_equal_to(vec![
63+
("/".to_string(), vec![("path1".to_string(), None), ("other".to_string(), None)]),
64+
("/path1".to_string(), vec![("path1".to_string(), None), ("other".to_string(), None)])
65+
]));
66+
expect!(dispatcher.match_paths(&resource("/path12"))).to(be_equal_to(vec![
67+
("/".to_string(), vec![("path12".to_string(), None)])
68+
]));
69+
expect!(dispatcher.match_paths(&resource("/"))).to(be_equal_to(vec![
70+
("/".to_string(), vec![])
71+
]));
72+
73+
expect!(dispatcher.match_paths(&resource("/path2"))).to(be_equal_to(vec![
74+
("/".to_string(), vec![("path2".to_string(), None)]),
75+
("/path2".to_string(), vec![("path2".to_string(), None)]),
76+
]));
77+
expect!(dispatcher.match_paths(&resource("/path2/1000"))).to(be_equal_to(vec![
78+
("/".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), None)]),
79+
("/path2".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), None)]),
80+
("/path2/{id}".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), Some("id".to_string()))])
81+
]));
82+
expect!(dispatcher.match_paths(&resource("/path2/1000/path3"))).to(be_equal_to(vec![
83+
("/".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), None), ("path3".to_string(), None)]),
84+
("/path2".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), None), ("path3".to_string(), None)]),
85+
("/path2/{id}".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), Some("id".to_string())), ("path3".to_string(), None)]),
86+
("/path2/{id}/path3".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), Some("id".to_string())), ("path3".to_string(), None)])
87+
]));
88+
expect!(dispatcher.match_paths(&resource("/path2/1000/other"))).to(be_equal_to(vec![
89+
("/".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), None), ("other".to_string(), None)]),
90+
("/path2".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), None), ("other".to_string(), None)]),
91+
("/path2/{id}".to_string(), vec![("path2".to_string(), None), ("1000".to_string(), Some("id".to_string())), ("other".to_string(), None)])
92+
]));
4793
}
4894

4995
#[test]
@@ -79,42 +125,54 @@ async fn execute_state_machine_returns_503_if_resource_indicates_not_available()
79125
#[test]
80126
fn update_paths_for_resource_test_with_root() {
81127
let mut request = WebmachineRequest::default();
82-
update_paths_for_resource(&mut request, "/");
83-
expect(request.request_path).to(be_equal_to("/".to_string()));
84-
expect(request.base_path).to(be_equal_to("/".to_string()));
128+
update_paths_for_resource(&mut request, "/", &vec![]);
129+
expect!(request.request_path).to(be_equal_to("/".to_string()));
130+
expect!(request.base_path).to(be_equal_to("/".to_string()));
85131
}
86132

87133
#[test]
88134
fn update_paths_for_resource_test_with_subpath() {
89135
let mut request = WebmachineRequest {
90136
request_path: "/subpath".to_string(),
91-
..WebmachineRequest::default()
137+
.. WebmachineRequest::default()
92138
};
93-
update_paths_for_resource(&mut request, "/");
94-
expect(request.request_path).to(be_equal_to("/subpath".to_string()));
95-
expect(request.base_path).to(be_equal_to("/".to_string()));
139+
update_paths_for_resource(&mut request, "/", &vec![]);
140+
expect!(request.request_path).to(be_equal_to("/subpath".to_string()));
141+
expect!(request.base_path).to(be_equal_to("/".to_string()));
96142
}
97143

98144
#[test]
99145
fn update_paths_for_resource_on_path() {
100146
let mut request = WebmachineRequest {
101147
request_path: "/path".to_string(),
102-
..WebmachineRequest::default()
148+
.. WebmachineRequest::default()
103149
};
104-
update_paths_for_resource(&mut request, "/path");
105-
expect(request.request_path).to(be_equal_to("/".to_string()));
106-
expect(request.base_path).to(be_equal_to("/path".to_string()));
150+
update_paths_for_resource(&mut request, "/path", &vec![]);
151+
expect!(request.request_path).to(be_equal_to("/".to_string()));
152+
expect!(request.base_path).to(be_equal_to("/path".to_string()));
107153
}
108154

109155
#[test]
110156
fn update_paths_for_resource_on_path_with_subpath() {
111157
let mut request = WebmachineRequest {
112158
request_path: "/path/path2".to_string(),
113-
..WebmachineRequest::default()
159+
.. WebmachineRequest::default()
160+
};
161+
update_paths_for_resource(&mut request, "/path", &vec![]);
162+
expect!(request.request_path).to(be_equal_to("/path2".to_string()));
163+
expect!(request.base_path).to(be_equal_to("/path".to_string()));
164+
}
165+
166+
#[test]
167+
fn update_paths_for_resource_on_path_with_mapped_parts() {
168+
let mut request = WebmachineRequest {
169+
request_path: "/path/1000".to_string(),
170+
.. WebmachineRequest::default()
114171
};
115-
update_paths_for_resource(&mut request, "/path");
116-
expect(request.request_path).to(be_equal_to("/path2".to_string()));
117-
expect(request.base_path).to(be_equal_to("/path".to_string()));
172+
update_paths_for_resource(&mut request, "/path", &vec![("1000".to_string(), Some("id".to_string()))]);
173+
expect!(request.request_path).to(be_equal_to("/1000".to_string()));
174+
expect!(request.base_path).to(be_equal_to("/path".to_string()));
175+
expect!(request.path_vars).to(be_equal_to(hashmap!{ "id".to_string() => "1000".to_string() }));
118176
}
119177

120178
#[tokio::test]

0 commit comments

Comments
 (0)