]> git.proxmox.com Git - proxmox.git/commitdiff
router: implement 'rest of the path' wildcard matching
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Sat, 8 Jun 2019 11:35:05 +0000 (13:35 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Wed, 12 Jun 2019 07:48:30 +0000 (09:48 +0200)
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
proxmox-api/src/router.rs
proxmox-api/tests/router.rs

index c409dcdd2275459e6f9607bd368fb8f777bead3c..4f4520eb28ca4179fdf50b6af5f0850cc6ca598d 100644 (file)
@@ -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<Value>)> {
         let mut matched_params = None;
+        let mut matched_wildcard: Option<String> = 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
+    }
+}
index 63ec88c2a13bacbf7ad344f940f578c4219c4071..c870bcf2c8bff1f3cd9b816a777656850101dc76 100644 (file)
@@ -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<Parameter> = {
             vec![Parameter {
@@ -84,6 +126,23 @@ mod methods {
                 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)]