diff --git a/proxmox-schema/src/de/mod.rs b/proxmox-schema/src/de/mod.rs index 52897fea..eca835e3 100644 --- a/proxmox-schema/src/de/mod.rs +++ b/proxmox-schema/src/de/mod.rs @@ -498,6 +498,9 @@ pub struct MapAccess<'de, 'i> { /// The current next value's key, value and schema (if available). value: Option<(Cow<'de, str>, Cow<'de, str>, Option<&'static Schema>)>, + + /// We just returned the key-value pair of a keyAlias: + was_alias: Option<&'static str>, } impl<'de, 'i> MapAccess<'de, 'i> { @@ -508,6 +511,7 @@ impl<'de, 'i> MapAccess<'de, 'i> { schema, input_at: 0, value: None, + was_alias: None, } } @@ -521,6 +525,7 @@ impl<'de, 'i> MapAccess<'de, 'i> { schema, input_at: 0, value: None, + was_alias: None, } } @@ -534,8 +539,26 @@ impl<'de, 'i> MapAccess<'de, 'i> { schema, input_at: 0, value: None, + was_alias: None, } } + + /// Returns (key, value), since the key exists as a static string in the KeyAliasInfo, we + /// return the static one instead of the passed parameter, this simplifies later lifetime + /// handling. + fn try_key_alias_info( + &self, + key: Option<&str>, + ) -> Option<(&'static str, &'static str, &'static str)> { + let key = key?; + let info = self.schema.key_alias_info()?; + + let Ok(index) = info.values.binary_search(&key) else { + return None; + }; + + Some((info.key_alias, info.values[index], info.alias)) + } } impl<'de> de::MapAccess<'de> for MapAccess<'de, '_> { @@ -552,11 +575,30 @@ impl<'de> de::MapAccess<'de> for MapAccess<'de, '_> { return Ok(None); } - let (key, value, rem) = match next_property(&self.input[self.input_at..]) { + let (mut key, value, rem) = match next_property(&self.input[self.input_at..]) { None => return Ok(None), Some(entry) => entry?, }; + if let Some(alias) = std::mem::take(&mut self.was_alias) { + key = Some(alias); + } else if let Some((key, value, alias)) = self.try_key_alias_info(key) { + // If the object schema has a "KeyAliasInfo", take a detour through it: if our + // "key" is defined in it, we first return the key as a value for its declared + // `keyAlias`. + self.was_alias = Some(alias); + let schema = self + .schema + .lookup(key) + .ok_or(Error::msg("key alias info pointed to key a without schema"))? + .1; + + let out = + seed.deserialize(de::value::BorrowedStrDeserializer::<'de, Error>::new(key))?; + self.value = Some((Cow::Borrowed(key), Cow::Borrowed(value), Some(schema))); + return Ok(Some(out)); + } + if rem.is_empty() { self.input_at = self.input.len(); } else { diff --git a/proxmox-schema/src/property_string.rs b/proxmox-schema/src/property_string.rs index e0620115..01b5727a 100644 --- a/proxmox-schema/src/property_string.rs +++ b/proxmox-schema/src/property_string.rs @@ -468,4 +468,51 @@ mod test { Ok(()) } + + #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] + pub struct NetworkCard { + model: String, + macaddr: String, + disconnected: Option, + } + + impl ApiType for NetworkCard { + const API_SCHEMA: Schema = ObjectSchema::new( + "A network card", + &[ + // MUST BE SORTED + ( + "disconnected", + true, + &BooleanSchema::new("disconnected").schema(), + ), + ("macaddr", false, &StringSchema::new("macaddr").schema()), + ("model", false, &StringSchema::new("model").schema()), + ], + ) + .key_alias_info(crate::schema::KeyAliasInfo::new( + "model", + &["e1000", "virtio"], + "macaddr", + )) + .schema(); + } + + #[test] + fn test_key_alias_info() -> Result<(), super::Error> { + let deserialized: NetworkCard = super::parse("virtio=aa:bb:cc:dd:ee,disconnected=0") + .expect("failed to parse property string"); + + assert_eq!( + deserialized, + NetworkCard { + model: "virtio".to_string(), + macaddr: "aa:bb:cc:dd:ee".to_string(), + disconnected: Some(false), + }, + "KeyAliasInfo deserialization failed" + ); + + Ok(()) + } } diff --git a/proxmox-schema/src/schema.rs b/proxmox-schema/src/schema.rs index fcf71b1c..8dfd2759 100644 --- a/proxmox-schema/src/schema.rs +++ b/proxmox-schema/src/schema.rs @@ -604,6 +604,31 @@ pub type SchemaPropertyEntry = (&'static str, bool, &'static Schema); /// This is a workaround unless RUST can const_fn `Hash::new()` pub type SchemaPropertyMap = &'static [SchemaPropertyEntry]; +/// Legacy property strings may contain shortcuts where the *value* of a specific key is used as a +/// *key* for yet another option. Most notably, PVE's `netX` properties use `=` +/// instead of `model=,macaddr=`. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))] +pub struct KeyAliasInfo { + pub key_alias: &'static str, + pub values: &'static [&'static str], + pub alias: &'static str, +} + +impl KeyAliasInfo { + pub const fn new( + key_alias: &'static str, + values: &'static [&'static str], + alias: &'static str, + ) -> Self { + Self { + key_alias, + values, + alias, + } + } +} + /// Data type to describe objects (maps). #[derive(Debug)] #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))] @@ -616,6 +641,13 @@ pub struct ObjectSchema { pub properties: SchemaPropertyMap, /// Default key name - used by `parse_parameter_string()` pub default_key: Option<&'static str>, + /// DO NOT USE! + /// + /// This is meant for the PVE schema generator ONLY! + /// + /// This is to support legacy property string information: declare a `keyAlias` and its + /// corresponding `alias` property (as defined in PVE's schema). + pub key_alias_info: Option, } impl ObjectSchema { @@ -625,6 +657,7 @@ impl ObjectSchema { properties, additional_properties: false, default_key: None, + key_alias_info: None, } } @@ -665,6 +698,17 @@ impl ObjectSchema { ) -> Result { ParameterSchema::from(self).parse_parameter_strings(data, test_required) } + + /// DO NOT USE! + /// + /// This is meant for the PVE schema generator ONLY! + /// + /// This is to support legacy property string information: declare a `keyAlias` and its + /// corresponding `alias` property (as defined in PVE's schema). + pub const fn key_alias_info(mut self, key_alias_info: KeyAliasInfo) -> Self { + self.key_alias_info = Some(key_alias_info); + self + } } /// Combines multiple *object* schemas into one. @@ -822,6 +866,11 @@ pub trait ObjectSchemaType: private::Sealed + Send + Sync { fn additional_properties(&self) -> bool; fn default_key(&self) -> Option<&'static str>; + /// Should always return `None`, unless dealing with *legacy* PVE property strings. + fn key_alias_info(&self) -> Option { + None + } + /// Verify JSON value using an object schema. fn verify_json(&self, data: &Value) -> Result<(), Error> { let map = match data { @@ -905,6 +954,10 @@ impl ObjectSchemaType for ObjectSchema { fn default_key(&self) -> Option<&'static str> { self.default_key } + + fn key_alias_info(&self) -> Option { + self.key_alias_info + } } impl ObjectSchemaType for AllOfSchema { diff --git a/proxmox-schema/tests/schema.rs b/proxmox-schema/tests/schema.rs index a918f6d1..7b87cbfe 100644 --- a/proxmox-schema/tests/schema.rs +++ b/proxmox-schema/tests/schema.rs @@ -25,6 +25,7 @@ fn test_schema1() { additional_properties: false, properties: &[], default_key: None, + key_alias_info: None, }); println!("TEST Schema: {:?}", schema);