diff --git a/proxmox-api/src/router.rs b/proxmox-api/src/router.rs index c409dcdd..4f4520eb 100644 --- a/proxmox-api/src/router.rs +++ b/proxmox-api/src/router.rs @@ -14,6 +14,10 @@ use super::ApiMethodInfo; /// 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. 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. Directories(HashMap<&'static str, Router>), @@ -59,14 +63,24 @@ impl Router { // The actual implementation taking the parameter as &str fn lookup_do(&self, path: &str) -> Option<(&Self, Option)> { let mut matched_params = None; + let mut matched_wildcard: Option = None; let mut this = self; 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() { // `foo//bar` or the first `/` in `/foo` continue; } this = match &this.subroute { + Some(SubRoute::Wildcard(_)) => { + matched_wildcard = Some(component.to_string()); + continue; + } Some(SubRoute::Directories(subdirs)) => subdirs.get(component)?, Some(SubRoute::Parameter(param_name, router)) => { 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))) } @@ -132,7 +155,7 @@ impl Router { 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 /// already have a subdir entry! @@ -152,5 +175,18 @@ impl Router { } 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 + } +} diff --git a/proxmox-api/tests/router.rs b/proxmox-api/tests/router.rs index 63ec88c2..c870bcf2 100644 --- a/proxmox-api/tests/router.rs +++ b/proxmox-api/tests/router.rs @@ -7,43 +7,78 @@ use proxmox_api::Router; #[test] fn basic() { let info: &proxmox_api::ApiMethod = &methods::GET_PEOPLE; - let router = Router::new().subdir( - "people", - Router::new().parameter_subdir("person", Router::new().get(info)), + let get_subpath: &proxmox_api::ApiMethod = &methods::GET_SUBPATH; + let router = Router::new() + .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 - .lookup("people/foo") - .expect("must be able to lookup 'people/foo'"); + .lookup(path) + .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 .get .as_ref() - .expect("expected GET method on people/foo") + .expect(&format!("expected GET method on {}", path)) .handler(); - let person = params["person"] - .as_str() - .expect("expected lookup() to fill the 'person' parameter"); + let arg = params[param_name].as_str().expect(&format!( + "expected lookup() to fill the '{}' parameter", + param_name + )); - assert!( - person == "foo", - "lookup of 'people/foo' should set 'person' to 'foo'" + assert_eq!( + arg, param_value, + "lookup of '{}' should set '{}' to '{}'", + path, param_name, param_value, ); let response = futures::executor::block_on(Pin::from(apifn(params))) .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 = std::str::from_utf8(response.body().as_ref()).expect("expected a valid utf8 repsonse body"); - assert!( - body == "foo", - "repsonse of people/foo should simply be 'foo'" + assert_eq!( + body, expected_body, + "response of {} should be '{}', got '{}'", + path, expected_body, body, ); } @@ -66,6 +101,13 @@ mod methods { .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! { static ref GET_PEOPLE_PARAMS: Vec = { vec![Parameter { @@ -84,6 +126,23 @@ mod methods { handler: |value: Value| -> ApiFuture { Box::pin(get_people(value)) }, } }; + static ref GET_SUBPATH_PARAMS: Vec = { + vec![Parameter { + name: "subpath", + description: "the matched relative subdir path", + type_info: get_type_info::(), + }] + }; + pub static ref GET_SUBPATH: ApiMethod = { + ApiMethod { + description: "get the 'subpath' parameter returned back", + parameters: &GET_SUBPATH_PARAMS, + return_type: get_type_info::(), + protected: false, + reload_timezone: false, + handler: |value: Value| -> ApiFuture { Box::pin(get_subpath(value)) }, + } + }; } #[derive(Deserialize, Serialize)]