all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [RFC proxmox v2] schema: add CommaSeparatedList<T> wrapper type for comma-separated values
@ 2026-02-09  8:45 Dietmar Maurer
  2026-02-10  8:25 ` Thomas Lamprecht
  0 siblings, 1 reply; 2+ messages in thread
From: Dietmar Maurer @ 2026-02-09  8:45 UTC (permalink / raw)
  To: pve-devel

Introduce a new CommaSeparatedList<T> wrapper type that provides
schema-aware serialization and deserialization of comma-separated
values, similar to PropertyString but designed for list/array types.

Key components:
- CommaSeparatedListSchema trait: Provides the static ARRAY_SCHEMA
  required for (de)serialization (workaround for unstable generic
  const items in Rust)
- CommaSeparatedList<T>: A transparent Vec<T> newtype with Deref/
  DerefMut implementations for ergonomic access

The wrapper automatically handles conversion between "1,2,3" string
representation and Vec<T> while validating against the element schema.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---

Changes in v2:

- use description from ARRAY_SCHEMA
- add test for that



 proxmox-schema/src/comma_separated_list.rs | 173 +++++++++++++++++++++
 proxmox-schema/src/lib.rs                  |   1 +
 2 files changed, 174 insertions(+)
 create mode 100644 proxmox-schema/src/comma_separated_list.rs

diff --git a/proxmox-schema/src/comma_separated_list.rs b/proxmox-schema/src/comma_separated_list.rs
new file mode 100644
index 00000000..381b17c5
--- /dev/null
+++ b/proxmox-schema/src/comma_separated_list.rs
@@ -0,0 +1,173 @@
+use std::fmt::Display;
+use std::ops::{Deref, DerefMut};
+use std::str::FromStr;
+
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+use crate::{ApiStringFormat, ApiType, Schema, StringSchema};
+
+fn serialize<S, T>(
+    data: &[T],
+    serializer: S,
+    array_schema: &'static Schema,
+) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+    T: Serialize,
+{
+    use serde::ser::{Error, SerializeSeq};
+
+    let mut ser = crate::ser::PropertyStringSerializer::new(String::new(), array_schema)
+        .serialize_seq(Some(data.len()))
+        .map_err(S::Error::custom)?;
+
+    for element in data {
+        ser.serialize_element(element).map_err(S::Error::custom)?;
+    }
+
+    let out = ser.end().map_err(S::Error::custom)?;
+    serializer.serialize_str(&out)
+}
+
+fn deserialize<'de, D, T>(deserializer: D, array_schema: &'static Schema) -> Result<T, D::Error>
+where
+    D: Deserializer<'de>,
+    T: Deserialize<'de>,
+{
+    use serde::de::Error;
+
+    let string = std::borrow::Cow::<'de, str>::deserialize(deserializer)?;
+
+    T::deserialize(crate::de::SchemaDeserializer::new(string, array_schema))
+        .map_err(D::Error::custom)
+}
+
+/// Trait to provide a static array schema for a type.
+/// This is needed because generic const items are unstable in Rust.
+pub trait CommaSeparatedListSchema: ApiType {
+    /// The static array schema for this type.
+    const ARRAY_SCHEMA: Schema;
+}
+
+#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Ord, PartialOrd)]
+#[repr(transparent)]
+pub struct CommaSeparatedList<T>(pub Vec<T>);
+
+impl<T> ApiType for CommaSeparatedList<T>
+where
+    T: CommaSeparatedListSchema,
+{
+    const API_SCHEMA: Schema = StringSchema::new(T::ARRAY_SCHEMA.unwrap_array_schema().description)
+        .format(&ApiStringFormat::PropertyString(&T::ARRAY_SCHEMA))
+        .schema();
+}
+
+impl<T: CommaSeparatedListSchema + Serialize> Serialize for CommaSeparatedList<T> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serialize(&self.0, serializer, &T::ARRAY_SCHEMA)
+    }
+}
+
+impl<'de, T: CommaSeparatedListSchema + Deserialize<'de>> Deserialize<'de>
+    for CommaSeparatedList<T>
+{
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let vec: Vec<T> = deserialize(deserializer, &T::ARRAY_SCHEMA)?;
+        Ok(CommaSeparatedList(vec))
+    }
+}
+
+impl<T: FromStr + Display> CommaSeparatedList<T> {
+    pub fn new(inner: Vec<T>) -> Self {
+        Self(inner)
+    }
+
+    pub fn into_inner(self) -> Vec<T> {
+        self.0
+    }
+}
+
+impl<T> Deref for CommaSeparatedList<T> {
+    type Target = Vec<T>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl<T> DerefMut for CommaSeparatedList<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl<T> AsRef<Vec<T>> for CommaSeparatedList<T> {
+    fn as_ref(&self) -> &Vec<T> {
+        &self.0
+    }
+}
+
+impl<T> AsMut<Vec<T>> for CommaSeparatedList<T> {
+    fn as_mut(&mut self) -> &mut Vec<T> {
+        &mut self.0
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{ArraySchema, IntegerSchema};
+
+    // Test type that implements CommaSeparatedListSchema
+    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
+    struct TestNum(u32);
+
+    const TEST_NUM_SCHEMA: Schema = IntegerSchema::new("Test number (0-3)").maximum(3).schema();
+    const TEST_NUM_ARRAY_SCHEMA: Schema =
+        ArraySchema::new("Array of test numbers.", &TEST_NUM_SCHEMA).schema();
+
+    impl ApiType for TestNum {
+        const API_SCHEMA: Schema = TEST_NUM_SCHEMA;
+    }
+
+    impl CommaSeparatedListSchema for TestNum {
+        const ARRAY_SCHEMA: Schema = TEST_NUM_ARRAY_SCHEMA;
+    }
+
+    #[test]
+    fn test_comma_separated_list_serialize() {
+        let list = CommaSeparatedList(vec![TestNum(1), TestNum(2), TestNum(3)]);
+        let s = serde_json::to_value(&list).unwrap();
+        // The serialize function should produce a property string
+        assert_eq!(s.as_str(), Some("1,2,3"));
+    }
+
+    #[test]
+    fn test_comma_separated_list_deref() {
+        let list = CommaSeparatedList(vec![TestNum(42)]);
+        assert_eq!(list.len(), 1);
+        assert_eq!(list[0], TestNum(42));
+    }
+
+    #[test]
+    fn test_comma_separated_list_deserialize() {
+        let list: CommaSeparatedList<TestNum> = serde_json::from_value("1,2,3".into()).unwrap();
+        assert_eq!(list.0, vec![TestNum(1), TestNum(2), TestNum(3)]);
+        // test integer maximum (4 > maximum)
+        let _ = serde_json::from_value::<CommaSeparatedList<TestNum>>("3,4".into()).unwrap_err();
+    }
+
+    #[test]
+    fn test_comma_separated_list_description() {
+        let descr = CommaSeparatedList::<TestNum>::API_SCHEMA
+            .unwrap_string_schema()
+            .description;
+        assert_eq!(descr, "Array of test numbers.");
+    }
+}
diff --git a/proxmox-schema/src/lib.rs b/proxmox-schema/src/lib.rs
index 1647e8a9..fd773a84 100644
--- a/proxmox-schema/src/lib.rs
+++ b/proxmox-schema/src/lib.rs
@@ -22,6 +22,7 @@ pub mod de;
 pub mod format;
 pub mod ser;
 
+pub mod comma_separated_list;
 pub mod property_string;
 
 mod schema;
-- 
2.47.3




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

* Re: [RFC proxmox v2] schema: add CommaSeparatedList<T> wrapper type for comma-separated values
  2026-02-09  8:45 [RFC proxmox v2] schema: add CommaSeparatedList<T> wrapper type for comma-separated values Dietmar Maurer
@ 2026-02-10  8:25 ` Thomas Lamprecht
  0 siblings, 0 replies; 2+ messages in thread
From: Thomas Lamprecht @ 2026-02-10  8:25 UTC (permalink / raw)
  To: Dietmar Maurer, pve-devel

Am 09.02.26 um 09:45 schrieb Dietmar Maurer:
> Introduce a new CommaSeparatedList<T> wrapper type that provides
> schema-aware serialization and deserialization of comma-separated
> values, similar to PropertyString but designed for list/array types.
> 
> Key components:
> - CommaSeparatedListSchema trait: Provides the static ARRAY_SCHEMA
>   required for (de)serialization (workaround for unstable generic
>   const items in Rust)
> - CommaSeparatedList<T>: A transparent Vec<T> newtype with Deref/
>   DerefMut implementations for ergonomic access
> 
> The wrapper automatically handles conversion between "1,2,3" string
> representation and Vec<T> while validating against the element schema.
> 
> Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
> ---
> 
> Changes in v2:
> 
> - use description from ARRAY_SCHEMA
> - add test for that
> 
> 
> 
>  proxmox-schema/src/comma_separated_list.rs | 173 +++++++++++++++++++++
>  proxmox-schema/src/lib.rs                  |   1 +
>  2 files changed, 174 insertions(+)
>  create mode 100644 proxmox-schema/src/comma_separated_list.rs
> 
> diff --git a/proxmox-schema/src/comma_separated_list.rs b/proxmox-schema/src/comma_separated_list.rs
> new file mode 100644
> index 00000000..381b17c5
> --- /dev/null
> +++ b/proxmox-schema/src/comma_separated_list.rs
> @@ -0,0 +1,173 @@
Would be nice to have a module-level rust doc for new modules with an usage example.
As those doc examples are run on testing/build, if not excluded explicitly, they could
also provide some (extra) coverage here.

E.g., from a quick check the cases for empty list serializing to "" (or?) and a 
list with spaces after the comma are not tested as of now.

> ...

> +impl<T: FromStr + Display> CommaSeparatedList<T> {

Is there any reason for limiting this to FromStr + Display ?
The implementation does not depend on it and if one would have a type with
Serialize + CommaSeparatedListSchema implemented, they couldn't use this constructor.

If there's a reason behind doing this, it'd be nice to have a short comment here
to describe why the type bounds are more limiting than necessary.

> +    pub fn new(inner: Vec<T>) -> Self {
> +        Self(inner)
> +    }
> +
> +    pub fn into_inner(self) -> Vec<T> {
> +        self.0
> +    }
> +}
> +

> ...

> diff --git a/proxmox-schema/src/lib.rs b/proxmox-schema/src/lib.rs
> index 1647e8a9..fd773a84 100644
> --- a/proxmox-schema/src/lib.rs
> +++ b/proxmox-schema/src/lib.rs
> @@ -22,6 +22,7 @@ pub mod de;
>  pub mod format;
>  pub mod ser;
>  
> +pub mod comma_separated_list;

This makes the main type accessible under "proxmox_schema::comma_separated_list::CommaSeparatedList"
which is rather redundant and IMO has not much benefits over re-exporting CommaSeparatedList
directly to allow accessing it under proxmox_schema::CommaSeparatedList

But no hard feelings, we got this "problem" quite a few times already.




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

end of thread, other threads:[~2026-02-10  8:25 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-09  8:45 [RFC proxmox v2] schema: add CommaSeparatedList<T> wrapper type for comma-separated values Dietmar Maurer
2026-02-10  8:25 ` Thomas Lamprecht

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