mirror of
https://git.proxmox.com/git/proxmox
synced 2025-08-16 13:35:33 +00:00
router: implement 'rest of the path' wildcard matching
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
60df564f73
commit
8036941977
@ -14,6 +14,10 @@ use super::ApiMethodInfo;
|
|||||||
/// current directory, a `Parameter` entry is used. Note that the parameter name is fixed at this
|
/// current directory, a `Parameter` entry is used. Note that the parameter name is fixed at this
|
||||||
/// point, so all method calls beneath will receive a parameter ot that particular name.
|
/// point, so all method calls beneath will receive a parameter ot that particular name.
|
||||||
pub enum SubRoute {
|
pub enum SubRoute {
|
||||||
|
/// Call this router for any further subdirectory paths, and provide the relative path via the
|
||||||
|
/// given parameter.
|
||||||
|
Wildcard(&'static str),
|
||||||
|
|
||||||
/// This is used for plain subdirectories.
|
/// This is used for plain subdirectories.
|
||||||
Directories(HashMap<&'static str, Router>),
|
Directories(HashMap<&'static str, Router>),
|
||||||
|
|
||||||
@ -59,14 +63,24 @@ impl Router {
|
|||||||
// The actual implementation taking the parameter as &str
|
// The actual implementation taking the parameter as &str
|
||||||
fn lookup_do(&self, path: &str) -> Option<(&Self, Option<Value>)> {
|
fn lookup_do(&self, path: &str) -> Option<(&Self, Option<Value>)> {
|
||||||
let mut matched_params = None;
|
let mut matched_params = None;
|
||||||
|
let mut matched_wildcard: Option<String> = None;
|
||||||
|
|
||||||
let mut this = self;
|
let mut this = self;
|
||||||
for component in path.split('/') {
|
for component in path.split('/') {
|
||||||
|
if let Some(ref mut relative_path) = matched_wildcard {
|
||||||
|
relative_path.push('/');
|
||||||
|
relative_path.push_str(component);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if component.is_empty() {
|
if component.is_empty() {
|
||||||
// `foo//bar` or the first `/` in `/foo`
|
// `foo//bar` or the first `/` in `/foo`
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this = match &this.subroute {
|
this = match &this.subroute {
|
||||||
|
Some(SubRoute::Wildcard(_)) => {
|
||||||
|
matched_wildcard = Some(component.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Some(SubRoute::Directories(subdirs)) => subdirs.get(component)?,
|
Some(SubRoute::Directories(subdirs)) => subdirs.get(component)?,
|
||||||
Some(SubRoute::Parameter(param_name, router)) => {
|
Some(SubRoute::Parameter(param_name, router)) => {
|
||||||
let previous = matched_params
|
let previous = matched_params
|
||||||
@ -81,6 +95,15 @@ impl Router {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(SubRoute::Wildcard(param_name)) = &this.subroute {
|
||||||
|
matched_params
|
||||||
|
.get_or_insert_with(serde_json::Map::new)
|
||||||
|
.insert(
|
||||||
|
param_name.to_string(),
|
||||||
|
Value::String(matched_wildcard.unwrap_or(String::new())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Some((this, matched_params.map(Value::Object)))
|
Some((this, matched_params.map(Value::Object)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +155,7 @@ impl Router {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder method to add a regular directory entro to this router.
|
/// Builder method to add a regular directory entry to this router.
|
||||||
///
|
///
|
||||||
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
|
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
|
||||||
/// already have a subdir entry!
|
/// already have a subdir entry!
|
||||||
@ -152,5 +175,18 @@ impl Router {
|
|||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Builder method to match the rest of the path into a parameter.
|
||||||
|
///
|
||||||
|
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
|
||||||
|
/// already have a subdir entry!
|
||||||
|
pub fn wildcard(mut self, path_parameter_name: &'static str) -> Self {
|
||||||
|
if self.subroute.is_some() {
|
||||||
|
panic!("'wildcard' and other sub routers are mutually exclusive");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.subroute = Some(SubRoute::Wildcard(path_parameter_name));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,43 +7,78 @@ use proxmox_api::Router;
|
|||||||
#[test]
|
#[test]
|
||||||
fn basic() {
|
fn basic() {
|
||||||
let info: &proxmox_api::ApiMethod = &methods::GET_PEOPLE;
|
let info: &proxmox_api::ApiMethod = &methods::GET_PEOPLE;
|
||||||
let router = Router::new().subdir(
|
let get_subpath: &proxmox_api::ApiMethod = &methods::GET_SUBPATH;
|
||||||
"people",
|
let router = Router::new()
|
||||||
Router::new().parameter_subdir("person", Router::new().get(info)),
|
.subdir(
|
||||||
|
"people",
|
||||||
|
Router::new().parameter_subdir("person", Router::new().get(info)),
|
||||||
|
)
|
||||||
|
.subdir(
|
||||||
|
"wildcard",
|
||||||
|
Router::new().wildcard("subpath").get(get_subpath),
|
||||||
|
);
|
||||||
|
|
||||||
|
check_with_matched_params(&router, "people/foo", "person", "foo", "foo");
|
||||||
|
check_with_matched_params(&router, "people//foo", "person", "foo", "foo");
|
||||||
|
check_with_matched_params(&router, "wildcard", "subpath", "", "");
|
||||||
|
check_with_matched_params(&router, "wildcard/", "subpath", "", "");
|
||||||
|
check_with_matched_params(&router, "wildcard//", "subpath", "", "");
|
||||||
|
check_with_matched_params(&router, "wildcard/dir1", "subpath", "dir1", "dir1");
|
||||||
|
check_with_matched_params(
|
||||||
|
&router,
|
||||||
|
"wildcard/dir1/dir2",
|
||||||
|
"subpath",
|
||||||
|
"dir1/dir2",
|
||||||
|
"dir1/dir2",
|
||||||
);
|
);
|
||||||
|
check_with_matched_params(&router, "wildcard/dir1//2", "subpath", "dir1//2", "dir1//2");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_with_matched_params(
|
||||||
|
router: &Router,
|
||||||
|
path: &str,
|
||||||
|
param_name: &str,
|
||||||
|
param_value: &str,
|
||||||
|
expected_body: &str,
|
||||||
|
) {
|
||||||
let (target, params) = router
|
let (target, params) = router
|
||||||
.lookup("people/foo")
|
.lookup(path)
|
||||||
.expect("must be able to lookup 'people/foo'");
|
.expect(&format!("must be able to lookup '{}'", path));
|
||||||
|
|
||||||
let params = params.expect("expected people/foo to create a parameter object");
|
let params = params.expect(&format!(
|
||||||
|
"expected parameters to be matched into '{}'",
|
||||||
|
param_name,
|
||||||
|
));
|
||||||
|
|
||||||
let apifn = target
|
let apifn = target
|
||||||
.get
|
.get
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("expected GET method on people/foo")
|
.expect(&format!("expected GET method on {}", path))
|
||||||
.handler();
|
.handler();
|
||||||
|
|
||||||
let person = params["person"]
|
let arg = params[param_name].as_str().expect(&format!(
|
||||||
.as_str()
|
"expected lookup() to fill the '{}' parameter",
|
||||||
.expect("expected lookup() to fill the 'person' parameter");
|
param_name
|
||||||
|
));
|
||||||
|
|
||||||
assert!(
|
assert_eq!(
|
||||||
person == "foo",
|
arg, param_value,
|
||||||
"lookup of 'people/foo' should set 'person' to 'foo'"
|
"lookup of '{}' should set '{}' to '{}'",
|
||||||
|
path, param_name, param_value,
|
||||||
);
|
);
|
||||||
|
|
||||||
let response = futures::executor::block_on(Pin::from(apifn(params)))
|
let response = futures::executor::block_on(Pin::from(apifn(params)))
|
||||||
.expect("expected the simple test api function to be ready immediately");
|
.expect("expected the simple test api function to be ready immediately");
|
||||||
|
|
||||||
assert!(response.status() == 200, "response status must be 200");
|
assert_eq!(response.status(), 200, "response status must be 200");
|
||||||
|
|
||||||
let body =
|
let body =
|
||||||
std::str::from_utf8(response.body().as_ref()).expect("expected a valid utf8 repsonse body");
|
std::str::from_utf8(response.body().as_ref()).expect("expected a valid utf8 repsonse body");
|
||||||
|
|
||||||
assert!(
|
assert_eq!(
|
||||||
body == "foo",
|
body, expected_body,
|
||||||
"repsonse of people/foo should simply be 'foo'"
|
"response of {} should be '{}', got '{}'",
|
||||||
|
path, expected_body, body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +101,13 @@ mod methods {
|
|||||||
.body(value["person"].as_str().unwrap().into())?)
|
.body(value["person"].as_str().unwrap().into())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_subpath(value: Value) -> ApiOutput {
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(value["subpath"].as_str().unwrap().into())?)
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref GET_PEOPLE_PARAMS: Vec<Parameter> = {
|
static ref GET_PEOPLE_PARAMS: Vec<Parameter> = {
|
||||||
vec![Parameter {
|
vec![Parameter {
|
||||||
@ -84,6 +126,23 @@ mod methods {
|
|||||||
handler: |value: Value| -> ApiFuture { Box::pin(get_people(value)) },
|
handler: |value: Value| -> ApiFuture { Box::pin(get_people(value)) },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
static ref GET_SUBPATH_PARAMS: Vec<Parameter> = {
|
||||||
|
vec![Parameter {
|
||||||
|
name: "subpath",
|
||||||
|
description: "the matched relative subdir path",
|
||||||
|
type_info: get_type_info::<String>(),
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
pub static ref GET_SUBPATH: ApiMethod = {
|
||||||
|
ApiMethod {
|
||||||
|
description: "get the 'subpath' parameter returned back",
|
||||||
|
parameters: &GET_SUBPATH_PARAMS,
|
||||||
|
return_type: get_type_info::<String>(),
|
||||||
|
protected: false,
|
||||||
|
reload_timezone: false,
|
||||||
|
handler: |value: Value| -> ApiFuture { Box::pin(get_subpath(value)) },
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
|
Loading…
Reference in New Issue
Block a user