all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [RFC PATCH 1/3] pdm-client: add query builder
@ 2025-02-14 14:11 Maximiliano Sandoval
  2025-02-14 14:11 ` [pdm-devel] [RFC PATCH 2/3] pdm-client: make use of " Maximiliano Sandoval
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Maximiliano Sandoval @ 2025-02-14 14:11 UTC (permalink / raw)
  To: pdm-devel

A big proportion of the consumers of this API use the `format!` macro to
define the base of the url, hence why we use a `Cow<'_, str>` on the
constructor.

A `perl-api-path-builder` feature was added to satisfy the needs of
pve-api-types.

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
---

This is marked as RFC as I think this is better suited for proxmox-client and
here it is easier to showcase in the second and third commit how the API falls
into place.

 lib/pdm-client/Cargo.toml              |   1 +
 lib/pdm-client/src/api_path_builder.rs | 207 +++++++++++++++++++++++++
 2 files changed, 208 insertions(+)
 create mode 100644 lib/pdm-client/src/api_path_builder.rs

diff --git a/lib/pdm-client/Cargo.toml b/lib/pdm-client/Cargo.toml
index fe5ddf7..af42c43 100644
--- a/lib/pdm-client/Cargo.toml
+++ b/lib/pdm-client/Cargo.toml
@@ -26,3 +26,4 @@ pbs-api-types.workspace = true
 [features]
 default = []
 hyper-client = [ "proxmox-client/hyper-client" ]
+perl-api-path-builder = []
diff --git a/lib/pdm-client/src/api_path_builder.rs b/lib/pdm-client/src/api_path_builder.rs
new file mode 100644
index 0000000..0058489
--- /dev/null
+++ b/lib/pdm-client/src/api_path_builder.rs
@@ -0,0 +1,207 @@
+use std::borrow::Cow;
+
+/// Builder for urls with a query.
+///
+/// The [Self::arg] method can be used to add multiple arguments to the query.
+///
+/// ```rust
+/// use pdm_client::api_path_builder::ApiPathBuilder;
+///
+/// let query = ApiPathBuilder::new("/api2/extjs/cluster/resources")
+///     .arg("type", pve_api_types::ClusterResourceKind::Vm)
+///     .build();
+///
+/// assert_eq!(&query, "/api2/extjs/cluster/resources?type=vm");
+/// ```
+///
+/// ## Compatibility with perl clients
+///
+/// The methods `bool_arg` and `list_arg` were added to translate booleans as
+/// `"0"`/`"1"` and split lists so that they can be feed to perl's
+/// `split_list()` respectively. These methods require the
+/// `perl-api-path-builder` feature.
+pub struct ApiPathBuilder {
+    url: String,
+    separator: char,
+}
+
+impl ApiPathBuilder {
+    /// Creates a new builder.
+    pub fn new<'a>(base: impl Into<Cow<'a, str>>) -> Self {
+        Self {
+            url: base.into().into_owned(),
+            separator: '?',
+        }
+    }
+
+    /// Adds an argument to the query.
+    ///
+    /// The value is percent-encoded.
+    pub fn arg<T: std::fmt::Display>(mut self, name: &str, value: T) -> Self {
+        self.push_name_and_separator(name);
+        self.push_encoded(value);
+        self
+    }
+
+    /// Adds an optional argument to the query.
+    ///
+    /// Does nothing if the value is `None`. See [Self::arg].
+    pub fn maybe_arg<T: std::fmt::Display>(mut self, name: &str, value: &Option<T>) -> Self {
+        if let Some(value) = value {
+            self = self.arg(name, value);
+        }
+        self
+    }
+
+    /// Builds the url.
+    pub fn build(self) -> String {
+        self.url
+    }
+
+    fn push_name_and_separator(&mut self, name: &str) {
+        self.url.push(self.separator);
+        self.separator = '&';
+        self.url.push_str(name);
+        self.url.push('=');
+    }
+
+    fn push_encoded<T: std::fmt::Display>(&mut self, value: T) {
+        let str_value = value.to_string();
+        let enc_value = percent_encoding::percent_encode(
+            str_value.as_bytes(),
+            percent_encoding::NON_ALPHANUMERIC,
+        );
+        self.url.extend(enc_value);
+    }
+}
+
+#[cfg(feature = "perl-api-path-builder")]
+impl ApiPathBuilder {
+    /// Adds a boolean arg in a perl-friendly fashion.
+    ///
+    /// `true` will be converted into `"1"` and `false` to `"0"`.
+    pub fn bool_arg(mut self, name: &str, value: bool) -> Self {
+        self.push_name_and_separator(name);
+        if value {
+            self.url.push('1');
+        } else {
+            self.url.push('0');
+        };
+        self
+    }
+
+    /// Adds an optional boolean arg in a perl-friendly fashion.
+    ///
+    /// Does nothing if `value` is `None`. See [Self::bool_arg].
+    pub fn maybe_bool_arg(mut self, name: &str, value: Option<bool>) -> Self {
+        if let Some(value) = value {
+            self = self.bool_arg(name, value);
+        }
+        self
+    }
+
+    /// Helper for building perl-friendly queries.
+    ///
+    /// For `<type>-list` entries we turn an array into a string ready for
+    /// perl's `split_list()` call.
+    ///
+    /// The values are percent-encoded.
+    pub fn list_arg<T: std::fmt::Display, P: Iterator<Item = T>>(
+        mut self,
+        name: &str,
+        values: P,
+    ) -> Self {
+        self.push_name_and_separator(name);
+        let mut list_separator = "";
+        for entry in values {
+            self.url.push_str(list_separator);
+            list_separator = "%00";
+            self.push_encoded(entry);
+        }
+        self
+    }
+
+    /// Helper for building perl-friendly queries.
+    ///
+    /// See [Self::list_arg].
+    pub fn maybe_list_arg<T: std::fmt::Display>(
+        mut self,
+        name: &str,
+        values: &Option<Vec<T>>,
+    ) -> Self {
+        if let Some(values) = values {
+            self = self.list_arg(name, values.iter());
+        };
+        self
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use pdm_api_types::ConfigurationState;
+    use pve_api_types::ClusterResourceKind;
+
+    use super::*;
+
+    #[test]
+    fn test_builder() {
+        let expected = "/api2/extjs/cluster/resources?type=vm";
+        let ty = ClusterResourceKind::Vm;
+
+        let query = ApiPathBuilder::new("/api2/extjs/cluster/resources")
+            .arg("type", ty)
+            .build();
+
+        assert_eq!(&query, expected);
+
+        let second_expected =
+            "/api2/extjs/pve/remotes/some-remote/qemu/100/config?state=active&node=myNode";
+        let state = ConfigurationState::Active;
+        let node = "myNode";
+        let snapshot = None::<&str>;
+
+        let second_query =
+            ApiPathBuilder::new("/api2/extjs/pve/remotes/some-remote/qemu/100/config")
+                .arg("state", state)
+                .arg("node", node)
+                .maybe_arg("snapshot", &snapshot)
+                .build();
+
+        assert_eq!(&second_query, &second_expected);
+    }
+}
+
+#[cfg(all(test, feature = "perl-api-path-builder"))]
+mod perl_tests {
+    use super::*;
+
+    use pve_api_types::client::{add_query_arg, add_query_bool};
+
+    #[test]
+    fn test_perl_builder() {
+        let history = true;
+        let local_only = false;
+        let start_time = 1000;
+
+        let (mut query, mut sep) = (String::new(), '?');
+        add_query_bool(&mut query, &mut sep, "history", Some(history));
+        add_query_bool(&mut query, &mut sep, "local-only", Some(local_only));
+        add_query_arg(&mut query, &mut sep, "start-time", &Some(start_time));
+        let url = format!("/api2/extjs/cluster/metrics/export{query}");
+
+        let query = ApiPathBuilder::new("/api2/extjs/cluster/metrics/export")
+            .bool_arg("history", history)
+            .bool_arg("local-only", local_only)
+            .arg("start-time", start_time)
+            .build();
+        assert_eq!(url, query);
+
+        let maybe_arg = ApiPathBuilder::new("/api2/extjs/cluster/metrics/export")
+            .maybe_bool_arg("history", Some(history))
+            .maybe_bool_arg("local-only", Some(local_only))
+            .maybe_arg("start-time", &Some(start_time))
+            .build();
+
+        assert_eq!(url, maybe_arg);
+    }
+}
-- 
2.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pdm-devel] [RFC PATCH 2/3] pdm-client: make use of query builder
  2025-02-14 14:11 [pdm-devel] [RFC PATCH 1/3] pdm-client: add query builder Maximiliano Sandoval
@ 2025-02-14 14:11 ` Maximiliano Sandoval
  2025-02-14 14:11 ` [pdm-devel] [RFC PATCH 3/3] pbs-client: " Maximiliano Sandoval
  2025-03-21 12:59 ` [pdm-devel] [RFC PATCH 1/3] pdm-client: add " Wolfgang Bumiller
  2 siblings, 0 replies; 4+ messages in thread
From: Maximiliano Sandoval @ 2025-02-14 14:11 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
---
 lib/pdm-client/src/lib.rs | 165 ++++++++++++++++++++------------------
 1 file changed, 87 insertions(+), 78 deletions(-)

diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index a41b82c..51cbc04 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -1,5 +1,9 @@
 //! Proxmox Datacenter Manager API client.
 
+pub mod api_path_builder;
+
+use api_path_builder::ApiPathBuilder;
+
 use std::collections::HashMap;
 use std::time::Duration;
 
@@ -134,13 +138,9 @@ impl<T: HttpApiClient> PdmClient<T> {
     }
 
     pub async fn list_users(&self, include_api_tokens: bool) -> Result<Vec<UserWithTokens>, Error> {
-        let mut path = "/api2/extjs/access/users".to_string();
-        add_query_arg(
-            &mut path,
-            &mut '?',
-            "include_tokens",
-            &Some(include_api_tokens),
-        );
+        let path = ApiPathBuilder::new("/api2/extjs/access/users")
+            .arg("include_tokens", include_api_tokens)
+            .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -221,8 +221,9 @@ impl<T: HttpApiClient> PdmClient<T> {
         password: Option<&str>,
         id: &str,
     ) -> Result<(), proxmox_client::Error> {
-        let mut path = format!("/api2/extjs/access/tfa/{userid}/{id}");
-        add_query_arg(&mut path, &mut '?', "password", &password);
+        let path = ApiPathBuilder::new(format!("/api2/extjs/access/tfa/{userid}/{id}"))
+            .maybe_arg("password", &password)
+            .build();
         self.0.delete(&path).await?.nodata()
     }
 
@@ -346,8 +347,9 @@ impl<T: HttpApiClient> PdmClient<T> {
         remote: &str,
         kind: Option<pve_api_types::ClusterResourceKind>,
     ) -> Result<Vec<PveResource>, Error> {
-        let mut query = format!("/api2/extjs/pve/remotes/{remote}/resources");
-        add_query_arg(&mut query, &mut '?', "kind", &kind);
+        let query = ApiPathBuilder::new(format!("/api2/extjs/pve/remotes/{remote}/resources"))
+            .maybe_arg("kind", &kind)
+            .build();
         Ok(self.0.get(&query).await?.expect_json()?.data)
     }
 
@@ -356,8 +358,9 @@ impl<T: HttpApiClient> PdmClient<T> {
         remote: &str,
         target_endpoint: Option<&str>,
     ) -> Result<Vec<ClusterNodeStatus>, Error> {
-        let mut query = format!("/api2/extjs/pve/remotes/{remote}/cluster-status");
-        add_query_arg(&mut query, &mut '?', "target-endpoint", &target_endpoint);
+        let query = ApiPathBuilder::new(format!("/api2/extjs/pve/remotes/{remote}/cluster-status"))
+            .maybe_arg("target-endpoint", &target_endpoint)
+            .build();
         Ok(self.0.get(&query).await?.expect_json()?.data)
     }
 
@@ -366,8 +369,9 @@ impl<T: HttpApiClient> PdmClient<T> {
         remote: &str,
         node: Option<&str>,
     ) -> Result<Vec<pve_api_types::VmEntry>, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/qemu");
-        add_query_arg(&mut path, &mut '?', "node", &node);
+        let path = ApiPathBuilder::new(format!("/api2/extjs/pve/remotes/{remote}/qemu"))
+            .maybe_arg("node", &node)
+            .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -376,8 +380,9 @@ impl<T: HttpApiClient> PdmClient<T> {
         remote: &str,
         node: Option<&str>,
     ) -> Result<Vec<pve_api_types::VmEntry>, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/lxc");
-        add_query_arg(&mut path, &mut '?', "node", &node);
+        let path = ApiPathBuilder::new(format!("/api2/extjs/pve/remotes/{remote}/lxc"))
+            .maybe_arg("node", &node)
+            .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -389,11 +394,13 @@ impl<T: HttpApiClient> PdmClient<T> {
         state: ConfigurationState,
         snapshot: Option<&str>,
     ) -> Result<pve_api_types::QemuConfig, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/qemu/{vmid}/config");
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "state", &Some(&state));
-        add_query_arg(&mut path, &mut sep, "node", &node);
-        add_query_arg(&mut path, &mut sep, "snapshot", &snapshot);
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/qemu/{vmid}/config"
+        ))
+        .arg("state", state)
+        .maybe_arg("node", &node)
+        .maybe_arg("snapshot", &snapshot)
+        .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -403,9 +410,11 @@ impl<T: HttpApiClient> PdmClient<T> {
         node: Option<&str>,
         vmid: u32,
     ) -> Result<pve_api_types::QemuStatus, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/qemu/{vmid}/status");
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "node", &node);
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/qemu/{vmid}/status"
+        ))
+        .maybe_arg("node", &node)
+        .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -415,9 +424,11 @@ impl<T: HttpApiClient> PdmClient<T> {
         node: Option<&str>,
         vmid: u32,
     ) -> Result<pve_api_types::LxcStatus, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/lxc/{vmid}/status");
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "node", &node);
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/lxc/{vmid}/status"
+        ))
+        .maybe_arg("node", &node)
+        .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -526,11 +537,13 @@ impl<T: HttpApiClient> PdmClient<T> {
         state: ConfigurationState,
         snapshot: Option<&str>,
     ) -> Result<pve_api_types::LxcConfig, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/lxc/{vmid}/config");
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "node", &node);
-        add_query_arg(&mut path, &mut sep, "state", &Some(&state));
-        add_query_arg(&mut path, &mut sep, "snapshot", &snapshot);
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/lxc/{vmid}/config"
+        ))
+        .maybe_arg("node", &node)
+        .arg("state", state)
+        .maybe_arg("snapshot", &snapshot)
+        .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -620,9 +633,9 @@ impl<T: HttpApiClient> PdmClient<T> {
         remote: &str,
         node: Option<&str>,
     ) -> Result<Vec<pve_api_types::ListTasksResponse>, Error> {
-        let mut query = format!("/api2/extjs/pve/remotes/{remote}/tasks");
-        let mut sep = '?';
-        pve_api_types::client::add_query_arg(&mut query, &mut sep, "node", &node);
+        let query = ApiPathBuilder::new(format!("/api2/extjs/pve/remotes/{remote}/tasks"))
+            .maybe_arg("node", &node)
+            .build();
         Ok(self.0.get(&query).await?.expect_json()?.data)
     }
 
@@ -657,9 +670,9 @@ impl<T: HttpApiClient> PdmClient<T> {
         path: Option<&str>,
         exact: bool,
     ) -> Result<(Vec<AclListItem>, Option<ConfigDigest>), Error> {
-        let mut query = format!("/api2/extjs/access/acl?exact={}", exact as u8);
-        let mut sep = '?';
-        pve_api_types::client::add_query_arg(&mut query, &mut sep, "path", &path);
+        let query = ApiPathBuilder::new(format!("/api2/extjs/access/acl?exact={}", exact as u8))
+            .maybe_arg("path", &path)
+            .build();
         let mut res = self.0.get(&query).await?.expect_json()?;
         Ok((res.data, res.attribs.remove("digest").map(ConfigDigest)))
     }
@@ -747,8 +760,11 @@ impl<T: HttpApiClient> PdmClient<T> {
         store: &str,
         namespace: Option<&str>,
     ) -> Result<Vec<pbs_api_types::SnapshotListItem>, Error> {
-        let mut path = format!("/api2/extjs/pbs/remotes/{remote}/datastore/{store}/snapshots");
-        add_query_arg(&mut path, &mut '?', "ns", &namespace);
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pbs/remotes/{remote}/datastore/{store}/snapshots"
+        ))
+        .maybe_arg("ns", &namespace)
+        .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -770,16 +786,19 @@ impl<T: HttpApiClient> PdmClient<T> {
         mode: RrdMode,
         timeframe: RrdTimeframe,
     ) -> Result<Vec<PbsDatastoreDataPoint>, Error> {
-        let mut path = format!("/api2/extjs/pbs/remotes/{remote}/datastore/{store}/rrddata");
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "cf", &Some(mode));
-        add_query_arg(&mut path, &mut sep, "timeframe", &Some(timeframe));
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pbs/remotes/{remote}/datastore/{store}/rrddata"
+        ))
+        .arg("cf", mode)
+        .arg("timeframe", timeframe)
+        .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
     pub async fn resources(&self, max_age: Option<u64>) -> Result<Vec<RemoteResources>, Error> {
-        let mut path = "/api2/extjs/resources/list".to_string();
-        add_query_arg(&mut path, &mut '?', "max-age", &max_age);
+        let path = ApiPathBuilder::new("/api2/extjs/resources/list")
+            .maybe_arg("max-age", &max_age)
+            .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -789,9 +808,11 @@ impl<T: HttpApiClient> PdmClient<T> {
         node: &str,
         interface_type: Option<ListNetworksType>,
     ) -> Result<Vec<NetworkInterface>, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/nodes/{node}/network");
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "interface-type", &interface_type);
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/nodes/{node}/network"
+        ))
+        .maybe_arg("interface-type", &interface_type)
+        .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -805,17 +826,20 @@ impl<T: HttpApiClient> PdmClient<T> {
         storage: Option<String>,
         target: Option<String>,
     ) -> Result<Vec<StorageInfo>, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/nodes/{node}/storage");
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "enabled", &enabled);
-        add_query_arg(&mut path, &mut sep, "format", &format);
-        add_query_arg(&mut path, &mut sep, "storage", &storage);
-        add_query_arg(&mut path, &mut sep, "target", &target);
+        let mut builder = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/nodes/{node}/storage"
+        ))
+        .maybe_arg("enabled", &enabled)
+        .maybe_arg("format", &format)
+        .maybe_arg("storage", &storage)
+        .maybe_arg("target", &target);
         if let Some(content) = content {
             for ty in content {
-                add_query_arg(&mut path, &mut sep, "content", &Some(ty));
+                builder = builder.arg("content", ty);
             }
         }
+        let path = builder.build();
+
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
@@ -836,10 +860,12 @@ impl<T: HttpApiClient> PdmClient<T> {
         vmid: u32,
         target: Option<String>,
     ) -> Result<QemuMigratePreconditions, Error> {
-        let mut path = format!("/api2/extjs/pve/remotes/{remote}/qemu/{vmid}/migrate");
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "node", &node);
-        add_query_arg(&mut path, &mut sep, "target", &target);
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/qemu/{vmid}/migrate"
+        ))
+        .maybe_arg("node", &node)
+        .maybe_arg("target", &target)
+        .build();
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 }
@@ -1150,23 +1176,6 @@ impl AddTfaEntry {
     }
 }
 
-/// Add an optional string parameter to the query, and if it was added, change `separator` to `&`.
-pub fn add_query_arg<T>(query: &mut String, separator: &mut char, name: &str, value: &Option<T>)
-where
-    T: std::fmt::Display,
-{
-    if let Some(value) = value {
-        query.push(*separator);
-        *separator = '&';
-        query.push_str(name);
-        query.push('=');
-        query.extend(percent_encoding::percent_encode(
-            value.to_string().as_bytes(),
-            percent_encoding::NON_ALPHANUMERIC,
-        ));
-    }
-}
-
 /// ACL entries are either for a user or for a group.
 #[derive(Clone, Serialize)]
 pub enum AclRecipient<'a> {
-- 
2.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pdm-devel] [RFC PATCH 3/3] pbs-client: make use of query builder
  2025-02-14 14:11 [pdm-devel] [RFC PATCH 1/3] pdm-client: add query builder Maximiliano Sandoval
  2025-02-14 14:11 ` [pdm-devel] [RFC PATCH 2/3] pdm-client: make use of " Maximiliano Sandoval
@ 2025-02-14 14:11 ` Maximiliano Sandoval
  2025-03-21 12:59 ` [pdm-devel] [RFC PATCH 1/3] pdm-client: add " Wolfgang Bumiller
  2 siblings, 0 replies; 4+ messages in thread
From: Maximiliano Sandoval @ 2025-02-14 14:11 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
---
 server/Cargo.toml        |  1 +
 server/src/pbs_client.rs | 31 ++++++++-----------------------
 2 files changed, 9 insertions(+), 23 deletions(-)

diff --git a/server/Cargo.toml b/server/Cargo.toml
index 7b0058e..0c30f93 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -71,6 +71,7 @@ proxmox-acme-api = { workspace = true, features = [ "impl" ] }
 pdm-api-types.workspace = true
 pdm-buildcfg.workspace = true
 pdm-config.workspace = true
+pdm-client.workspace = true
 
 pve-api-types = { workspace = true, features = [ "client" ] }
 pbs-api-types.workspace = true
diff --git a/server/src/pbs_client.rs b/server/src/pbs_client.rs
index c4115c1..ad9ef95 100644
--- a/server/src/pbs_client.rs
+++ b/server/src/pbs_client.rs
@@ -13,6 +13,7 @@ use proxmox_schema::api;
 use proxmox_section_config::typed::SectionConfigData;
 
 use pdm_api_types::remotes::{Remote, RemoteType};
+use pdm_client::api_path_builder::ApiPathBuilder;
 
 pub fn get_remote<'a>(
     config: &'a SectionConfigData<Remote>,
@@ -100,8 +101,9 @@ impl PbsClient {
         datastore: &str,
         namespace: Option<&str>,
     ) -> Result<JsonRecords<pbs_api_types::SnapshotListItem>, anyhow::Error> {
-        let mut path = format!("/api2/extjs/admin/datastore/{datastore}/snapshots");
-        add_query_arg(&mut path, &mut '?', "ns", &namespace);
+        let path = ApiPathBuilder::new(format!("/api2/extjs/admin/datastore/{datastore}/snapshots"))
+            .maybe_arg("ns", &namespace)
+            .build();
         let response = self
             .0
             .streaming_request(http::Method::GET, &path, None::<()>)
@@ -169,10 +171,10 @@ impl PbsClient {
         history: Option<bool>,
         start_time: Option<i64>,
     ) -> Result<pbs_api_types::Metrics, Error> {
-        let mut path = "/api2/extjs/status/metrics".to_string();
-        let mut sep = '?';
-        add_query_arg(&mut path, &mut sep, "history", &history);
-        add_query_arg(&mut path, &mut sep, "start-time", &start_time);
+        let path = ApiPathBuilder::new("/api2/extjs/status/metrics")
+            .maybe_arg("history", &history)
+            .maybe_arg("start-time", &start_time)
+            .build();
 
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
@@ -192,20 +194,3 @@ impl PbsClient {
 struct JsonData<T> {
     data: T,
 }
-
-/// Add an optional string parameter to the query, and if it was added, change `separator` to `&`.
-fn add_query_arg<T>(query: &mut String, separator: &mut char, name: &str, value: &Option<T>)
-where
-    T: std::fmt::Display,
-{
-    if let Some(value) = value {
-        query.push(*separator);
-        *separator = '&';
-        query.push_str(name);
-        query.push('=');
-        query.extend(percent_encoding::percent_encode(
-            value.to_string().as_bytes(),
-            percent_encoding::NON_ALPHANUMERIC,
-        ));
-    }
-}
-- 
2.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [pdm-devel] [RFC PATCH 1/3] pdm-client: add query builder
  2025-02-14 14:11 [pdm-devel] [RFC PATCH 1/3] pdm-client: add query builder Maximiliano Sandoval
  2025-02-14 14:11 ` [pdm-devel] [RFC PATCH 2/3] pdm-client: make use of " Maximiliano Sandoval
  2025-02-14 14:11 ` [pdm-devel] [RFC PATCH 3/3] pbs-client: " Maximiliano Sandoval
@ 2025-03-21 12:59 ` Wolfgang Bumiller
  2 siblings, 0 replies; 4+ messages in thread
From: Wolfgang Bumiller @ 2025-03-21 12:59 UTC (permalink / raw)
  To: Maximiliano Sandoval; +Cc: pdm-devel

On Fri, Feb 14, 2025 at 03:11:25PM +0100, Maximiliano Sandoval wrote:
> A big proportion of the consumers of this API use the `format!` macro to
> define the base of the url, hence why we use a `Cow<'_, str>` on the
> constructor.
> 
> A `perl-api-path-builder` feature was added to satisfy the needs of
> pve-api-types.
> 
> Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
> ---
> 
> This is marked as RFC as I think this is better suited for proxmox-client and
> here it is easier to showcase in the second and third commit how the API falls
> into place.

Given that `add_query_arg` (and some variations) currently exist as
duplicates in pdm-client, pve-api-types and pdm/server, all of which
base their http part on proxmox-client - moving its functionality one
way or another into proxmox-client makes sense.

The builder seems fine to me, if you want to prepare patches for
proxmox-client with it.

Two minor nits below.

> 
>  lib/pdm-client/Cargo.toml              |   1 +
>  lib/pdm-client/src/api_path_builder.rs | 207 +++++++++++++++++++++++++
>  2 files changed, 208 insertions(+)
>  create mode 100644 lib/pdm-client/src/api_path_builder.rs
> 
> diff --git a/lib/pdm-client/Cargo.toml b/lib/pdm-client/Cargo.toml
> index fe5ddf7..af42c43 100644
> --- a/lib/pdm-client/Cargo.toml
> +++ b/lib/pdm-client/Cargo.toml
> @@ -26,3 +26,4 @@ pbs-api-types.workspace = true
>  [features]
>  default = []
>  hyper-client = [ "proxmox-client/hyper-client" ]
> +perl-api-path-builder = []
> diff --git a/lib/pdm-client/src/api_path_builder.rs b/lib/pdm-client/src/api_path_builder.rs
> new file mode 100644
> index 0000000..0058489
> --- /dev/null
> +++ b/lib/pdm-client/src/api_path_builder.rs
> @@ -0,0 +1,207 @@
> +use std::borrow::Cow;
> +
> +/// Builder for urls with a query.
> +///
> +/// The [Self::arg] method can be used to add multiple arguments to the query.
> +///
> +/// ```rust
> +/// use pdm_client::api_path_builder::ApiPathBuilder;
> +///
> +/// let query = ApiPathBuilder::new("/api2/extjs/cluster/resources")
> +///     .arg("type", pve_api_types::ClusterResourceKind::Vm)
> +///     .build();
> +///
> +/// assert_eq!(&query, "/api2/extjs/cluster/resources?type=vm");
> +/// ```
> +///
> +/// ## Compatibility with perl clients
> +///
> +/// The methods `bool_arg` and `list_arg` were added to translate booleans as
> +/// `"0"`/`"1"` and split lists so that they can be feed to perl's
> +/// `split_list()` respectively. These methods require the
> +/// `perl-api-path-builder` feature.
> +pub struct ApiPathBuilder {
> +    url: String,
> +    separator: char,
> +}
> +
> +impl ApiPathBuilder {
> +    /// Creates a new builder.
> +    pub fn new<'a>(base: impl Into<Cow<'a, str>>) -> Self {
> +        Self {
> +            url: base.into().into_owned(),
> +            separator: '?',
> +        }
> +    }
> +
> +    /// Adds an argument to the query.
> +    ///
> +    /// The value is percent-encoded.
> +    pub fn arg<T: std::fmt::Display>(mut self, name: &str, value: T) -> Self {
> +        self.push_name_and_separator(name);
> +        self.push_encoded(value);
> +        self
> +    }
> +
> +    /// Adds an optional argument to the query.
> +    ///
> +    /// Does nothing if the value is `None`. See [Self::arg].
> +    pub fn maybe_arg<T: std::fmt::Display>(mut self, name: &str, value: &Option<T>) -> Self {
> +        if let Some(value) = value {
> +            self = self.arg(name, value);
> +        }
> +        self
> +    }
> +
> +    /// Builds the url.
> +    pub fn build(self) -> String {
> +        self.url
> +    }
> +
> +    fn push_name_and_separator(&mut self, name: &str) {
> +        self.url.push(self.separator);
> +        self.separator = '&';
> +        self.url.push_str(name);

With the builder being a more public facing thing now, it would probably
make sense to also encode the name rather than pushing it as-is.

> +        self.url.push('=');
> +    }
> +
> +    fn push_encoded<T: std::fmt::Display>(&mut self, value: T) {
> +        let str_value = value.to_string();
> +        let enc_value = percent_encoding::percent_encode(
> +            str_value.as_bytes(),
> +            percent_encoding::NON_ALPHANUMERIC,
> +        );
> +        self.url.extend(enc_value);
> +    }
> +}
> +
> +#[cfg(feature = "perl-api-path-builder")]
> +impl ApiPathBuilder {
> +    /// Adds a boolean arg in a perl-friendly fashion.
> +    ///
> +    /// `true` will be converted into `"1"` and `false` to `"0"`.
> +    pub fn bool_arg(mut self, name: &str, value: bool) -> Self {
> +        self.push_name_and_separator(name);
> +        if value {
> +            self.url.push('1');
> +        } else {
> +            self.url.push('0');
> +        };
> +        self
> +    }
> +
> +    /// Adds an optional boolean arg in a perl-friendly fashion.
> +    ///
> +    /// Does nothing if `value` is `None`. See [Self::bool_arg].
> +    pub fn maybe_bool_arg(mut self, name: &str, value: Option<bool>) -> Self {
> +        if let Some(value) = value {
> +            self = self.bool_arg(name, value);
> +        }
> +        self
> +    }
> +
> +    /// Helper for building perl-friendly queries.
> +    ///
> +    /// For `<type>-list` entries we turn an array into a string ready for
> +    /// perl's `split_list()` call.
> +    ///
> +    /// The values are percent-encoded.
> +    pub fn list_arg<T: std::fmt::Display, P: Iterator<Item = T>>(

I think it is more common to use `IntoIterator<Item = T>` as a bound for
such functions.

> +        mut self,
> +        name: &str,
> +        values: P,
> +    ) -> Self {
> +        self.push_name_and_separator(name);
> +        let mut list_separator = "";
> +        for entry in values {
> +            self.url.push_str(list_separator);
> +            list_separator = "%00";
> +            self.push_encoded(entry);
> +        }
> +        self
> +    }
> +
> +    /// Helper for building perl-friendly queries.
> +    ///
> +    /// See [Self::list_arg].
> +    pub fn maybe_list_arg<T: std::fmt::Display>(
> +        mut self,
> +        name: &str,
> +        values: &Option<Vec<T>>,
> +    ) -> Self {
> +        if let Some(values) = values {
> +            self = self.list_arg(name, values.iter());
> +        };
> +        self
> +    }
> +}
> +
> +#[cfg(test)]
> +mod tests {
> +    use pdm_api_types::ConfigurationState;
> +    use pve_api_types::ClusterResourceKind;
> +
> +    use super::*;
> +
> +    #[test]
> +    fn test_builder() {
> +        let expected = "/api2/extjs/cluster/resources?type=vm";
> +        let ty = ClusterResourceKind::Vm;
> +
> +        let query = ApiPathBuilder::new("/api2/extjs/cluster/resources")
> +            .arg("type", ty)
> +            .build();
> +
> +        assert_eq!(&query, expected);
> +
> +        let second_expected =
> +            "/api2/extjs/pve/remotes/some-remote/qemu/100/config?state=active&node=myNode";
> +        let state = ConfigurationState::Active;
> +        let node = "myNode";
> +        let snapshot = None::<&str>;
> +
> +        let second_query =
> +            ApiPathBuilder::new("/api2/extjs/pve/remotes/some-remote/qemu/100/config")
> +                .arg("state", state)
> +                .arg("node", node)
> +                .maybe_arg("snapshot", &snapshot)
> +                .build();
> +
> +        assert_eq!(&second_query, &second_expected);
> +    }
> +}
> +
> +#[cfg(all(test, feature = "perl-api-path-builder"))]
> +mod perl_tests {
> +    use super::*;
> +
> +    use pve_api_types::client::{add_query_arg, add_query_bool};
> +
> +    #[test]
> +    fn test_perl_builder() {
> +        let history = true;
> +        let local_only = false;
> +        let start_time = 1000;
> +
> +        let (mut query, mut sep) = (String::new(), '?');
> +        add_query_bool(&mut query, &mut sep, "history", Some(history));
> +        add_query_bool(&mut query, &mut sep, "local-only", Some(local_only));
> +        add_query_arg(&mut query, &mut sep, "start-time", &Some(start_time));
> +        let url = format!("/api2/extjs/cluster/metrics/export{query}");
> +
> +        let query = ApiPathBuilder::new("/api2/extjs/cluster/metrics/export")
> +            .bool_arg("history", history)
> +            .bool_arg("local-only", local_only)
> +            .arg("start-time", start_time)
> +            .build();
> +        assert_eq!(url, query);
> +
> +        let maybe_arg = ApiPathBuilder::new("/api2/extjs/cluster/metrics/export")
> +            .maybe_bool_arg("history", Some(history))
> +            .maybe_bool_arg("local-only", Some(local_only))
> +            .maybe_arg("start-time", &Some(start_time))
> +            .build();
> +
> +        assert_eq!(url, maybe_arg);
> +    }
> +}
> -- 
> 2.39.5


_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2025-03-21 13:00 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-02-14 14:11 [pdm-devel] [RFC PATCH 1/3] pdm-client: add query builder Maximiliano Sandoval
2025-02-14 14:11 ` [pdm-devel] [RFC PATCH 2/3] pdm-client: make use of " Maximiliano Sandoval
2025-02-14 14:11 ` [pdm-devel] [RFC PATCH 3/3] pbs-client: " Maximiliano Sandoval
2025-03-21 12:59 ` [pdm-devel] [RFC PATCH 1/3] pdm-client: add " Wolfgang Bumiller

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal