router: implement 'rest of the path' wildcard matching

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-06-08 13:35:05 +02:00
parent 60df564f73
commit 8036941977
2 changed files with 114 additions and 19 deletions

View File

@ -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
}
}

View File

@ -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)]