From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 3FAD261D20 for ; Fri, 18 Dec 2020 12:37:36 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3BFBA2F857 for ; Fri, 18 Dec 2020 12:37:36 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 981612F7FB for ; Fri, 18 Dec 2020 12:37:32 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id CA5B045430 for ; Fri, 18 Dec 2020 12:26:28 +0100 (CET) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Fri, 18 Dec 2020 12:25:58 +0100 Message-Id: <20201218112608.6845-11-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201218112608.6845-1-w.bumiller@proxmox.com> References: <20201218112608.6845-1-w.bumiller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.017 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [de.rs, mod.rs] Subject: [pbs-devel] [PATCH proxmox 10/18] schema: ExtractValueDeserializer X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 18 Dec 2020 11:37:36 -0000 A deserializer which takes an `&mut Value` and an object schema reference and deserializes by extracting (removing) the values from the references serde Value. Signed-off-by: Wolfgang Bumiller --- proxmox/src/api/de.rs | 270 +++++++++++++++++++++++++++++++++++++++++ proxmox/src/api/mod.rs | 2 + 2 files changed, 272 insertions(+) create mode 100644 proxmox/src/api/de.rs diff --git a/proxmox/src/api/de.rs b/proxmox/src/api/de.rs new file mode 100644 index 0000000..b48fd85 --- /dev/null +++ b/proxmox/src/api/de.rs @@ -0,0 +1,270 @@ +//! 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::api::schema::{ObjectSchema, ObjectSchemaType, Schema}; + +pub struct Error { + inner: anyhow::Error, +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.inner, f) + } +} + +impl std::error::Error for Error {} + +impl serde::de::Error for Error { + fn custom(msg: T) -> Self { + Self { + inner: anyhow::format_err!("{}", msg), + } + } +} + +impl From for Error { + fn from(inner: serde_json::Error) -> Self { + Error { + inner: inner.into(), + } + } +} + +pub struct ExtractValueDeserializer<'o> { + object: &'o mut serde_json::Map, + schema: &'static ObjectSchema, +} + +impl<'o> ExtractValueDeserializer<'o> { + pub fn try_new( + object: &'o mut serde_json::Map, + schema: &'static Schema, + ) -> Option { + match schema { + Schema::Object(schema) => Some(Self { object, schema }), + _ => None, + } + } +} + +macro_rules! deserialize_non_object { + ($name:ident) => { + fn $name(self, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::Error::custom( + "deserializing partial object into type which is not an object", + )) + } + }; + ($name:ident ( $($args:tt)* )) => { + fn $name(self, $($args)*, _visitor: V) -> Result + 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(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_map(visitor) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_map(MapAccess::<'de>::new( + self.object, + self.schema.properties().map(|(name, _, _)| *name), + )) + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_map(MapAccess::<'de>::new( + self.object, + self.schema.properties().map(|(name, _, _)| *name), + )) + } + + 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(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } +} + +struct MapAccess<'o, I> { + object: &'o mut serde_json::Map, + iter: I, + value: Option, +} + +impl<'o, I> MapAccess<'o, I> +where + I: Iterator, +{ + fn new(object: &'o mut serde_json::Map, iter: I) -> Self { + Self { + object, + iter, + value: None, + } + } +} + +impl<'de, I> de::MapAccess<'de> for MapAccess<'de, I> +where + I: Iterator, +{ + type Error = Error; + + fn next_key_seed(&mut self, seed: K) -> Result, 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(&mut self, seed: V) -> Result + 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] +fn test_extraction() { + use serde::Deserialize; + + use crate::api::schema::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/src/api/mod.rs b/proxmox/src/api/mod.rs index b0b8333..8c6f597 100644 --- a/proxmox/src/api/mod.rs +++ b/proxmox/src/api/mod.rs @@ -48,3 +48,5 @@ pub use router::{ #[cfg(feature = "cli")] pub mod cli; + +pub mod de; -- 2.20.1