all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [RFC proxmox] schema: add CommaSeparatedList<T> wrapper type for comma-separated values
@ 2026-01-28 12:19 Dietmar Maurer
  0 siblings, 0 replies; only message in thread
From: Dietmar Maurer @ 2026-01-28 12:19 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>
---
 proxmox-schema/src/comma_separated_list.rs | 165 +++++++++++++++++++++
 proxmox-schema/src/lib.rs                  |   1 +
 2 files changed, 166 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..06b85aa9
--- /dev/null
+++ b/proxmox-schema/src/comma_separated_list.rs
@@ -0,0 +1,165 @@
+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("Comma separated list")
+        .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();
+    }
+}
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


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


^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2026-01-28 12:19 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-01-28 12:19 [pve-devel] [RFC proxmox] schema: add CommaSeparatedList<T> wrapper type for comma-separated values Dietmar Maurer

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