]> git.proxmox.com Git - proxmox.git/commitdiff
schema: serde based property string de- and serialization
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Wed, 15 Feb 2023 13:55:10 +0000 (14:55 +0100)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Thu, 13 Jul 2023 14:18:58 +0000 (16:18 +0200)
This provides `proxmox_schema::property_string::PropertyString<T>` for
a typed property-string.

To facilitate this, this introduces
`proxmox_schema::de::SchemaDeserializer` which is a serde deserializer
for property strings given a schema.

This basically maps to one of `de::SeqAccess` (for array schemas) or
`de::MapAccess` (for object schemas).

Additionally, a `de::NoSchemaDeserializer` is added, since properties
within the strings may have string schemas with no format to it, while
the type we serialize to may ask for an array (a simple "list") via
serde.

The deserializers support borrowing, for which a helper `Cow3` needed
to be added, since property strings support quoting with escape
sequences where an intermediate string would be allocated and with an
intermediate lifetime distinct from the `'de` lifetime.

A `de::verify` module is added which uses serde infrastructure to
validate schemas without first having to deserialize a complete
`serde_json::Value`.

For serialization, `proxmox_schema::ser::PropertyStringSerializer` is
added split into similar parts `ser::SerializeStruct` and
`ser::SerializeSeq` at the top level, and the same prefixed with
`Element` for inside the actual string. This should also properly
quote the contents if required.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
proxmox-schema/src/de.rs [deleted file]
proxmox-schema/src/de/cow3.rs [new file with mode: 0644]
proxmox-schema/src/de/extract.rs [new file with mode: 0644]
proxmox-schema/src/de/mod.rs [new file with mode: 0644]
proxmox-schema/src/de/no_schema.rs [new file with mode: 0644]
proxmox-schema/src/de/verify.rs [new file with mode: 0644]
proxmox-schema/src/lib.rs
proxmox-schema/src/property_string.rs
proxmox-schema/src/schema.rs
proxmox-schema/src/ser/mod.rs [new file with mode: 0644]

diff --git a/proxmox-schema/src/de.rs b/proxmox-schema/src/de.rs
deleted file mode 100644 (file)
index 6c2edcd..0000000
+++ /dev/null
@@ -1,297 +0,0 @@
-//! Partial object deserialization by extracting object portions from a Value using an api schema.
-
-use std::fmt;
-
-use serde::de::{self, IntoDeserializer, Visitor};
-use serde_json::Value;
-
-use crate::{ObjectSchemaType, Schema};
-
-pub struct Error {
-    msg: String,
-}
-
-impl fmt::Debug for Error {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        fmt::Debug::fmt(&self.msg, f)
-    }
-}
-
-impl fmt::Display for Error {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        fmt::Display::fmt(&self.msg, f)
-    }
-}
-
-impl std::error::Error for Error {}
-
-impl serde::de::Error for Error {
-    fn custom<T: fmt::Display>(msg: T) -> Self {
-        Self {
-            msg: msg.to_string(),
-        }
-    }
-}
-
-impl From<serde_json::Error> for Error {
-    fn from(error: serde_json::Error) -> Self {
-        Error {
-            msg: error.to_string(),
-        }
-    }
-}
-
-pub struct ExtractValueDeserializer<'o> {
-    object: &'o mut serde_json::Map<String, Value>,
-    schema: &'static Schema,
-}
-
-impl<'o> ExtractValueDeserializer<'o> {
-    pub fn try_new(
-        object: &'o mut serde_json::Map<String, Value>,
-        schema: &'static Schema,
-    ) -> Option<Self> {
-        match schema {
-            Schema::Object(_) | Schema::AllOf(_) => Some(Self { object, schema }),
-            _ => None,
-        }
-    }
-}
-
-macro_rules! deserialize_non_object {
-    ($name:ident) => {
-        fn $name<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
-        where
-            V: Visitor<'de>,
-        {
-            Err(de::Error::custom(
-                "deserializing partial object into type which is not an object",
-            ))
-        }
-    };
-    ($name:ident ( $($args:tt)* )) => {
-        fn $name<V>(self, $($args)*, _visitor: V) -> Result<V::Value, Self::Error>
-        where
-            V: Visitor<'de>,
-        {
-            Err(de::Error::custom(
-                "deserializing partial object into type which is not an object",
-            ))
-        }
-    };
-}
-
-impl<'de> de::Deserializer<'de> for ExtractValueDeserializer<'de> {
-    type Error = Error;
-
-    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
-    where
-        V: Visitor<'de>,
-    {
-        self.deserialize_map(visitor)
-    }
-
-    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Error>
-    where
-        V: Visitor<'de>,
-    {
-        use serde::de::Error;
-
-        match self.schema {
-            Schema::Object(schema) => visitor.visit_map(MapAccess::<'de>::new(
-                self.object,
-                schema.properties().map(|(name, _, _)| *name),
-            )),
-            Schema::AllOf(schema) => visitor.visit_map(MapAccess::<'de>::new(
-                self.object,
-                schema.properties().map(|(name, _, _)| *name),
-            )),
-
-            // The following should be caught by ExtractValueDeserializer::new()!
-            _ => Err(Error::custom(
-                "ExtractValueDeserializer used with invalid schema",
-            )),
-        }
-    }
-
-    fn deserialize_struct<V>(
-        self,
-        _name: &'static str,
-        _fields: &'static [&'static str],
-        visitor: V,
-    ) -> Result<V::Value, Error>
-    where
-        V: Visitor<'de>,
-    {
-        use serde::de::Error;
-
-        match self.schema {
-            Schema::Object(schema) => visitor.visit_map(MapAccess::<'de>::new(
-                self.object,
-                schema.properties().map(|(name, _, _)| *name),
-            )),
-            Schema::AllOf(schema) => visitor.visit_map(MapAccess::<'de>::new(
-                self.object,
-                schema.properties().map(|(name, _, _)| *name),
-            )),
-
-            // The following should be caught by ExtractValueDeserializer::new()!
-            _ => Err(Error::custom(
-                "ExtractValueDeserializer used with invalid schema",
-            )),
-        }
-    }
-
-    deserialize_non_object!(deserialize_i8);
-    deserialize_non_object!(deserialize_i16);
-    deserialize_non_object!(deserialize_i32);
-    deserialize_non_object!(deserialize_i64);
-    deserialize_non_object!(deserialize_u8);
-    deserialize_non_object!(deserialize_u16);
-    deserialize_non_object!(deserialize_u32);
-    deserialize_non_object!(deserialize_u64);
-    deserialize_non_object!(deserialize_f32);
-    deserialize_non_object!(deserialize_f64);
-    deserialize_non_object!(deserialize_char);
-    deserialize_non_object!(deserialize_bool);
-    deserialize_non_object!(deserialize_str);
-    deserialize_non_object!(deserialize_string);
-    deserialize_non_object!(deserialize_bytes);
-    deserialize_non_object!(deserialize_byte_buf);
-    deserialize_non_object!(deserialize_option);
-    deserialize_non_object!(deserialize_seq);
-    deserialize_non_object!(deserialize_unit);
-    deserialize_non_object!(deserialize_identifier);
-    deserialize_non_object!(deserialize_unit_struct(_: &'static str));
-    deserialize_non_object!(deserialize_newtype_struct(_: &'static str));
-    deserialize_non_object!(deserialize_tuple(_: usize));
-    deserialize_non_object!(deserialize_tuple_struct(_: &'static str, _: usize));
-    deserialize_non_object!(deserialize_enum(
-        _: &'static str,
-        _: &'static [&'static str]
-    ));
-
-    fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        visitor.visit_unit()
-    }
-}
-
-struct MapAccess<'o, I> {
-    object: &'o mut serde_json::Map<String, Value>,
-    iter: I,
-    value: Option<Value>,
-}
-
-impl<'o, I> MapAccess<'o, I>
-where
-    I: Iterator<Item = &'static str>,
-{
-    fn new(object: &'o mut serde_json::Map<String, Value>, iter: I) -> Self {
-        Self {
-            object,
-            iter,
-            value: None,
-        }
-    }
-}
-
-impl<'de, I> de::MapAccess<'de> for MapAccess<'de, I>
-where
-    I: Iterator<Item = &'static str>,
-{
-    type Error = Error;
-
-    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
-    where
-        K: de::DeserializeSeed<'de>,
-    {
-        loop {
-            return match self.iter.next() {
-                Some(key) => match self.object.remove(key) {
-                    Some(value) => {
-                        self.value = Some(value);
-                        seed.deserialize(key.into_deserializer()).map(Some)
-                    }
-                    None => continue,
-                },
-                None => Ok(None),
-            };
-        }
-    }
-
-    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
-    where
-        V: de::DeserializeSeed<'de>,
-    {
-        match self.value.take() {
-            Some(value) => seed.deserialize(value).map_err(Error::from),
-            None => Err(de::Error::custom("value is missing")),
-        }
-    }
-}
-
-#[test]
-#[allow(clippy::disallowed_names)]
-fn test_extraction() {
-    use serde::Deserialize;
-
-    use crate::{ObjectSchema, StringSchema};
-
-    #[derive(Deserialize)]
-    struct Foo {
-        foo1: String,
-        foo2: String,
-    }
-
-    const SIMPLE_STRING: Schema = StringSchema::new("simple").schema();
-    const FOO_SCHEMA: Schema = ObjectSchema::new(
-        "A Foo",
-        &[
-            ("foo1", false, &SIMPLE_STRING),
-            ("foo2", false, &SIMPLE_STRING),
-        ],
-    )
-    .schema();
-
-    #[derive(Deserialize)]
-    struct Bar {
-        bar1: String,
-        bar2: String,
-    }
-
-    const BAR_SCHEMA: Schema = ObjectSchema::new(
-        "A Bar",
-        &[
-            ("bar1", false, &SIMPLE_STRING),
-            ("bar2", false, &SIMPLE_STRING),
-        ],
-    )
-    .schema();
-
-    let mut data = serde_json::json!({
-        "foo1": "hey1",
-        "foo2": "hey2",
-        "bar1": "there1",
-        "bar2": "there2",
-    });
-
-    let data = data.as_object_mut().unwrap();
-
-    let foo: Foo =
-        Foo::deserialize(ExtractValueDeserializer::try_new(data, &FOO_SCHEMA).unwrap()).unwrap();
-
-    assert!(data.remove("foo1").is_none());
-    assert!(data.remove("foo2").is_none());
-    assert_eq!(foo.foo1, "hey1");
-    assert_eq!(foo.foo2, "hey2");
-
-    let bar =
-        Bar::deserialize(ExtractValueDeserializer::try_new(data, &BAR_SCHEMA).unwrap()).unwrap();
-
-    assert!(data.is_empty());
-    assert_eq!(bar.bar1, "there1");
-    assert_eq!(bar.bar2, "there2");
-}
diff --git a/proxmox-schema/src/de/cow3.rs b/proxmox-schema/src/de/cow3.rs
new file mode 100644 (file)
index 0000000..3044681
--- /dev/null
@@ -0,0 +1,171 @@
+use std::borrow::{Borrow, Cow};
+use std::fmt;
+use std::ops::Range;
+
+/// Manage 2 lifetimes for deserializing.
+///
+/// When deserializing from a value it is considered to have lifetime `'de`. Any value that doesn't
+/// need to live longer than the deserialized *input* can *borrow* from that lifetime.
+///
+/// For example, from the `String` `{ "hello": "you" }` you can deserialize a `HashMap<&'de str,
+/// &'de str>`, as long as that map only exists as long as the original string.
+///
+/// However, if the data is `{ "hello": "\"hello\"" }`, then the value string needs to be
+/// unescaped, and can only be owned. However, if you only need it *temporarily*, eg. to parse a
+/// property string of numbers, you may want to avoid cloning individual parts from that.
+///
+/// Due to implementation details (particularly not wanting to provide a `Cow` version of
+/// `PropertyIterator`), we may need to be able to hold references to such intermediate values.
+///
+/// For the above scenario, `'o` would be the original `'de` lifetime, and `'i` the intermediate
+/// lifetime for the unescaped string.
+///
+/// Finally we also have an "Owned" value as a 3rd option.
+pub enum Cow3<'o, 'i, B>
+where
+    B: 'o + 'i + ToOwned + ?Sized,
+{
+    /// Original lifetime from the deserialization entry point.
+    Original(&'o B),
+
+    /// Borrowed from an intermediate value.
+    Intermediate(&'i B),
+
+    /// Owned data.
+    Owned(<B as ToOwned>::Owned),
+}
+
+impl<'o, 'i, B> Cow3<'o, 'i, B>
+where
+    B: 'o + 'i + ToOwned + ?Sized,
+{
+    /// From a `Cow` with the original lifetime.
+    pub fn from_original<T>(value: T) -> Self
+    where
+        T: Into<Cow<'o, B>>,
+    {
+        match value.into() {
+            Cow::Borrowed(v) => Self::Original(v),
+            Cow::Owned(v) => Self::Owned(v),
+        }
+    }
+
+    /// From a `Cow` with the intermediate lifetime.
+    pub fn from_intermediate<T>(value: T) -> Self
+    where
+        T: Into<Cow<'i, B>>,
+    {
+        match value.into() {
+            Cow::Borrowed(v) => Self::Intermediate(v),
+            Cow::Owned(v) => Self::Owned(v),
+        }
+    }
+
+    /// Turn into a `Cow`, forcing intermediate values to become owned.
+    pub fn into_original_or_owned(self) -> Cow<'o, B> {
+        match self {
+            Self::Original(v) => Cow::Borrowed(v),
+            Self::Intermediate(v) => Cow::Owned(v.to_owned()),
+            Self::Owned(v) => Cow::Owned(v),
+        }
+    }
+}
+
+impl<'o, 'i, B> std::ops::Deref for Cow3<'o, 'i, B>
+where
+    B: 'o + 'i + ToOwned + ?Sized,
+    <B as ToOwned>::Owned: Borrow<B>,
+{
+    type Target = B;
+
+    fn deref(&self) -> &B {
+        match self {
+            Self::Original(v) => v,
+            Self::Intermediate(v) => v,
+            Self::Owned(v) => v.borrow(),
+        }
+    }
+}
+
+impl<'o, 'i, B> AsRef<B> for Cow3<'o, 'i, B>
+where
+    B: 'o + 'i + ToOwned + ?Sized,
+    <B as ToOwned>::Owned: Borrow<B>,
+{
+    fn as_ref(&self) -> &B {
+        &self
+    }
+}
+
+/// Build a `Cow3` with a value surviving the `'o` lifetime.
+impl<'x, 'o, 'i, B> From<&'x B> for Cow3<'o, 'i, B>
+where
+    B: 'o + 'i + ToOwned + ?Sized,
+    <B as ToOwned>::Owned: Borrow<B>,
+    'x: 'o,
+{
+    fn from(value: &'x B) -> Self {
+        Self::Original(value)
+    }
+}
+
+impl<B: ?Sized> fmt::Display for Cow3<'_, '_, B>
+where
+    B: fmt::Display + ToOwned,
+    B::Owned: fmt::Display,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match *self {
+            Self::Original(ref b) => fmt::Display::fmt(b, f),
+            Self::Intermediate(ref b) => fmt::Display::fmt(b, f),
+            Self::Owned(ref o) => fmt::Display::fmt(o, f),
+        }
+    }
+}
+
+impl<B: ?Sized> fmt::Debug for Cow3<'_, '_, B>
+where
+    B: fmt::Debug + ToOwned,
+    B::Owned: fmt::Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match *self {
+            Self::Original(ref b) => fmt::Debug::fmt(b, f),
+            Self::Intermediate(ref b) => fmt::Debug::fmt(b, f),
+            Self::Owned(ref o) => fmt::Debug::fmt(o, f),
+        }
+    }
+}
+
+impl<'o, 'i> Cow3<'o, 'i, str> {
+    /// Index value as a borrowed value.
+    pub fn slice<'ni, I>(&'ni self, index: I) -> Cow3<'o, 'ni, str>
+    where
+        I: std::slice::SliceIndex<str, Output = str>,
+        'i: 'ni,
+    {
+        match self {
+            Self::Original(value) => Cow3::Original(&value[index]),
+            Self::Intermediate(value) => Cow3::Intermediate(&value[index]),
+            Self::Owned(value) => Cow3::Intermediate(&value.as_str()[index]),
+        }
+    }
+}
+
+pub fn str_slice_to_range(original: &str, slice: &str) -> Option<Range<usize>> {
+    let bytes = original.as_bytes();
+
+    let orig_addr = bytes.as_ptr() as usize;
+    let slice_addr = slice.as_bytes().as_ptr() as usize;
+    let offset = slice_addr.checked_sub(orig_addr)?;
+    if offset > orig_addr + bytes.len() {
+        return None;
+    }
+
+    let end = offset + slice.as_bytes().len();
+    if end > orig_addr + bytes.len() {
+        return None;
+    }
+
+    Some(offset..end)
+}
diff --git a/proxmox-schema/src/de/extract.rs b/proxmox-schema/src/de/extract.rs
new file mode 100644 (file)
index 0000000..6c2edcd
--- /dev/null
@@ -0,0 +1,297 @@
+//! Partial object deserialization by extracting object portions from a Value using an api schema.
+
+use std::fmt;
+
+use serde::de::{self, IntoDeserializer, Visitor};
+use serde_json::Value;
+
+use crate::{ObjectSchemaType, Schema};
+
+pub struct Error {
+    msg: String,
+}
+
+impl fmt::Debug for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt::Debug::fmt(&self.msg, f)
+    }
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt::Display::fmt(&self.msg, f)
+    }
+}
+
+impl std::error::Error for Error {}
+
+impl serde::de::Error for Error {
+    fn custom<T: fmt::Display>(msg: T) -> Self {
+        Self {
+            msg: msg.to_string(),
+        }
+    }
+}
+
+impl From<serde_json::Error> for Error {
+    fn from(error: serde_json::Error) -> Self {
+        Error {
+            msg: error.to_string(),
+        }
+    }
+}
+
+pub struct ExtractValueDeserializer<'o> {
+    object: &'o mut serde_json::Map<String, Value>,
+    schema: &'static Schema,
+}
+
+impl<'o> ExtractValueDeserializer<'o> {
+    pub fn try_new(
+        object: &'o mut serde_json::Map<String, Value>,
+        schema: &'static Schema,
+    ) -> Option<Self> {
+        match schema {
+            Schema::Object(_) | Schema::AllOf(_) => Some(Self { object, schema }),
+            _ => None,
+        }
+    }
+}
+
+macro_rules! deserialize_non_object {
+    ($name:ident) => {
+        fn $name<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
+        where
+            V: Visitor<'de>,
+        {
+            Err(de::Error::custom(
+                "deserializing partial object into type which is not an object",
+            ))
+        }
+    };
+    ($name:ident ( $($args:tt)* )) => {
+        fn $name<V>(self, $($args)*, _visitor: V) -> Result<V::Value, Self::Error>
+        where
+            V: Visitor<'de>,
+        {
+            Err(de::Error::custom(
+                "deserializing partial object into type which is not an object",
+            ))
+        }
+    };
+}
+
+impl<'de> de::Deserializer<'de> for ExtractValueDeserializer<'de> {
+    type Error = Error;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_map(visitor)
+    }
+
+    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        use serde::de::Error;
+
+        match self.schema {
+            Schema::Object(schema) => visitor.visit_map(MapAccess::<'de>::new(
+                self.object,
+                schema.properties().map(|(name, _, _)| *name),
+            )),
+            Schema::AllOf(schema) => visitor.visit_map(MapAccess::<'de>::new(
+                self.object,
+                schema.properties().map(|(name, _, _)| *name),
+            )),
+
+            // The following should be caught by ExtractValueDeserializer::new()!
+            _ => Err(Error::custom(
+                "ExtractValueDeserializer used with invalid schema",
+            )),
+        }
+    }
+
+    fn deserialize_struct<V>(
+        self,
+        _name: &'static str,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        use serde::de::Error;
+
+        match self.schema {
+            Schema::Object(schema) => visitor.visit_map(MapAccess::<'de>::new(
+                self.object,
+                schema.properties().map(|(name, _, _)| *name),
+            )),
+            Schema::AllOf(schema) => visitor.visit_map(MapAccess::<'de>::new(
+                self.object,
+                schema.properties().map(|(name, _, _)| *name),
+            )),
+
+            // The following should be caught by ExtractValueDeserializer::new()!
+            _ => Err(Error::custom(
+                "ExtractValueDeserializer used with invalid schema",
+            )),
+        }
+    }
+
+    deserialize_non_object!(deserialize_i8);
+    deserialize_non_object!(deserialize_i16);
+    deserialize_non_object!(deserialize_i32);
+    deserialize_non_object!(deserialize_i64);
+    deserialize_non_object!(deserialize_u8);
+    deserialize_non_object!(deserialize_u16);
+    deserialize_non_object!(deserialize_u32);
+    deserialize_non_object!(deserialize_u64);
+    deserialize_non_object!(deserialize_f32);
+    deserialize_non_object!(deserialize_f64);
+    deserialize_non_object!(deserialize_char);
+    deserialize_non_object!(deserialize_bool);
+    deserialize_non_object!(deserialize_str);
+    deserialize_non_object!(deserialize_string);
+    deserialize_non_object!(deserialize_bytes);
+    deserialize_non_object!(deserialize_byte_buf);
+    deserialize_non_object!(deserialize_option);
+    deserialize_non_object!(deserialize_seq);
+    deserialize_non_object!(deserialize_unit);
+    deserialize_non_object!(deserialize_identifier);
+    deserialize_non_object!(deserialize_unit_struct(_: &'static str));
+    deserialize_non_object!(deserialize_newtype_struct(_: &'static str));
+    deserialize_non_object!(deserialize_tuple(_: usize));
+    deserialize_non_object!(deserialize_tuple_struct(_: &'static str, _: usize));
+    deserialize_non_object!(deserialize_enum(
+        _: &'static str,
+        _: &'static [&'static str]
+    ));
+
+    fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_unit()
+    }
+}
+
+struct MapAccess<'o, I> {
+    object: &'o mut serde_json::Map<String, Value>,
+    iter: I,
+    value: Option<Value>,
+}
+
+impl<'o, I> MapAccess<'o, I>
+where
+    I: Iterator<Item = &'static str>,
+{
+    fn new(object: &'o mut serde_json::Map<String, Value>, iter: I) -> Self {
+        Self {
+            object,
+            iter,
+            value: None,
+        }
+    }
+}
+
+impl<'de, I> de::MapAccess<'de> for MapAccess<'de, I>
+where
+    I: Iterator<Item = &'static str>,
+{
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        loop {
+            return match self.iter.next() {
+                Some(key) => match self.object.remove(key) {
+                    Some(value) => {
+                        self.value = Some(value);
+                        seed.deserialize(key.into_deserializer()).map(Some)
+                    }
+                    None => continue,
+                },
+                None => Ok(None),
+            };
+        }
+    }
+
+    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
+    where
+        V: de::DeserializeSeed<'de>,
+    {
+        match self.value.take() {
+            Some(value) => seed.deserialize(value).map_err(Error::from),
+            None => Err(de::Error::custom("value is missing")),
+        }
+    }
+}
+
+#[test]
+#[allow(clippy::disallowed_names)]
+fn test_extraction() {
+    use serde::Deserialize;
+
+    use crate::{ObjectSchema, StringSchema};
+
+    #[derive(Deserialize)]
+    struct Foo {
+        foo1: String,
+        foo2: String,
+    }
+
+    const SIMPLE_STRING: Schema = StringSchema::new("simple").schema();
+    const FOO_SCHEMA: Schema = ObjectSchema::new(
+        "A Foo",
+        &[
+            ("foo1", false, &SIMPLE_STRING),
+            ("foo2", false, &SIMPLE_STRING),
+        ],
+    )
+    .schema();
+
+    #[derive(Deserialize)]
+    struct Bar {
+        bar1: String,
+        bar2: String,
+    }
+
+    const BAR_SCHEMA: Schema = ObjectSchema::new(
+        "A Bar",
+        &[
+            ("bar1", false, &SIMPLE_STRING),
+            ("bar2", false, &SIMPLE_STRING),
+        ],
+    )
+    .schema();
+
+    let mut data = serde_json::json!({
+        "foo1": "hey1",
+        "foo2": "hey2",
+        "bar1": "there1",
+        "bar2": "there2",
+    });
+
+    let data = data.as_object_mut().unwrap();
+
+    let foo: Foo =
+        Foo::deserialize(ExtractValueDeserializer::try_new(data, &FOO_SCHEMA).unwrap()).unwrap();
+
+    assert!(data.remove("foo1").is_none());
+    assert!(data.remove("foo2").is_none());
+    assert_eq!(foo.foo1, "hey1");
+    assert_eq!(foo.foo2, "hey2");
+
+    let bar =
+        Bar::deserialize(ExtractValueDeserializer::try_new(data, &BAR_SCHEMA).unwrap()).unwrap();
+
+    assert!(data.is_empty());
+    assert_eq!(bar.bar1, "there1");
+    assert_eq!(bar.bar2, "there2");
+}
diff --git a/proxmox-schema/src/de/mod.rs b/proxmox-schema/src/de/mod.rs
new file mode 100644 (file)
index 0000000..25efb42
--- /dev/null
@@ -0,0 +1,624 @@
+//! Property string deserialization.
+
+use std::borrow::Cow;
+use std::fmt;
+use std::ops::Range;
+
+use serde::de::{self, IntoDeserializer};
+
+use crate::schema::{self, ArraySchema, Schema};
+
+mod cow3;
+mod extract;
+mod no_schema;
+
+pub mod verify;
+
+pub use extract::ExtractValueDeserializer;
+
+use cow3::{str_slice_to_range, Cow3};
+
+#[derive(Debug)]
+pub struct Error(Cow<'static, str>);
+
+impl std::error::Error for Error {}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt::Display::fmt(&self.0, f)
+    }
+}
+
+impl Error {
+    pub(crate) fn msg<T: Into<Cow<'static, str>>>(msg: T) -> Self {
+        Self(msg.into())
+    }
+
+    fn invalid<T: fmt::Display>(msg: T) -> Self {
+        Self::msg(format!("schema validation failed: {}", msg))
+    }
+}
+
+impl serde::de::Error for Error {
+    fn custom<T: fmt::Display>(msg: T) -> Self {
+        Self(msg.to_string().into())
+    }
+}
+
+impl From<serde_json::Error> for Error {
+    fn from(error: serde_json::Error) -> Self {
+        Self(error.to_string().into())
+    }
+}
+
+impl From<fmt::Error> for Error {
+    fn from(err: fmt::Error) -> Self {
+        Self::msg(err.to_string())
+    }
+}
+
+/// Deserializer for parts a part of a property string given a schema.
+pub struct SchemaDeserializer<'de, 'i> {
+    input: Cow3<'de, 'i, str>,
+    schema: &'static Schema,
+}
+
+impl<'de, 'i> SchemaDeserializer<'de, 'i> {
+    pub fn new_cow(input: Cow3<'de, 'i, str>, schema: &'static Schema) -> Self {
+        Self { input, schema }
+    }
+
+    pub fn new<T>(input: T, schema: &'static Schema) -> Self
+    where
+        T: Into<Cow<'de, str>>,
+    {
+        Self {
+            input: Cow3::from_original(input.into()),
+            schema,
+        }
+    }
+
+    fn deserialize_str<V>(
+        self,
+        visitor: V,
+        schema: &'static schema::StringSchema,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        schema
+            .check_constraints(&self.input)
+            .map_err(|err| Error::invalid(err))?;
+        match self.input {
+            Cow3::Original(input) => visitor.visit_borrowed_str(input),
+            Cow3::Intermediate(input) => visitor.visit_str(input),
+            Cow3::Owned(input) => visitor.visit_string(input),
+        }
+    }
+
+    fn deserialize_property_string<V>(
+        self,
+        visitor: V,
+        schema: &'static Schema,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match schema {
+            Schema::Object(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)),
+            Schema::AllOf(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)),
+            _ => Err(Error::msg(
+                "non-object-like schema in ApiStringFormat::PropertyString while deserializing a property string",
+            )),
+        }
+    }
+
+    fn deserialize_array_string<V>(
+        self,
+        visitor: V,
+        schema: &'static Schema,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match schema {
+            Schema::Array(schema) => visitor.visit_seq(SeqAccess::new(self.input, schema)),
+            _ => Err(Error::msg(
+                "non-array schema in ApiStringFormat::PropertyString while deserializing an array",
+            )),
+        }
+    }
+}
+
+impl<'de, 'i> de::Deserializer<'de> for SchemaDeserializer<'de, 'i> {
+    type Error = Error;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.schema {
+            Schema::Array(schema) => visitor.visit_seq(SeqAccess::new(self.input, schema)),
+            Schema::AllOf(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)),
+            Schema::Object(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)),
+            Schema::Null => Err(Error::msg("null")),
+            Schema::Boolean(_) => visitor.visit_bool(
+                schema::parse_boolean(&self.input)
+                    .map_err(|_| Error::msg(format!("not a boolean: {:?}", self.input)))?,
+            ),
+            Schema::Integer(schema) => {
+                // FIXME: isize vs explicit i64, needs fixing in schema check_constraints api
+                let value: isize = self
+                    .input
+                    .parse()
+                    .map_err(|_| Error::msg(format!("not an integer: {:?}", self.input)))?;
+
+                schema
+                    .check_constraints(value)
+                    .map_err(|err| Error::invalid(err))?;
+
+                let value: i64 = i64::try_from(value)
+                    .map_err(|_| Error::invalid("isize did not fit into i64"))?;
+
+                if let Ok(value) = u64::try_from(value) {
+                    visitor.visit_u64(value)
+                } else {
+                    visitor.visit_i64(value)
+                }
+            }
+            Schema::Number(schema) => {
+                let value: f64 = self
+                    .input
+                    .parse()
+                    .map_err(|_| Error::msg(format!("not a valid number: {:?}", self.input)))?;
+
+                schema
+                    .check_constraints(value)
+                    .map_err(|err| Error::invalid(err))?;
+
+                visitor.visit_f64(value)
+            }
+            Schema::String(schema) => {
+                // If not requested differently, strings stay strings, otherwise deserializing to a
+                // `Value` will get objects here instead of strings, which we do not expect
+                // anywhere.
+                self.deserialize_str(visitor, schema)
+            }
+        }
+    }
+
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if self.input.is_empty() {
+            visitor.visit_none()
+        } else {
+            visitor.visit_some(self)
+        }
+    }
+
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.schema {
+            Schema::String(schema) => self.deserialize_str(visitor, schema),
+            _ => Err(Error::msg(
+                "tried to deserialize a string with a non-string-schema",
+            )),
+        }
+    }
+
+    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.schema {
+            Schema::String(schema) => self.deserialize_str(visitor, schema),
+            _ => Err(Error::msg(
+                "tried to deserialize a string with a non-string-schema",
+            )),
+        }
+    }
+
+    fn deserialize_newtype_struct<V>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_newtype_struct(self)
+    }
+
+    fn deserialize_struct<V>(
+        self,
+        name: &'static str,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.schema {
+            Schema::Object(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)),
+            Schema::AllOf(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)),
+            Schema::String(schema) => match schema.format {
+                Some(schema::ApiStringFormat::PropertyString(schema)) => {
+                    self.deserialize_property_string(visitor, schema)
+                }
+                _ => Err(Error::msg(format!(
+                    "cannot deserialize struct '{}' with a string schema",
+                    name
+                ))),
+            },
+            _ => Err(Error::msg(format!(
+                "cannot deserialize struct '{}' with non-object schema",
+                name,
+            ))),
+        }
+    }
+
+    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.schema {
+            Schema::Object(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)),
+            Schema::AllOf(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)),
+            Schema::String(schema) => match schema.format {
+                Some(schema::ApiStringFormat::PropertyString(schema)) => {
+                    self.deserialize_property_string(visitor, schema)
+                }
+                _ => Err(Error::msg(format!(
+                    "cannot deserialize map with a string schema",
+                ))),
+            },
+            _ => Err(Error::msg(format!(
+                "cannot deserialize map with non-object schema",
+            ))),
+        }
+    }
+
+    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.schema {
+            Schema::Array(schema) => visitor.visit_seq(SeqAccess::new(self.input, schema)),
+            Schema::String(schema) => match schema.format {
+                Some(schema::ApiStringFormat::PropertyString(schema)) => {
+                    self.deserialize_array_string(visitor, schema)
+                }
+                _ => Err(Error::msg("cannot deserialize array with a string schema")),
+            },
+            _ => Err(Error::msg(
+                "cannot deserialize array with non-object schema",
+            )),
+        }
+    }
+
+    fn deserialize_enum<V>(
+        self,
+        name: &'static str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.schema {
+            Schema::String(_) => visitor.visit_enum(self.input.into_deserializer()),
+            _ => Err(Error::msg(format!(
+                "cannot deserialize enum '{}' with non-string schema",
+                name,
+            ))),
+        }
+    }
+
+    serde::forward_to_deserialize_any! {
+            i8 i16 i32 i64
+            u8 u16 u32 u64
+            f32 f64
+            bool
+            char
+            bytes byte_buf
+            unit unit_struct
+            tuple tuple_struct
+            identifier
+            ignored_any
+    }
+}
+
+fn next_str_entry(input: &str, at: &mut usize, has_null: bool) -> Option<Range<usize>> {
+    while *at != input.len() {
+        let begin = *at;
+
+        let part = &input[*at..];
+
+        let part_end = if has_null {
+            part.find('\0')
+        } else {
+            part.find(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
+        };
+
+        let end = match part_end {
+            None => {
+                *at = input.len();
+                input.len()
+            }
+            Some(rel_end) => {
+                *at += rel_end + 1;
+                begin + rel_end
+            }
+        };
+
+        if input[..end].is_empty() {
+            continue;
+        }
+
+        return Some(begin..end);
+    }
+
+    None
+}
+
+/// Parse an array with a schema.
+///
+/// Provides both `SeqAccess` and `Deserializer` implementations.
+pub struct SeqAccess<'o, 'i, 's> {
+    schema: &'s ArraySchema,
+    was_empty: bool,
+    input: Cow3<'o, 'i, str>,
+    has_null: bool,
+    at: usize,
+    count: usize,
+}
+
+impl<'o, 'i, 's> SeqAccess<'o, 'i, 's> {
+    pub fn new(input: Cow3<'o, 'i, str>, schema: &'s ArraySchema) -> Self {
+        Self {
+            schema,
+            was_empty: input.is_empty(),
+            has_null: input.contains('\0'),
+            input,
+            at: 0,
+            count: 0,
+        }
+    }
+}
+
+impl<'de, 'i, 's> de::SeqAccess<'de> for SeqAccess<'de, 'i, 's> {
+    type Error = Error;
+
+    fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Error>
+    where
+        T: de::DeserializeSeed<'de>,
+    {
+        if self.was_empty {
+            return Ok(None);
+        }
+
+        while let Some(el_range) = next_str_entry(&self.input, &mut self.at, self.has_null) {
+            if el_range.is_empty() {
+                continue;
+            }
+
+            if let Some(max) = self.schema.max_length {
+                if self.count == max {
+                    return Err(Error::msg("too many elements"));
+                }
+            }
+
+            self.count += 1;
+
+            return seed
+                .deserialize(SchemaDeserializer::new_cow(
+                    self.input.slice(el_range),
+                    self.schema.items,
+                ))
+                .map(Some);
+        }
+
+        if let Some(min) = self.schema.min_length {
+            if self.count < min {
+                return Err(Error::msg("not enough elements"));
+            }
+        }
+
+        Ok(None)
+    }
+}
+
+impl<'de, 'i, 's> de::Deserializer<'de> for SeqAccess<'de, 'i, 's> {
+    type Error = Error;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_seq(self)
+    }
+
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if self.was_empty {
+            visitor.visit_none()
+        } else {
+            visitor.visit_some(self)
+        }
+    }
+
+    serde::forward_to_deserialize_any! {
+        i8 i16 i32 i64 u8 u16 u32 u64 f32 f64
+        bool char str string
+        bytes byte_buf
+        unit unit_struct
+        newtype_struct
+        tuple tuple_struct
+        enum map seq
+        struct
+        identifier ignored_any
+    }
+}
+
+/// Provides serde's `MapAccess` for parsing a property string.
+pub struct MapAccess<'de, 'i> {
+    // The property string iterator and quoted string handler.
+    input: Cow3<'de, 'i, str>,
+    input_at: usize, // for when using `Cow3::Owned`.
+
+    /// As a `Deserializer` we want to be able to handle `deserialize_option` and need to know
+    /// whether this was an empty string.
+    was_empty: bool,
+
+    /// The schema used to verify the contents and distinguish between structs and property
+    /// strings.
+    schema: &'static dyn schema::ObjectSchemaType,
+
+    /// The current next value's key, value and schema (if available).
+    value: Option<(Cow<'de, str>, Cow<'de, str>, Option<&'static Schema>)>,
+}
+
+impl<'de, 'i> MapAccess<'de, 'i> {
+    pub fn new<S: schema::ObjectSchemaType>(input: &'de str, schema: &'static S) -> Self {
+        Self {
+            was_empty: input.is_empty(),
+            input: Cow3::Original(input),
+            schema,
+            input_at: 0,
+            value: None,
+        }
+    }
+
+    pub fn new_cow<S: schema::ObjectSchemaType>(
+        input: Cow3<'de, 'i, str>,
+        schema: &'static S,
+    ) -> Self {
+        Self {
+            was_empty: input.is_empty(),
+            input,
+            schema,
+            input_at: 0,
+            value: None,
+        }
+    }
+
+    pub fn new_intermediate<S: schema::ObjectSchemaType>(
+        input: &'i str,
+        schema: &'static S,
+    ) -> Self {
+        Self {
+            was_empty: input.is_empty(),
+            input: Cow3::Intermediate(input),
+            schema,
+            input_at: 0,
+            value: None,
+        }
+    }
+}
+
+impl<'de, 'i> de::MapAccess<'de> for MapAccess<'de, 'i> {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        use crate::property_string::next_property;
+
+        if self.was_empty {
+            // shortcut
+            return Ok(None);
+        }
+
+        let (key, value, rem) = match next_property(&self.input[self.input_at..]) {
+            None => return Ok(None),
+            Some(entry) => entry?,
+        };
+
+        if rem.is_empty() {
+            self.input_at = self.input.len();
+        } else {
+            let ofs = unsafe { rem.as_ptr().offset_from(self.input.as_ptr()) };
+            if ofs < 0 || (ofs as usize) > self.input.len() {
+                // 'rem' is either an empty string (rem.is_empty() is true), or a valid offset into
+                // the input string...
+                panic!("unexpected remainder in next_property");
+            }
+            self.input_at = ofs as usize;
+        }
+
+        let value = match value {
+            Cow::Owned(value) => Cow::Owned(value),
+            Cow::Borrowed(value) => match str_slice_to_range(&self.input, value) {
+                None => Cow::Owned(value.to_string()),
+                Some(range) => match &self.input {
+                    Cow3::Original(orig) => Cow::Borrowed(&orig[range]),
+                    _ => Cow::Owned(value.to_string()),
+                },
+            },
+        };
+
+        let (key, schema) = match key {
+            Some(key) => {
+                let schema = self.schema.lookup(&key);
+                let key = match str_slice_to_range(&self.input, key) {
+                    None => Cow::Owned(key.to_string()),
+                    Some(range) => match &self.input {
+                        Cow3::Original(orig) => Cow::Borrowed(&orig[range]),
+                        _ => Cow::Owned(key.to_string()),
+                    },
+                };
+                (key, schema)
+            }
+            None => match self.schema.default_key() {
+                Some(key) => {
+                    let schema = self
+                        .schema
+                        .lookup(key)
+                        .ok_or(Error::msg("bad default key"))?;
+                    (Cow::Borrowed(key), Some(schema))
+                }
+                None => return Err(Error::msg("missing key")),
+            },
+        };
+        let schema = schema.map(|(_optional, schema)| schema);
+
+        let out = match &key {
+            Cow::Borrowed(key) => {
+                seed.deserialize(de::value::BorrowedStrDeserializer::<'de, Error>::new(key))?
+            }
+            Cow::Owned(key) => {
+                seed.deserialize(IntoDeserializer::<Error>::into_deserializer(key.as_str()))?
+            }
+        };
+
+        self.value = Some((key, value, schema));
+
+        Ok(Some(out))
+    }
+
+    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
+    where
+        V: de::DeserializeSeed<'de>,
+    {
+        let (key, input, schema) = self.value.take().ok_or(Error::msg("bad map access"))?;
+
+        if let Some(schema) = schema {
+            seed.deserialize(SchemaDeserializer::new(input, schema))
+        } else {
+            if !verify::is_verifying() && !self.schema.additional_properties() {
+                return Err(Error::msg(format!("unknown key {:?}", key.as_ref())));
+            }
+
+            // additional properties are treated as strings...
+            let deserializer = no_schema::NoSchemaDeserializer::new(input);
+            seed.deserialize(deserializer)
+        }
+    }
+}
diff --git a/proxmox-schema/src/de/no_schema.rs b/proxmox-schema/src/de/no_schema.rs
new file mode 100644 (file)
index 0000000..254ebd9
--- /dev/null
@@ -0,0 +1,311 @@
+//! When we have no schema we allow simple values and arrays.
+
+use std::borrow::Cow;
+
+use serde::de;
+
+use super::cow3::Cow3;
+use super::Error;
+
+/// This can only deserialize strings and lists of strings and has no schema.
+pub struct NoSchemaDeserializer<'de, 'i> {
+    input: Cow3<'de, 'i, str>,
+}
+
+impl<'de, 'i> NoSchemaDeserializer<'de, 'i> {
+    pub fn new<T>(input: T) -> Self
+    where
+        T: Into<Cow<'de, str>>,
+    {
+        Self {
+            input: Cow3::from_original(input),
+        }
+    }
+}
+
+macro_rules! deserialize_num {
+    ($( $name:ident : $visit:ident : $ty:ty : $error:literal, )*) => {$(
+        fn $name<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
+            let value: $ty = self
+                .input
+                .parse()
+                .map_err(|_| Error::msg(format!($error, self.input)))?;
+            visitor.$visit(value)
+        }
+    )*}
+}
+
+impl<'de, 'i> de::Deserializer<'de> for NoSchemaDeserializer<'de, 'i> {
+    type Error = Error;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.input {
+            Cow3::Original(input) => visitor.visit_borrowed_str(input),
+            Cow3::Intermediate(input) => visitor.visit_str(input),
+            Cow3::Owned(input) => visitor.visit_string(input),
+        }
+    }
+
+    fn deserialize_struct<V>(
+        self,
+        _name: &'static str,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        self.deserialize_any(visitor)
+    }
+
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if self.input.is_empty() {
+            visitor.visit_none()
+        } else {
+            visitor.visit_some(self)
+        }
+    }
+
+    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        self.deserialize_any(visitor)
+    }
+
+    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_seq(SimpleSeqAccess::new(self.input))
+    }
+
+    fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_seq(SimpleSeqAccess::new(self.input))
+    }
+
+    fn deserialize_tuple_struct<V>(
+        self,
+        _name: &'static str,
+        _len: usize,
+        visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_seq(SimpleSeqAccess::new(self.input))
+    }
+
+    deserialize_num! {
+        deserialize_i8   : visit_i8   : i8   : "not an integer: {:?}",
+        deserialize_u8   : visit_u8   : u8   : "not an integer: {:?}",
+        deserialize_i16  : visit_i16  : i16  : "not an integer: {:?}",
+        deserialize_u16  : visit_u16  : u16  : "not an integer: {:?}",
+        deserialize_i32  : visit_i32  : i32  : "not an integer: {:?}",
+        deserialize_u32  : visit_u32  : u32  : "not an integer: {:?}",
+        deserialize_i64  : visit_i64  : i64  : "not an integer: {:?}",
+        deserialize_u64  : visit_u64  : u64  : "not an integer: {:?}",
+        deserialize_f32  : visit_f32  : f32  : "not a number: {:?}",
+        deserialize_f64  : visit_f64  : f64  : "not a number: {:?}",
+        deserialize_bool : visit_bool : bool : "not a boolean: {:?}",
+    }
+
+    fn deserialize_char<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        let mut chars = self.input.chars();
+        let ch = chars
+            .next()
+            .ok_or_else(|| Error::msg(format!("not a single character: {:?}", self.input)))?;
+        if chars.next().is_some() {
+            return Err(Error::msg(format!(
+                "not a single character: {:?}",
+                self.input
+            )));
+        }
+        visitor.visit_char(ch)
+    }
+
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.input {
+            Cow3::Original(input) => visitor.visit_borrowed_str(input),
+            Cow3::Intermediate(input) => visitor.visit_str(input),
+            Cow3::Owned(input) => visitor.visit_string(input),
+        }
+    }
+
+    fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.input {
+            Cow3::Original(input) => visitor.visit_borrowed_str(input),
+            Cow3::Intermediate(input) => visitor.visit_str(input),
+            Cow3::Owned(input) => visitor.visit_string(input),
+        }
+    }
+
+    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.input {
+            Cow3::Original(input) => visitor.visit_borrowed_str(input),
+            Cow3::Intermediate(input) => visitor.visit_str(input),
+            Cow3::Owned(input) => visitor.visit_string(input),
+        }
+    }
+
+    fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.input {
+            Cow3::Original(input) => visitor.visit_borrowed_bytes(input.as_bytes()),
+            Cow3::Intermediate(input) => visitor.visit_bytes(input.as_bytes()),
+            Cow3::Owned(input) => visitor.visit_byte_buf(input.into_bytes()),
+        }
+    }
+
+    fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.input {
+            Cow3::Original(input) => visitor.visit_borrowed_bytes(input.as_bytes()),
+            Cow3::Intermediate(input) => visitor.visit_bytes(input.as_bytes()),
+            Cow3::Owned(input) => visitor.visit_byte_buf(input.into_bytes()),
+        }
+    }
+
+    fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if self.input.is_empty() {
+            visitor.visit_unit()
+        } else {
+            self.deserialize_string(visitor)
+        }
+    }
+
+    fn deserialize_unit_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if self.input.is_empty() {
+            visitor.visit_unit()
+        } else {
+            self.deserialize_string(visitor)
+        }
+    }
+
+    fn deserialize_newtype_struct<V>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_newtype_struct(self)
+    }
+
+    fn deserialize_enum<V>(
+        self,
+        _name: &'static str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        use serde::de::IntoDeserializer;
+        visitor.visit_enum(self.input.into_deserializer())
+    }
+
+    fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        self.deserialize_string(visitor)
+    }
+}
+
+/// Parse an array without a schema.
+///
+/// It may only contain simple values.
+struct SimpleSeqAccess<'de, 'i> {
+    input: Cow3<'de, 'i, str>,
+    has_null: bool,
+    at: usize,
+}
+
+impl<'de, 'i> SimpleSeqAccess<'de, 'i> {
+    fn new(input: Cow3<'de, 'i, str>) -> Self {
+        Self {
+            has_null: input.contains('\0'),
+            input,
+            at: 0,
+        }
+    }
+}
+
+impl<'de, 'i> de::SeqAccess<'de> for SimpleSeqAccess<'de, 'i> {
+    type Error = Error;
+
+    fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Error>
+    where
+        T: de::DeserializeSeed<'de>,
+    {
+        while self.at != self.input.len() {
+            let begin = self.at;
+
+            let input = &self.input[self.at..];
+
+            let end = if self.has_null {
+                input.find('\0')
+            } else {
+                input.find(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
+            };
+
+            let end = match end {
+                None => {
+                    self.at = self.input.len();
+                    input.len()
+                }
+                Some(pos) => {
+                    self.at += pos + 1;
+                    pos
+                }
+            };
+
+            if input[..end].is_empty() {
+                continue;
+            }
+
+            return seed
+                .deserialize(NoSchemaDeserializer::new(match &self.input {
+                    Cow3::Original(input) => Cow::Borrowed(&input[begin..end]),
+                    Cow3::Intermediate(input) => Cow::Owned(input[begin..end].to_string()),
+                    Cow3::Owned(input) => Cow::Owned(input[begin..end].to_string()),
+                }))
+                .map(Some);
+        }
+
+        Ok(None)
+    }
+}
diff --git a/proxmox-schema/src/de/verify.rs b/proxmox-schema/src/de/verify.rs
new file mode 100644 (file)
index 0000000..a626d28
--- /dev/null
@@ -0,0 +1,298 @@
+use std::borrow::Cow;
+use std::cell::UnsafeCell;
+use std::collections::HashSet;
+use std::fmt;
+use std::mem;
+
+use anyhow::format_err;
+use serde::de::{self, Deserialize, Unexpected};
+
+use super::Schema;
+use crate::schema::ParameterError;
+
+struct VerifyState {
+    schema: Option<&'static Schema>,
+    path: String,
+}
+
+thread_local! {
+    static VERIFY_SCHEMA: UnsafeCell<Option<VerifyState>> = UnsafeCell::new(None);
+    static ERRORS: UnsafeCell<Vec<(String, anyhow::Error)>> = UnsafeCell::new(Vec::new());
+}
+
+pub(crate) struct SchemaGuard(Option<VerifyState>);
+
+impl Drop for SchemaGuard {
+    fn drop(&mut self) {
+        VERIFY_SCHEMA.with(|schema| unsafe {
+            if self.0.is_none() {
+                ERRORS.with(|errors| (*errors.get()).clear())
+            }
+            *schema.get() = self.0.take();
+        });
+    }
+}
+
+impl SchemaGuard {
+    /// If this is the "final" guard, take out the errors:
+    fn errors(self) -> Option<Vec<(String, anyhow::Error)>> {
+        if self.0.is_none() {
+            Some(ERRORS.with(|e| mem::take(unsafe { &mut *e.get() })))
+        } else {
+            None
+        }
+    }
+}
+
+pub(crate) fn push_schema(schema: Option<&'static Schema>, path: Option<&str>) -> SchemaGuard {
+    SchemaGuard(VERIFY_SCHEMA.with(|s| {
+        let prev = unsafe { (*s.get()).take() };
+        let path = match (path, &prev) {
+            (Some(path), Some(prev)) => join_path(&prev.path, path),
+            (Some(path), None) => path.to_owned(),
+            (None, Some(prev)) => prev.path.clone(),
+            (None, None) => String::new(),
+        };
+
+        unsafe {
+            (*s.get()) = Some(VerifyState { schema, path });
+        }
+
+        prev
+    }))
+}
+
+fn get_path() -> Option<String> {
+    VERIFY_SCHEMA.with(|s| unsafe { (*s.get()).as_ref().map(|state| state.path.clone()) })
+}
+
+fn get_schema() -> Option<&'static Schema> {
+    VERIFY_SCHEMA.with(|s| unsafe { (*s.get()).as_ref().and_then(|state| state.schema) })
+}
+
+pub(crate) fn is_verifying() -> bool {
+    VERIFY_SCHEMA.with(|s| unsafe { (*s.get()).as_ref().is_some() })
+}
+
+fn join_path(a: &str, b: &str) -> String {
+    if a.is_empty() {
+        b.to_string()
+    } else {
+        format!("{}/{}", a, b)
+    }
+}
+
+fn push_errstr_path(err_path: &str, err: &str) {
+    if let Some(path) = get_path() {
+        push_err_do(join_path(&path, err_path), format_err!("{}", err));
+    }
+}
+
+fn push_err(err: impl fmt::Display) {
+    if let Some(path) = get_path() {
+        push_err_do(path, format_err!("{}", err));
+    }
+}
+
+fn push_err_do(path: String, err: anyhow::Error) {
+    ERRORS.with(move |errors| unsafe { (*errors.get()).push((path, err)) })
+}
+
+/// Helper to collect multiple deserialization errors for better reporting.
+///
+/// This is similar to [`IgnoredAny`] in that it implements [`Deserialize`]
+/// but does not actually deserialize to anything, however, when a deserialization error occurs,
+/// it'll try to continue and collect further errors.
+///
+/// This only makes sense with the [`SchemaDeserializer`](super::SchemaDeserializer).
+pub struct Verifier;
+
+impl<'de> Deserialize<'de> for Verifier {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        if let Some(schema) = get_schema() {
+            let visitor = Visitor(schema);
+            match schema {
+                Schema::Boolean(_) => deserializer.deserialize_bool(visitor),
+                Schema::Integer(_) => deserializer.deserialize_i64(visitor),
+                Schema::Number(_) => deserializer.deserialize_f64(visitor),
+                Schema::String(_) => deserializer.deserialize_str(visitor),
+                Schema::Object(_) => deserializer.deserialize_map(visitor),
+                Schema::AllOf(_) => deserializer.deserialize_map(visitor),
+                Schema::Array(_) => deserializer.deserialize_seq(visitor),
+                Schema::Null => deserializer.deserialize_unit(visitor),
+            }
+        } else {
+            Ok(Verifier)
+        }
+    }
+}
+
+pub fn verify(schema: &'static Schema, value: &str) -> Result<(), anyhow::Error> {
+    let guard = push_schema(Some(schema), None);
+    Verifier::deserialize(super::SchemaDeserializer::new(value, schema))?;
+
+    if let Some(errors) = guard.errors() {
+        Err(ParameterError::from_list(errors).into())
+    } else {
+        Ok(())
+    }
+}
+
+struct Visitor(&'static Schema);
+
+impl<'de> de::Visitor<'de> for Visitor {
+    type Value = Verifier;
+
+    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self.0 {
+            Schema::Boolean(_) => f.write_str("boolean"),
+            Schema::Integer(_) => f.write_str("integer"),
+            Schema::Number(_) => f.write_str("number"),
+            Schema::String(_) => f.write_str("string"),
+            Schema::Object(_) => f.write_str("object"),
+            Schema::AllOf(_) => f.write_str("allOf"),
+            Schema::Array(_) => f.write_str("Array"),
+            Schema::Null => f.write_str("null"),
+        }
+    }
+
+    fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
+        match self.0 {
+            Schema::Boolean(_) => (),
+            _ => return Err(E::invalid_type(Unexpected::Bool(v), &self)),
+        }
+        Ok(Verifier)
+    }
+
+    fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
+        match self.0 {
+            Schema::Integer(schema) => match schema.check_constraints(v as isize) {
+                Ok(()) => Ok(Verifier),
+                Err(err) => Err(E::custom(err)),
+            },
+            _ => Err(E::invalid_type(Unexpected::Signed(v), &self)),
+        }
+    }
+
+    fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
+        match self.0 {
+            Schema::Integer(schema) => match schema.check_constraints(v as isize) {
+                Ok(()) => Ok(Verifier),
+                Err(err) => Err(E::custom(err)),
+            },
+            _ => Err(E::invalid_type(Unexpected::Unsigned(v), &self)),
+        }
+    }
+
+    fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
+        match self.0 {
+            Schema::Number(schema) => match schema.check_constraints(v) {
+                Ok(()) => Ok(Verifier),
+                Err(err) => Err(E::custom(err)),
+            },
+            _ => Err(E::invalid_type(Unexpected::Float(v), &self)),
+        }
+    }
+
+    fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
+        use de::Error;
+
+        let schema = match self.0 {
+            Schema::Array(schema) => schema,
+            _ => return Err(A::Error::invalid_type(Unexpected::Seq, &self)),
+        };
+
+        let _guard = push_schema(Some(schema.items), None);
+
+        let mut count = 0;
+        loop {
+            match seq.next_element::<Verifier>() {
+                Ok(Some(_)) => count += 1,
+                Ok(None) => break,
+                Err(err) => push_err(err),
+            }
+        }
+
+        schema.check_length(count).map_err(de::Error::custom)?;
+
+        Ok(Verifier)
+    }
+
+    fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
+        use de::Error;
+
+        let schema: &'static dyn crate::schema::ObjectSchemaType = match self.0 {
+            Schema::Object(schema) => schema,
+            Schema::AllOf(schema) => schema,
+            _ => return Err(A::Error::invalid_type(Unexpected::Map, &self)),
+        };
+
+        let mut required_keys = HashSet::<&'static str>::new();
+        for (key, optional, _schema) in schema.properties() {
+            if !optional {
+                required_keys.insert(key);
+            }
+        }
+
+        let mut other_keys = HashSet::<String>::new();
+        loop {
+            let key: Cow<'de, str> = match map.next_key()? {
+                Some(key) => key,
+                None => break,
+            };
+
+            let _guard = match schema.lookup(&key) {
+                Some((optional, schema)) => {
+                    if !optional {
+                        // required keys are only tracked in the required_keys hashset
+                        if !required_keys.remove(key.as_ref()) {
+                            // duplicate key
+                            push_errstr_path(&key, "duplicate key");
+                        }
+                    } else {
+                        // optional keys
+                        if !other_keys.insert(key.clone().into_owned()) {
+                            push_errstr_path(&key, "duplicate key");
+                        }
+                    }
+
+                    push_schema(Some(schema), Some(&key))
+                }
+                None => {
+                    if !schema.additional_properties() {
+                        push_errstr_path(&key, "schema does not allow additional properties");
+                    } else if !other_keys.insert(key.clone().into_owned()) {
+                        push_errstr_path(&key, "duplicate key");
+                    }
+
+                    push_schema(None, Some(&key))
+                }
+            };
+
+            match map.next_value::<Verifier>() {
+                Ok(Verifier) => (),
+                Err(err) => push_err(err),
+            }
+        }
+
+        for key in required_keys {
+            push_errstr_path(key, "property is missing and it is not optional");
+        }
+
+        Ok(Verifier)
+    }
+
+    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
+        let schema = match self.0 {
+            Schema::String(schema) => schema,
+            _ => return Err(E::invalid_type(Unexpected::Str(value), &self)),
+        };
+
+        let _: () = schema.check_constraints(value).map_err(E::custom)?;
+
+        Ok(Verifier)
+    }
+}
index cfebed7e54d898cfa759705c93a643ce2316e9d2..09c271bfbbafdca694c6a02f6ee9dc91e336435b 100644 (file)
@@ -19,6 +19,7 @@ pub use const_regex::ConstRegexPattern;
 
 pub mod de;
 pub mod format;
+pub mod ser;
 
 pub mod property_string;
 
index b4269448b6c83e51dd7830753f936ac1132d8ad8..bda6ed878625f469a9eb7d4974feaf9d272fd705 100644 (file)
@@ -3,9 +3,13 @@
 //! strings.
 
 use std::borrow::Cow;
+use std::fmt;
 use std::mem;
 
-use anyhow::{bail, format_err, Error};
+use serde::{Deserialize, Serialize};
+
+use crate::de::Error;
+use crate::schema::ApiType;
 
 /// Iterate over the `key=value` pairs of a property string.
 ///
@@ -26,46 +30,59 @@ impl<'a> Iterator for PropertyIterator<'a> {
     type Item = Result<(Option<&'a str>, Cow<'a, str>), Error>;
 
     fn next(&mut self) -> Option<Self::Item> {
-        if self.data.is_empty() {
-            return None;
-        }
-
-        let key = if self.data.starts_with('"') {
-            // value without key and quoted
-            None
-        } else {
-            let key = match self.data.find([',', '=']) {
-                Some(pos) if self.data.as_bytes()[pos] == b',' => None,
-                Some(pos) => Some(ascii_split_off(&mut self.data, pos)),
-                None => None,
-            };
-
-            if !self.data.starts_with('"') {
-                let value = match self.data.find(',') {
-                    Some(pos) => ascii_split_off(&mut self.data, pos),
-                    None => mem::take(&mut self.data),
-                };
-                return Some(Ok((key, Cow::Borrowed(value))));
+        Some(match next_property(self.data)? {
+            Ok((key, value, data)) => {
+                self.data = data;
+                Ok((key, value))
             }
+            Err(err) => Err(err),
+        })
+    }
+}
 
-            key
-        };
+/// Returns an optional key, its value, and the remainder of `data`.
+pub(crate) fn next_property(
+    mut data: &str,
+) -> Option<Result<(Option<&str>, Cow<str>, &str), Error>> {
+    if data.is_empty() {
+        return None;
+    }
 
-        let value = match parse_quoted_string(&mut self.data) {
-            Ok(value) => value,
-            Err(err) => return Some(Err(err)),
+    let key = if data.starts_with('"') {
+        // value without key and quoted
+        None
+    } else {
+        let key = match data.find([',', '=']) {
+            Some(pos) if data.as_bytes()[pos] == b',' => None,
+            Some(pos) => Some(ascii_split_off(&mut data, pos)),
+            None => None,
         };
 
-        if !self.data.is_empty() {
-            if self.data.starts_with(',') {
-                self.data = &self.data[1..];
-            } else {
-                return Some(Err(format_err!("garbage after quoted string")));
-            }
+        if !data.starts_with('"') {
+            let value = match data.find(',') {
+                Some(pos) => ascii_split_off(&mut data, pos),
+                None => mem::take(&mut data),
+            };
+            return Some(Ok((key, Cow::Borrowed(value), data)));
         }
 
-        Some(Ok((key, value)))
+        key
+    };
+
+    let value = match parse_quoted_string(&mut data) {
+        Ok(value) => value,
+        Err(err) => return Some(Err(err)),
+    };
+
+    if !data.is_empty() {
+        if data.starts_with(',') {
+            data = &data[1..];
+        } else {
+            return Some(Err(Error::msg("garbage after quoted string")));
+        }
     }
+
+    Some(Ok((key, value, data)))
 }
 
 impl<'a> std::iter::FusedIterator for PropertyIterator<'a> {}
@@ -83,7 +100,7 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result<Cow<'s, str>, Error>
     let data = data_out.as_bytes();
 
     if data[0] != b'"' {
-        bail!("not a quoted string");
+        return Err(Error::msg("not a quoted string"));
     }
 
     let mut i = 1;
@@ -101,7 +118,7 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result<Cow<'s, str>, Error>
     }
     if i == data.len() {
         // reached the end before reaching a quote
-        bail!("unexpected end of string");
+        return Err(Error::msg("unexpected end of string"));
     }
 
     // we're now at the first backslash, don't include it in the output:
@@ -111,7 +128,7 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result<Cow<'s, str>, Error>
     let mut was_backslash = true;
     loop {
         if i == data.len() {
-            bail!("unexpected end of string");
+            return Err(Error::msg("unexpected end of string"));
         }
 
         match (data[i], mem::replace(&mut was_backslash, false)) {
@@ -122,7 +139,7 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result<Cow<'s, str>, Error>
             (b'"', true) => out.push(b'"'),
             (b'\\', true) => out.push(b'\\'),
             (b'n', true) => out.push(b'\n'),
-            (_, true) => bail!("unsupported escape sequence"),
+            (_, true) => return Err(Error::msg("unsupported escape sequence")),
             (b'\\', false) => was_backslash = true,
             (ch, false) => out.push(ch),
         }
@@ -134,6 +151,20 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result<Cow<'s, str>, Error>
     Ok(Cow::Owned(unsafe { String::from_utf8_unchecked(out) }))
 }
 
+/// Counterpart to `parse_quoted_string`, only supporting the above-supported escape sequences.
+/// Returns `true`
+pub(crate) fn quote<T: fmt::Write>(s: &str, out: &mut T) -> fmt::Result {
+    for b in s.chars() {
+        match b {
+            '"' => out.write_str(r#"\""#)?,
+            '\\' => out.write_str(r#"\\"#)?,
+            '\n' => out.write_str(r#"\n"#)?,
+            b => out.write_char(b)?,
+        }
+    }
+    Ok(())
+}
+
 /// Like `str::split_at` but with assumes `mid` points to an ASCII character and the 2nd slice
 /// *excludes* `mid`.
 fn ascii_split_around(s: &str, mid: usize) -> (&str, &str) {
@@ -174,3 +205,258 @@ fn iterate_over_property_string() {
         .unwrap()
         .is_err());
 }
+
+/// A wrapper for a de/serializable type which is stored as a property string.
+#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, Ord, PartialOrd)]
+#[repr(transparent)]
+pub struct PropertyString<T>(T);
+
+impl<T> PropertyString<T> {
+    pub fn new(inner: T) -> Self {
+        Self(inner)
+    }
+
+    pub fn into_inner(self) -> T {
+        self.0
+    }
+}
+
+impl<T: Serialize> PropertyString<T> {
+    pub fn to_property_string(&self) -> Result<String, Error> {
+        print(&self.0)
+    }
+}
+
+impl<T> From<T> for PropertyString<T> {
+    fn from(inner: T) -> Self {
+        Self(inner)
+    }
+}
+
+impl<T> std::ops::Deref for PropertyString<T> {
+    type Target = T;
+
+    fn deref(&self) -> &T {
+        &self.0
+    }
+}
+
+impl<T> std::ops::DerefMut for PropertyString<T> {
+    fn deref_mut(&mut self) -> &mut T {
+        &mut self.0
+    }
+}
+
+impl<T> AsRef<T> for PropertyString<T> {
+    fn as_ref(&self) -> &T {
+        &self.0
+    }
+}
+
+impl<T> AsMut<T> for PropertyString<T> {
+    fn as_mut(&mut self) -> &mut T {
+        &mut self.0
+    }
+}
+
+impl<'de, T> Deserialize<'de> for PropertyString<T>
+where
+    T: Deserialize<'de> + ApiType,
+{
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use std::marker::PhantomData;
+
+        struct V<T>(PhantomData<T>);
+
+        impl<'de, T> serde::de::Visitor<'de> for V<T>
+        where
+            T: Deserialize<'de> + ApiType,
+        {
+            type Value = T;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                f.write_str("a property string")
+            }
+
+            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                self.visit_string(s.to_string())
+            }
+
+            fn visit_string<E>(self, s: String) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                T::deserialize(crate::de::SchemaDeserializer::new(s, &T::API_SCHEMA))
+                    .map_err(|err| E::custom(err.to_string()))
+            }
+
+            fn visit_borrowed_str<E>(self, s: &'de str) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                T::deserialize(crate::de::SchemaDeserializer::new(s, &T::API_SCHEMA))
+                    .map_err(|err| E::custom(err.to_string()))
+            }
+        }
+
+        deserializer.deserialize_string(V(PhantomData)).map(Self)
+    }
+}
+
+impl<T> std::str::FromStr for PropertyString<T>
+where
+    T: ApiType + for<'de> Deserialize<'de>,
+{
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        T::deserialize(crate::de::SchemaDeserializer::new(s, &T::API_SCHEMA)).map(Self)
+    }
+}
+
+impl<T> Serialize for PropertyString<T>
+where
+    T: Serialize,
+{
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        use serde::ser::Error;
+
+        serializer.serialize_str(&print(&self.0).map_err(S::Error::custom)?)
+    }
+}
+
+/// Serialize a value as a property string.
+pub fn print<T: Serialize>(value: &T) -> Result<String, Error> {
+    value.serialize(crate::ser::PropertyStringSerializer::new(String::new()))
+}
+
+/// Deserialize a value from a property string.
+pub fn parse<T: ApiType>(value: &str) -> Result<T, Error>
+where
+    T: for<'de> Deserialize<'de>,
+{
+    parse_with_schema(value, &T::API_SCHEMA)
+}
+
+/// Deserialize a value from a property string.
+pub fn parse_with_schema<T>(value: &str, schema: &'static crate::Schema) -> Result<T, Error>
+where
+    T: for<'de> Deserialize<'de>,
+{
+    T::deserialize(crate::de::SchemaDeserializer::new(value, schema))
+}
+
+#[cfg(test)]
+mod test {
+    use serde::{Deserialize, Serialize};
+
+    use crate::schema::*;
+
+    impl ApiType for Object {
+        const API_SCHEMA: Schema = ObjectSchema::new(
+            "An object",
+            &[
+                // MUST BE SORTED
+                ("count", false, &IntegerSchema::new("name").schema()),
+                ("name", false, &StringSchema::new("name").schema()),
+                ("nested", true, &Nested::API_SCHEMA),
+                (
+                    "optional",
+                    true,
+                    &BooleanSchema::new("an optional boolean").schema(),
+                ),
+            ],
+        )
+        .schema();
+    }
+
+    #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
+    pub struct Object {
+        name: String,
+        count: u32,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        optional: Option<bool>,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        nested: Option<Nested>,
+    }
+
+    impl ApiType for Nested {
+        const API_SCHEMA: Schema = ObjectSchema::new(
+            "An object",
+            &[
+                // MUST BE SORTED
+                (
+                    "count",
+                    true,
+                    &ArraySchema::new("count", &IntegerSchema::new("a value").schema()).schema(),
+                ),
+                ("name", false, &StringSchema::new("name").schema()),
+                ("third", true, &Third::API_SCHEMA),
+            ],
+        )
+        .schema();
+    }
+
+    #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
+    pub struct Nested {
+        name: String,
+
+        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+        count: Vec<u32>,
+
+        #[serde(skip_serializing_if = "Option::is_none")]
+        third: Option<Third>,
+    }
+
+    impl ApiType for Third {
+        const API_SCHEMA: Schema = ObjectSchema::new(
+            "An object",
+            &[
+                // MUST BE SORTED
+                ("count", false, &IntegerSchema::new("name").schema()),
+                ("name", false, &StringSchema::new("name").schema()),
+            ],
+        )
+        .schema();
+    }
+
+    #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
+    pub struct Third {
+        name: String,
+        count: u32,
+    }
+
+    #[test]
+    fn test() -> Result<(), super::Error> {
+        let obj = Object {
+            name: "One \"Mo\\re\" Name".to_string(),
+            count: 12,
+            optional: Some(true),
+            nested: Some(Nested {
+                name: "a \"bobby\"".to_string(),
+                count: vec![22, 23, 24],
+                third: Some(Third {
+                    name: "oh\\backslash".to_string(),
+                    count: 37,
+                }),
+            }),
+        };
+
+        let s = super::print(&obj)?;
+
+        let deserialized: Object = super::parse(&s).expect("failed to parse property string");
+
+        assert_eq!(obj, deserialized, "deserialized does not equal original");
+
+        Ok(())
+    }
+}
index a2b165c810db4a91582de54eb693cd6d110ac847..2ae58c0f42c06e6dfbd5de491836891a7cf033b5 100644 (file)
@@ -88,6 +88,10 @@ impl ParameterError {
             Err(err) => self.push(prefix.to_string(), err),
         }
     }
+
+    pub(crate) fn from_list(error_list: Vec<(String, Error)>) -> Self {
+        Self { error_list }
+    }
 }
 
 impl fmt::Display for ParameterError {
@@ -248,7 +252,7 @@ impl IntegerSchema {
         Schema::Integer(self)
     }
 
-    fn check_constraints(&self, value: isize) -> Result<(), Error> {
+    pub fn check_constraints(&self, value: isize) -> Result<(), Error> {
         if let Some(minimum) = self.minimum {
             if value < minimum {
                 bail!(
@@ -323,7 +327,7 @@ impl NumberSchema {
         Schema::Number(self)
     }
 
-    fn check_constraints(&self, value: f64) -> Result<(), Error> {
+    pub fn check_constraints(&self, value: f64) -> Result<(), Error> {
         if let Some(minimum) = self.minimum {
             if value < minimum {
                 bail!(
@@ -436,7 +440,7 @@ impl StringSchema {
         Schema::String(self)
     }
 
-    fn check_length(&self, length: usize) -> Result<(), Error> {
+    pub(crate) fn check_length(&self, length: usize) -> Result<(), Error> {
         if let Some(min_length) = self.min_length {
             if length < min_length {
                 bail!("value must be at least {} characters long", min_length);
@@ -537,7 +541,7 @@ impl ArraySchema {
         Schema::Array(self)
     }
 
-    fn check_length(&self, length: usize) -> Result<(), Error> {
+    pub(crate) fn check_length(&self, length: usize) -> Result<(), Error> {
         if let Some(min_length) = self.min_length {
             if length < min_length {
                 bail!("array must contain at least {} elements", min_length);
@@ -722,6 +726,7 @@ pub trait ObjectSchemaType {
     fn lookup(&self, key: &str) -> Option<(bool, &Schema)>;
     fn properties(&self) -> ObjectPropertyIterator;
     fn additional_properties(&self) -> bool;
+    fn default_key(&self) -> Option<&'static str>;
 
     /// Verify JSON value using an object schema.
     fn verify_json(&self, data: &Value) -> Result<(), Error> {
@@ -785,6 +790,10 @@ impl ObjectSchemaType for ObjectSchema {
     fn additional_properties(&self) -> bool {
         self.additional_properties
     }
+
+    fn default_key(&self) -> Option<&'static str> {
+        self.default_key
+    }
 }
 
 impl ObjectSchemaType for AllOfSchema {
@@ -807,6 +816,22 @@ impl ObjectSchemaType for AllOfSchema {
     fn additional_properties(&self) -> bool {
         true
     }
+
+    fn default_key(&self) -> Option<&'static str> {
+        for schema in self.list {
+            let default_key = match schema {
+                Schema::Object(schema) => schema.default_key(),
+                Schema::AllOf(schema) => schema.default_key(),
+                _ => panic!("non-object-schema in `AllOfSchema`"),
+            };
+
+            if default_key.is_some() {
+                return default_key;
+            }
+        }
+
+        None
+    }
 }
 
 #[doc(hidden)]
@@ -1246,6 +1271,13 @@ impl ObjectSchemaType for ParameterSchema {
             ParameterSchema::AllOf(o) => o.additional_properties(),
         }
     }
+
+    fn default_key(&self) -> Option<&'static str> {
+        match self {
+            ParameterSchema::Object(o) => o.default_key(),
+            ParameterSchema::AllOf(o) => o.default_key(),
+        }
+    }
 }
 
 impl From<&'static ObjectSchema> for ParameterSchema {
diff --git a/proxmox-schema/src/ser/mod.rs b/proxmox-schema/src/ser/mod.rs
new file mode 100644 (file)
index 0000000..ab81c4b
--- /dev/null
@@ -0,0 +1,636 @@
+//! Property string serialization.
+
+use std::fmt;
+use std::mem;
+
+use serde::ser::{self, Serialize, Serializer};
+
+use crate::de::Error;
+
+impl serde::ser::Error for Error {
+    fn custom<T: fmt::Display>(msg: T) -> Self {
+        Self::msg(msg.to_string())
+    }
+}
+
+pub struct PropertyStringSerializer<T> {
+    inner: T,
+}
+
+impl<T> PropertyStringSerializer<T> {
+    pub fn new(inner: T) -> Self {
+        Self { inner }
+    }
+}
+
+macro_rules! not_an_object {
+    () => {};
+    ($name:ident($ty:ty) $($rest:tt)*) => {
+        fn $name(self, _v: $ty) -> Result<Self::Ok, Error> {
+            Err(Error::msg("property string serializer used with a non-object type"))
+        }
+
+        not_an_object! { $($rest)* }
+    };
+    ($name:ident($($args:tt)*) $($rest:tt)*) => {
+        fn $name(self, $($args)*) -> Result<Self::Ok, Error> {
+            Err(Error::msg("property string serializer used with a non-object type"))
+        }
+
+        not_an_object! { $($rest)* }
+    };
+    ($name:ident<($($gen:tt)*)>($($args:tt)*) $($rest:tt)*) => {
+        fn $name<$($gen)*>(self, $($args)*) -> Result<Self::Ok, Error> {
+            Err(Error::msg("property string serializer used with a non-object type"))
+        }
+
+        not_an_object! { $($rest)* }
+    };
+}
+
+macro_rules! same_impl {
+    (as impl<T: fmt::Write> _ for $struct:ident<T> { $($code:tt)* }) => {};
+    (
+        ser::$trait:ident
+        $(ser::$more_traits:ident)*
+        as impl<T: fmt::Write> _ for $struct:ident<T> { $($code:tt)* }
+    ) => {
+        impl<T: fmt::Write> ser::$trait for $struct<T> { $($code)* }
+        same_impl! {
+            $(ser::$more_traits)*
+            as impl<T: fmt::Write> _ for $struct<T> { $($code)* }
+        }
+    }
+}
+
+impl<T: fmt::Write> Serializer for PropertyStringSerializer<T> {
+    type Ok = T;
+    type Error = Error;
+
+    type SerializeSeq = SerializeSeq<T>;
+    type SerializeTuple = SerializeSeq<T>;
+    type SerializeTupleStruct = SerializeSeq<T>;
+    type SerializeTupleVariant = SerializeSeq<T>;
+    type SerializeMap = SerializeStruct<T>;
+    type SerializeStruct = SerializeStruct<T>;
+    type SerializeStructVariant = SerializeStruct<T>;
+
+    fn is_human_readable(&self) -> bool {
+        true
+    }
+
+    not_an_object! {
+        serialize_bool(bool)
+        serialize_i8(i8)
+        serialize_i16(i16)
+        serialize_i32(i32)
+        serialize_i64(i64)
+        serialize_u8(u8)
+        serialize_u16(u16)
+        serialize_u32(u32)
+        serialize_u64(u64)
+        serialize_f32(f32)
+        serialize_f64(f64)
+        serialize_char(char)
+        serialize_str(&str)
+        serialize_bytes(&[u8])
+        serialize_none()
+        serialize_some<(V: Serialize + ?Sized)>(_value: &V)
+        serialize_unit()
+        serialize_unit_struct(&'static str)
+        serialize_unit_variant(_name: &'static str, _index: u32, _var: &'static str)
+    }
+
+    fn serialize_newtype_struct<V>(self, _name: &'static str, value: &V) -> Result<T, Error>
+    where
+        V: Serialize + ?Sized,
+    {
+        value.serialize(self)
+    }
+
+    fn serialize_newtype_variant<V>(
+        self,
+        name: &'static str,
+        _variant_index: u32,
+        _variant: &'static str,
+        _value: &V,
+    ) -> Result<T, Error>
+    where
+        V: Serialize + ?Sized,
+    {
+        Err(Error::msg(format!(
+            "cannot serialize enum {name:?} with newtype variants"
+        )))
+    }
+
+    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Error> {
+        Ok(SerializeSeq::new(self.inner))
+    }
+
+    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Error> {
+        Ok(SerializeSeq::new(self.inner))
+    }
+
+    fn serialize_tuple_struct(
+        self,
+        _name: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeTupleStruct, Error> {
+        Ok(SerializeSeq::new(self.inner))
+    }
+
+    fn serialize_tuple_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        _variant: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeTupleVariant, Error> {
+        Ok(SerializeSeq::new(self.inner))
+    }
+
+    fn serialize_struct(
+        self,
+        _name: &'static str,
+        _len: usize,
+    ) -> Result<SerializeStruct<T>, Error> {
+        Ok(SerializeStruct::new(self.inner))
+    }
+
+    fn serialize_struct_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        _variant: &'static str,
+        _len: usize,
+    ) -> Result<SerializeStruct<T>, Error> {
+        Ok(SerializeStruct::new(self.inner))
+    }
+
+    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Error> {
+        Ok(SerializeStruct::new(self.inner))
+    }
+}
+
+pub struct SerializeStruct<T> {
+    inner: Option<T>,
+    comma: bool,
+}
+
+impl<T: fmt::Write> SerializeStruct<T> {
+    fn new(inner: T) -> Self {
+        Self {
+            inner: Some(inner),
+            comma: false,
+        }
+    }
+
+    fn field<V>(&mut self, key: &'static str, value: &V) -> Result<(), Error>
+    where
+        V: Serialize + ?Sized,
+    {
+        let mut inner = self.inner.take().unwrap();
+
+        if mem::replace(&mut self.comma, true) {
+            inner.write_char(',')?;
+        }
+        write!(inner, "{key}=")?;
+        self.inner = Some(value.serialize(ElementSerializer::new(inner))?);
+        Ok(())
+    }
+
+    fn finish(mut self) -> Result<T, Error> {
+        Ok(self.inner.take().unwrap())
+    }
+}
+
+same_impl! {
+    ser::SerializeStruct
+    ser::SerializeStructVariant
+    as impl<T: fmt::Write> _ for SerializeStruct<T> {
+        type Ok = T;
+        type Error = Error;
+
+        fn serialize_field<V>(&mut self, key: &'static str, value: &V) -> Result<(), Self::Error>
+        where
+            V: Serialize + ?Sized,
+        {
+            self.field(key, value)
+        }
+
+        fn end(self) -> Result<Self::Ok, Self::Error> {
+            self.finish()
+        }
+    }
+}
+
+impl<T: fmt::Write> ser::SerializeMap for SerializeStruct<T> {
+    type Ok = T;
+    type Error = Error;
+
+    fn serialize_key<K>(&mut self, key: &K) -> Result<(), Self::Error>
+    where
+        K: Serialize + ?Sized,
+    {
+        let mut inner = self.inner.take().unwrap();
+        if mem::replace(&mut self.comma, true) {
+            inner.write_char(',')?;
+        }
+        inner = key.serialize(ElementSerializer::new(inner))?;
+        inner.write_char('=')?;
+        self.inner = Some(inner);
+        Ok(())
+    }
+
+    fn serialize_value<V>(&mut self, value: &V) -> Result<(), Self::Error>
+    where
+        V: Serialize + ?Sized,
+    {
+        let mut inner = self.inner.take().unwrap();
+        inner = value.serialize(ElementSerializer::new(inner))?;
+        self.inner = Some(inner);
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok, Self::Error> {
+        self.finish()
+    }
+}
+
+pub struct SerializeSeq<T: fmt::Write> {
+    inner: Option<T>,
+    comma: bool,
+}
+
+impl<T: fmt::Write> SerializeSeq<T> {
+    fn new(inner: T) -> Self {
+        Self {
+            inner: Some(inner),
+            comma: false,
+        }
+    }
+
+    fn element<V>(&mut self, value: &V) -> Result<(), Error>
+    where
+        V: Serialize + ?Sized,
+    {
+        let mut inner = self.inner.take().unwrap();
+        if mem::replace(&mut self.comma, true) {
+            inner.write_char(',')?;
+        }
+
+        inner = value.serialize(ElementSerializer::new(inner))?;
+        self.inner = Some(inner);
+        Ok(())
+    }
+
+    fn finish(mut self) -> Result<T, Error> {
+        Ok(self.inner.take().unwrap())
+    }
+}
+
+same_impl! {
+    ser::SerializeSeq
+    ser::SerializeTuple
+    as impl<T: fmt::Write> _ for SerializeSeq<T> {
+        type Ok = T;
+        type Error = Error;
+
+        fn serialize_element<V>(&mut self, value: &V) -> Result<(), Error>
+        where
+            V: Serialize + ?Sized,
+        {
+            self.element(value)
+        }
+
+        fn end(self) -> Result<T, Error> {
+            self.finish()
+        }
+    }
+}
+
+same_impl! {
+    ser::SerializeTupleStruct
+    ser::SerializeTupleVariant
+    as impl<T: fmt::Write> _ for SerializeSeq<T> {
+        type Ok = T;
+        type Error = Error;
+
+        fn serialize_field<V>(&mut self, value: &V) -> Result<(), Error>
+        where
+            V: Serialize + ?Sized,
+        {
+            self.element(value)
+        }
+
+        fn end(self) -> Result<T, Error> {
+            self.finish()
+        }
+    }
+}
+
+pub struct ElementSerializer<T> {
+    inner: T,
+}
+
+impl<T> ElementSerializer<T> {
+    fn new(inner: T) -> Self {
+        Self { inner }
+    }
+}
+
+impl<T: fmt::Write> ElementSerializer<T> {
+    fn serialize_with_display<V: fmt::Display>(mut self, v: V) -> Result<T, Error> {
+        write!(self.inner, "{v}")
+            .map_err(|err| Error::msg(format!("failed to write string: {err}")))?;
+        Ok(self.inner)
+    }
+}
+
+macro_rules! forward_to_display {
+    () => {};
+    ($name:ident($ty:ty) $($rest:tt)*) => {
+        fn $name(self, v: $ty) -> Result<Self::Ok, Error> {
+            self.serialize_with_display(v)
+        }
+
+        forward_to_display! { $($rest)* }
+    };
+}
+
+impl<T: fmt::Write> Serializer for ElementSerializer<T> {
+    type Ok = T;
+    type Error = Error;
+
+    type SerializeSeq = ElementSerializeSeq<T>;
+    type SerializeTuple = ElementSerializeSeq<T>;
+    type SerializeTupleStruct = ElementSerializeSeq<T>;
+    type SerializeTupleVariant = ElementSerializeSeq<T>;
+    type SerializeMap = ElementSerializeStruct<T>;
+    type SerializeStruct = ElementSerializeStruct<T>;
+    type SerializeStructVariant = ElementSerializeStruct<T>;
+
+    fn is_human_readable(&self) -> bool {
+        true
+    }
+
+    forward_to_display! {
+        serialize_bool(bool)
+        serialize_i8(i8)
+        serialize_i16(i16)
+        serialize_i32(i32)
+        serialize_i64(i64)
+        serialize_u8(u8)
+        serialize_u16(u16)
+        serialize_u32(u32)
+        serialize_u64(u64)
+        serialize_f32(f32)
+        serialize_f64(f64)
+        serialize_char(char)
+    }
+
+    fn serialize_str(mut self, v: &str) -> Result<Self::Ok, Error> {
+        if v.contains(&['"', '\\', '\n']) {
+            self.inner.write_char('"')?;
+            crate::property_string::quote(v, &mut self.inner)?;
+            self.inner.write_char('"')?;
+        } else {
+            self.inner.write_str(v)?;
+        }
+        Ok(self.inner)
+    }
+
+    fn serialize_bytes(self, _: &[u8]) -> Result<Self::Ok, Error> {
+        Err(Error::msg(
+            "raw byte value not supported in property string",
+        ))
+    }
+
+    fn serialize_none(self) -> Result<Self::Ok, Error> {
+        Err(Error::msg("tried to serialize 'None' value"))
+    }
+
+    fn serialize_some<V: Serialize + ?Sized>(self, v: &V) -> Result<Self::Ok, Error> {
+        v.serialize(self)
+    }
+
+    fn serialize_unit(self) -> Result<Self::Ok, Error> {
+        Err(Error::msg("tried to serialize a unit value"))
+    }
+
+    fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Error> {
+        Err(Error::msg(format!(
+            "tried to serialize a unit value (struct {name})"
+        )))
+    }
+
+    fn serialize_unit_variant(
+        self,
+        name: &'static str,
+        _index: u32,
+        variant: &'static str,
+    ) -> Result<Self::Ok, Error> {
+        Err(Error::msg(format!(
+            "tried to serialize a unit variant ({name}::{variant})"
+        )))
+    }
+
+    fn serialize_newtype_struct<V>(self, _name: &'static str, value: &V) -> Result<T, Error>
+    where
+        V: Serialize + ?Sized,
+    {
+        value.serialize(self)
+    }
+
+    fn serialize_newtype_variant<V>(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        _variant: &'static str,
+        value: &V,
+    ) -> Result<T, Error>
+    where
+        V: Serialize + ?Sized,
+    {
+        value.serialize(self)
+    }
+
+    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Error> {
+        Ok(ElementSerializeSeq::new(self.inner))
+    }
+
+    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Error> {
+        Ok(ElementSerializeSeq::new(self.inner))
+    }
+
+    fn serialize_tuple_struct(
+        self,
+        _name: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeTupleStruct, Error> {
+        Ok(ElementSerializeSeq::new(self.inner))
+    }
+
+    fn serialize_tuple_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        _variant: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeTupleVariant, Error> {
+        Ok(ElementSerializeSeq::new(self.inner))
+    }
+
+    fn serialize_struct(
+        self,
+        _name: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeStruct, Error> {
+        Ok(ElementSerializeStruct::new(self.inner))
+    }
+
+    fn serialize_struct_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        _variant: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeStructVariant, Error> {
+        Ok(ElementSerializeStruct::new(self.inner))
+    }
+
+    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Error> {
+        Ok(ElementSerializeStruct::new(self.inner))
+    }
+}
+
+pub struct ElementSerializeStruct<T> {
+    output: T,
+    inner: SerializeStruct<String>,
+}
+
+impl<T: fmt::Write> ElementSerializeStruct<T> {
+    fn new(inner: T) -> Self {
+        Self {
+            output: inner,
+            inner: SerializeStruct::new(String::new()),
+        }
+    }
+
+    fn finish(mut self) -> Result<T, Error> {
+        let value = self.inner.finish()?;
+        self.output.write_char('"')?;
+        crate::property_string::quote(&value, &mut self.output)?;
+        self.output.write_char('"')?;
+        Ok(self.output)
+    }
+}
+
+same_impl! {
+    ser::SerializeStruct
+    ser::SerializeStructVariant
+    as impl<T: fmt::Write> _ for ElementSerializeStruct<T> {
+        type Ok = T;
+        type Error = Error;
+
+        fn serialize_field<V>(&mut self, key: &'static str, value: &V) -> Result<(), Self::Error>
+        where
+            V: Serialize + ?Sized,
+        {
+            self.inner.field(key, value)
+        }
+
+        fn end(self) -> Result<Self::Ok, Self::Error> {
+            self.finish()
+        }
+    }
+}
+
+impl<T: fmt::Write> ser::SerializeMap for ElementSerializeStruct<T> {
+    type Ok = T;
+    type Error = Error;
+
+    fn serialize_key<K>(&mut self, key: &K) -> Result<(), Self::Error>
+    where
+        K: Serialize + ?Sized,
+    {
+        self.inner.serialize_key(key)
+    }
+
+    fn serialize_value<V>(&mut self, value: &V) -> Result<(), Self::Error>
+    where
+        V: Serialize + ?Sized,
+    {
+        self.inner.serialize_value(value)
+    }
+
+    fn end(self) -> Result<Self::Ok, Self::Error> {
+        self.finish()
+    }
+}
+
+pub struct ElementSerializeSeq<T: fmt::Write> {
+    output: T,
+    inner: SerializeSeq<String>,
+}
+
+impl<T: fmt::Write> ElementSerializeSeq<T> {
+    fn new(inner: T) -> Self {
+        Self {
+            output: inner,
+            inner: SerializeSeq::new(String::new()),
+        }
+    }
+
+    fn finish(mut self) -> Result<T, Error> {
+        let value = self.inner.finish()?;
+        if value.contains(&[',', ';', ' ', '"', '\\', '\n']) {
+            self.output.write_char('"')?;
+            crate::property_string::quote(&value, &mut self.output)?;
+            self.output.write_char('"')?;
+        } else {
+            self.output.write_str(&value)?;
+        }
+        Ok(self.output)
+    }
+}
+
+same_impl! {
+    ser::SerializeSeq
+    ser::SerializeTuple
+    as impl<T: fmt::Write> _ for ElementSerializeSeq<T> {
+        type Ok = T;
+        type Error = Error;
+
+        fn serialize_element<V>(&mut self, value: &V) -> Result<(), Error>
+        where
+            V: Serialize + ?Sized,
+        {
+            self.inner.serialize_element(value)
+        }
+
+        fn end(self) -> Result<T, Error> {
+            self.finish()
+        }
+    }
+}
+
+same_impl! {
+    ser::SerializeTupleStruct
+    ser::SerializeTupleVariant
+    as impl<T: fmt::Write> _ for ElementSerializeSeq<T> {
+        type Ok = T;
+        type Error = Error;
+
+        fn serialize_field<V>(&mut self, value: &V) -> Result<(), Error>
+        where
+            V: Serialize + ?Sized,
+        {
+            self.inner.element(value)
+        }
+
+        fn end(self) -> Result<T, Error> {
+            self.finish()
+        }
+    }
+}