From 069a6e22fef3d23cd83eb6918f55d2fbfb1cc491 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 24 Aug 2023 17:11:11 +0300 Subject: [PATCH] add impl Deserializer for Question + postcard test works with optional(default/skiped) fileds by conditional seq deserialization based on flags check --- lib/src/questions.rs | 398 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 368 insertions(+), 30 deletions(-) diff --git a/lib/src/questions.rs b/lib/src/questions.rs index 7eb481f..e0f83ee 100644 --- a/lib/src/questions.rs +++ b/lib/src/questions.rs @@ -71,42 +71,28 @@ pub struct BatchInfo { pub rating: String, } -#[derive(Debug, Default, Clone, Deserialize, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Question { - #[serde(default, skip_serializing_if = "u32_is_zero")] pub num: u32, - pub id: String, + // required fields + pub id: String, pub description: String, pub answer: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub author: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub comment: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub comment1: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub tour: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub url: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub date: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub processed_by: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub redacted_by: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub copyright: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub theme: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub kind: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub source: String, - #[serde(default, skip_serializing_if = "String::is_empty")] pub rating: String, - #[serde(default, skip_serializing_if = "BatchInfo::is_default")] pub batch_info: BatchInfo, } @@ -116,9 +102,8 @@ impl BatchInfo { } } -macro_rules! count_string_fields { - (($self:ident, $flags:ident, $len:ident, $FlagsType:ident) <- {$($field:ident:$flag:ident),+} ) => {$( - $len += 1; +macro_rules! flags_from_strings { + (($self:ident, $flags:ident, $FlagsType:ident) <- {$($field:ident:$flag:ident),+} ) => {$( if !$self.$field.is_empty() { $flags |= $FlagsType::$flag; } @@ -142,9 +127,9 @@ impl serde::Serialize for BatchInfo { { let is_human_readable = serializer.is_human_readable(); let mut flags: BatchFlags = Default::default(); - let mut len = 1; // (+flags) + let len = 13; - count_string_fields!((self, flags, len, BatchFlags) <- { + flags_from_strings!((self, flags, BatchFlags) <- { filename: FILENAME, description: DESCRIPTION, author: AUTHOR, comment: COMMENT, url: URL, date: DATE, processed_by: PROCESSED, redacted_by: REDACTED, copyright: COPYRIGHT, theme: THEME, kind: KIND, source: SOURCE, rating: RATING }); @@ -173,7 +158,7 @@ impl serde::Serialize for Question { { let is_human_readable = serializer.is_human_readable(); let mut flags: QuestionFlags = Default::default(); - let mut len = 6; //(_flags + id + description + answer + num + batch_info) + let len = 19; if self.num != 0 { flags |= QuestionFlags::NUM; @@ -183,7 +168,7 @@ impl serde::Serialize for Question { flags |= QuestionFlags::BATCH_INFO; } - count_string_fields!((self, flags, len, QuestionFlags) <- { + flags_from_strings!((self, flags, QuestionFlags) <- { author: AUTHOR, comment: COMMENT, comment1: COMMENT1, tour: TOUR, url: URL, date: DATE, processed_by: PROCESSED_BY, redacted_by: REDACTED_BY, copyright: COPYRIGHT, theme: THEME, kind: KIND, source: SOURCE, rating: RATING @@ -229,7 +214,6 @@ impl<'de> Deserialize<'de> for BatchInfo { D: Deserializer<'de>, { #[allow(non_camel_case_types)] - #[doc(hidden)] enum BatchField { filename, description, @@ -330,7 +314,6 @@ impl<'de> Deserialize<'de> for BatchInfo { Deserializer::deserialize_identifier(deserializer, BatchFieldVisitor) } } - #[doc(hidden)] struct BatchInfoVisitor<'de> { marker: PhantomData, lifetime: PhantomData<&'de ()>, @@ -345,7 +328,7 @@ impl<'de> Deserialize<'de> for BatchInfo { where V: SeqAccess<'de>, { - let _flags = BatchFlags::from_bits( + let flags = BatchFlags::from_bits( SeqAccess::next_element::(&mut seq)?.unwrap_or_default(), ) .unwrap_or_default(); @@ -353,14 +336,14 @@ impl<'de> Deserialize<'de> for BatchInfo { macro_rules! seq_read_strings { (($flags:ident, $seq:ident, $FlagsType:ident) <- {$($field:ident:$flag:ident),+} ) => {$( let $field: String = if $flags.intersects($FlagsType::$flag) { - SeqAccess::next_element::(&mut seq)?.unwrap_or_default() + SeqAccess::next_element::(&mut $seq)?.unwrap_or_default() } else { Default::default() }; )+} } - seq_read_strings!((_flags, seq, BatchFlags) <- { + seq_read_strings!((flags, seq, BatchFlags) <- { filename: FILENAME, description: DESCRIPTION, author: AUTHOR, comment: COMMENT, url: URL, date: DATE, processed_by: PROCESSED, redacted_by: REDACTED, copyright: COPYRIGHT, theme: THEME, kind: KIND, source: SOURCE, rating: RATING }); @@ -458,7 +441,6 @@ impl<'de> Deserialize<'de> for BatchInfo { }) } } - #[doc(hidden)] const FIELDS: &[&str] = &[ "filename", "description", @@ -486,6 +468,350 @@ impl<'de> Deserialize<'de> for BatchInfo { } } +impl<'de> Deserialize<'de> for Question { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[allow(non_camel_case_types)] + enum QuestionField { + num, + id, + description, + answer, + author, + comment, + comment1, + tour, + url, + date, + processed_by, + redacted_by, + copyright, + theme, + kind, + source, + rating, + batch_info, + _flags, + __ignore, + } + + struct QuestionFieldVisitor; + impl<'de> Visitor<'de> for QuestionFieldVisitor { + type Value = QuestionField; + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + Formatter::write_str(formatter, "field identifier") + } + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + match value { + 0u64 => Ok(QuestionField::num), + 1u64 => Ok(QuestionField::id), + 2u64 => Ok(QuestionField::description), + 3u64 => Ok(QuestionField::answer), + 4u64 => Ok(QuestionField::author), + 5u64 => Ok(QuestionField::comment), + 6u64 => Ok(QuestionField::comment1), + 7u64 => Ok(QuestionField::tour), + 8u64 => Ok(QuestionField::url), + 9u64 => Ok(QuestionField::date), + 10u64 => Ok(QuestionField::processed_by), + 11u64 => Ok(QuestionField::redacted_by), + 12u64 => Ok(QuestionField::copyright), + 13u64 => Ok(QuestionField::theme), + 14u64 => Ok(QuestionField::kind), + 15u64 => Ok(QuestionField::source), + 16u64 => Ok(QuestionField::rating), + 17u64 => Ok(QuestionField::batch_info), + 18u64 => Ok(QuestionField::_flags), + _ => Ok(QuestionField::__ignore), + } + } + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + match value { + "num" => Ok(QuestionField::num), + "id" => Ok(QuestionField::id), + "description" => Ok(QuestionField::description), + "answer" => Ok(QuestionField::answer), + "author" => Ok(QuestionField::author), + "comment" => Ok(QuestionField::comment), + "comment1" => Ok(QuestionField::comment1), + "tour" => Ok(QuestionField::tour), + "url" => Ok(QuestionField::url), + "date" => Ok(QuestionField::date), + "processed_by" => Ok(QuestionField::processed_by), + "redacted_by" => Ok(QuestionField::redacted_by), + "copyright" => Ok(QuestionField::copyright), + "theme" => Ok(QuestionField::theme), + "kind" => Ok(QuestionField::kind), + "source" => Ok(QuestionField::source), + "rating" => Ok(QuestionField::rating), + "batch_info" => Ok(QuestionField::batch_info), + "_flags" => Ok(QuestionField::_flags), + _ => Ok(QuestionField::__ignore), + } + } + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + match value { + b"num" => Ok(QuestionField::num), + b"id" => Ok(QuestionField::id), + b"description" => Ok(QuestionField::description), + b"answer" => Ok(QuestionField::answer), + b"author" => Ok(QuestionField::author), + b"comment" => Ok(QuestionField::comment), + b"comment1" => Ok(QuestionField::comment1), + b"tour" => Ok(QuestionField::tour), + b"url" => Ok(QuestionField::url), + b"date" => Ok(QuestionField::date), + b"processed_by" => Ok(QuestionField::processed_by), + b"redacted_by" => Ok(QuestionField::redacted_by), + b"copyright" => Ok(QuestionField::copyright), + b"theme" => Ok(QuestionField::theme), + b"kind" => Ok(QuestionField::kind), + b"source" => Ok(QuestionField::source), + b"rating" => Ok(QuestionField::rating), + b"batch_info" => Ok(QuestionField::batch_info), + b"_flags" => Ok(QuestionField::_flags), + _ => Ok(QuestionField::__ignore), + } + } + } + impl<'de> Deserialize<'de> for QuestionField { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Deserializer::deserialize_identifier(deserializer, QuestionFieldVisitor) + } + } + #[doc(hidden)] + struct QuestionVisitor<'de> { + marker: PhantomData, + lifetime: PhantomData<&'de ()>, + } + impl<'de> Visitor<'de> for QuestionVisitor<'de> { + type Value = Question; + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + Formatter::write_str(formatter, "struct Question") + } + #[inline] + fn visit_seq(self, mut seq: V) -> Result + where + V: SeqAccess<'de>, + { + let _flags = QuestionFlags::from_bits( + SeqAccess::next_element::(&mut seq)?.unwrap_or_default(), + ) + .unwrap_or_default(); + + macro_rules! seq_read_strings { + (($flags:ident, $seq:ident, $FlagsType:ident) <- {$($field:ident:$flag:ident),+} ) => {$( + let $field: String = if $flags.intersects($FlagsType::$flag) { + SeqAccess::next_element::(&mut $seq)?.unwrap_or_default() + } else { + Default::default() + }; + )+} + } + + let num: u32 = if _flags.intersects(QuestionFlags::NUM) { + SeqAccess::next_element::(&mut seq)?.unwrap_or_default() + } else { + Default::default() + }; + + let id = match SeqAccess::next_element::(&mut seq)? { + Some(value) => value, + _ => return Err(::missing_field("id")), + }; + let description = match SeqAccess::next_element::(&mut seq)? { + Some(value) => value, + _ => return Err(::missing_field("description")), + }; + let answer = match SeqAccess::next_element::(&mut seq)? { + Some(value) => value, + _ => return Err(::missing_field("answer")), + }; + + seq_read_strings!((_flags, seq, QuestionFlags) <- { + author: AUTHOR, comment: COMMENT, comment1: COMMENT1, tour: TOUR, url: URL, date: DATE, + processed_by: PROCESSED_BY, redacted_by: REDACTED_BY, copyright: COPYRIGHT, theme: THEME, + kind: KIND, source: SOURCE, rating: RATING + }); + + let batch_info: BatchInfo = if _flags.intersects(QuestionFlags::BATCH_INFO) { + SeqAccess::next_element::(&mut seq)?.unwrap_or_default() + } else { + Default::default() + }; + + Ok(Question { + num, + id, + description, + answer, + author, + comment, + comment1, + tour, + url, + date, + processed_by, + redacted_by, + copyright, + theme, + kind, + source, + rating, + batch_info, + }) + } + #[inline] + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut num: Option = None; + let mut id: Option = None; + let mut description: Option = None; + let mut answer: Option = None; + let mut author: Option = None; + let mut comment: Option = None; + let mut comment1: Option = None; + let mut tour: Option = None; + let mut url: Option = None; + let mut date: Option = None; + let mut processed_by: Option = None; + let mut redacted_by: Option = None; + let mut copyright: Option = None; + let mut theme: Option = None; + let mut kind: Option = None; + let mut source: Option = None; + let mut rating: Option = None; + let mut batch_info: Option = None; + + macro_rules! match_map_fields { + (($map:ident, $key:ident, $FieldType:ident) <- {$($field:ident:$FieldValueType:ident),+} ) => { + match $key { + $( + $FieldType::$field => { + if $field.is_some() { + return Err(::duplicate_field( + std::stringify!($field), + )); + } + $field = Some(MapAccess::next_value::<$FieldValueType>(&mut $map)?); + }, + )+ + _ => { + let _ = MapAccess::next_value::(&mut $map)?; + } + } + } + } + + while let Some(key) = MapAccess::next_key::(&mut map)? { + match_map_fields!((map, key, QuestionField) <- { + num: u32, id: String, description: String, answer: String, author: String, comment: String, + comment1: String, tour: String, url: String, date: String, processed_by: String, redacted_by: String, + copyright: String, theme: String, kind: String, source: String, rating: String, batch_info: BatchInfo + }); + } + + let id = match id { + Some(value) => value, + _ => return Err(::missing_field("id")), + }; + let description = match description { + Some(value) => value, + _ => return Err(::missing_field("description")), + }; + let answer = match answer { + Some(value) => value, + _ => return Err(::missing_field("answer")), + }; + + let num = num.unwrap_or_default(); + let author = author.unwrap_or_default(); + let comment = comment.unwrap_or_default(); + let comment1 = comment1.unwrap_or_default(); + let tour = tour.unwrap_or_default(); + let url = url.unwrap_or_default(); + let date = date.unwrap_or_default(); + let processed_by = processed_by.unwrap_or_default(); + let redacted_by = redacted_by.unwrap_or_default(); + let copyright = copyright.unwrap_or_default(); + let theme = theme.unwrap_or_default(); + let kind = kind.unwrap_or_default(); + let source = source.unwrap_or_default(); + let rating = rating.unwrap_or_default(); + let batch_info = batch_info.unwrap_or_default(); + + Ok(Question { + num, + id, + description, + answer, + author, + comment, + comment1, + tour, + url, + date, + processed_by, + redacted_by, + copyright, + theme, + kind, + source, + rating, + batch_info, + }) + } + } + #[doc(hidden)] + const FIELDS: &[&str] = &[ + "num", + "id", + "description", + "answer", + "author", + "comment", + "comment1", + "tour", + "url", + "date", + "processed_by", + "redacted_by", + "copyright", + "theme", + "kind", + "source", + "rating", + "batch_info", + ]; + Deserializer::deserialize_struct( + deserializer, + "Question", + FIELDS, + QuestionVisitor { + marker: PhantomData::, + lifetime: PhantomData, + }, + ) + } +} + #[cfg(any(feature = "convert", feature = "convert_async"))] pub mod convert_common { use super::{BatchInfo, Question}; @@ -775,6 +1101,18 @@ mod test { "#); } #[test] + fn test_question_serde_postcard() { + use postcard::{from_bytes, to_slice}; + + let original = sample_question(); + let mut buff: Vec = vec![0; 135]; + + to_slice(&original, buff.as_mut_slice()).expect("to_slice"); + let actual: Question = from_bytes(buff.as_slice()).expect("from_bytes"); + + assert_eq!(actual, original); + } + #[test] fn test_question_de() { let question_from_json: Result = serde_json::from_value(json!({ "id": "Вопрос 1",