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 <[email protected]> --- 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 [email protected] https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
