use std::{ fmt::{self, Formatter}, marker::PhantomData, }; use serde::{ de::{MapAccess, SeqAccess, Visitor}, Deserialize, Deserializer, }; use serde::ser::SerializeStruct; use bitflags::bitflags; bitflags! { #[repr(transparent)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct BatchFlags: u16 { const FILENAME = 1; const DESCRIPTION = 1 << 1; const AUTHOR = 1 << 2; const COMMENT = 1 << 3; const URL = 1 << 4; const DATE = 1 << 5; const PROCESSED = 1 << 6; const REDACTED = 1 << 7; const COPYRIGHT = 1 << 8; const THEME = 1 << 9; const KIND = 1 << 10; const SOURCE = 1 << 11; const RATING = 1 << 12; } } bitflags! { #[repr(transparent)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct QuestionFlags: u16 { const NUM = 1; const AUTHOR = 1 << 1; const COMMENT = 1 << 2; const COMMENT1 = 1 << 3; const TOUR = 1 << 4; const URL = 1 << 5; const DATE = 1 << 6; const PROCESSED_BY = 1 << 7; const REDACTED_BY = 1 << 8; const COPYRIGHT = 1 << 9; const THEME = 1 << 10; const KIND = 1 << 11; const SOURCE = 1 << 12; const RATING = 1 << 13; const BATCH_INFO = 1 << 14; } } #[derive(Debug, Default, Clone, PartialEq)] pub struct BatchInfo { pub filename: String, pub description: String, pub author: String, pub comment: String, pub url: String, pub date: String, pub processed_by: String, pub redacted_by: String, pub copyright: String, pub theme: String, pub kind: String, pub source: String, pub rating: String, } #[derive(Debug, Default, Clone, PartialEq)] pub struct Question { pub num: u32, // required fields pub id: String, pub description: String, pub answer: String, pub author: String, pub comment: String, pub comment1: String, pub tour: String, pub url: String, pub date: String, pub processed_by: String, pub redacted_by: String, pub copyright: String, pub theme: String, pub kind: String, pub source: String, pub rating: String, pub batch_info: BatchInfo, } impl BatchInfo { pub fn is_default(&self) -> bool { *self == BatchInfo::default() } } macro_rules! flags_from_strings { (($self:ident, $flags:ident, $FlagsType:ident) <- {$($field:ident:$flag:ident),+} ) => {$( if !$self.$field.is_empty() { $flags |= $FlagsType::$flag; } )+} } macro_rules! serialize_fields { (($self:ident, $flags:ident, $state:ident, $FlagsType:ident) <- {$($field:ident:$flag:ident),+} ) => {$( if $flags.intersects($FlagsType::$flag) { $state.serialize_field(std::stringify!($field), &$self.$field)?; } else { $state.skip_field(std::stringify!($field))?; } )+} } impl serde::Serialize for BatchInfo { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { let is_human_readable = serializer.is_human_readable(); let mut flags: BatchFlags = Default::default(); let len = 13; 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 }); let mut state = serializer.serialize_struct("BatchInfo", len)?; if is_human_readable { state.skip_field("_flags")?; } else { state.serialize_field("_flags", &flags.bits())?; } serialize_fields!((self, flags, state, 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 }); state.end() } } impl serde::Serialize for Question { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { let is_human_readable = serializer.is_human_readable(); let mut flags: QuestionFlags = Default::default(); let len = 19; if self.num != 0 { flags |= QuestionFlags::NUM; } if !self.batch_info.is_default() { flags |= QuestionFlags::BATCH_INFO; } 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 }); let mut state = serializer.serialize_struct("Question", len)?; if is_human_readable { state.skip_field("_flags")?; } else { state.serialize_field("_flags", &flags.bits())?; } if flags.intersects(QuestionFlags::NUM) { state.serialize_field("num", &self.num)?; } else { state.skip_field("num")?; } state.serialize_field("id", &self.id)?; state.serialize_field("description", &self.description)?; state.serialize_field("answer", &self.answer)?; serialize_fields!((self, flags, state, 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, batch_info: BATCH_INFO }); if flags.intersects(QuestionFlags::BATCH_INFO) { state.serialize_field("batch_info", &self.batch_info)?; } else { state.skip_field("batch_info")?; } state.end() } } impl<'de> Deserialize<'de> for BatchInfo { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { #[allow(non_camel_case_types)] enum BatchField { filename, description, author, comment, url, date, processed_by, redacted_by, copyright, theme, kind, source, rating, _flags, __ignore, } struct BatchFieldVisitor; impl<'de> Visitor<'de> for BatchFieldVisitor { type Value = BatchField; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { Formatter::write_str(formatter, "field identifier") } fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> where E: serde::de::Error, { match value { 0u64 => Ok(BatchField::filename), 1u64 => Ok(BatchField::description), 2u64 => Ok(BatchField::author), 3u64 => Ok(BatchField::comment), 4u64 => Ok(BatchField::url), 5u64 => Ok(BatchField::date), 6u64 => Ok(BatchField::processed_by), 7u64 => Ok(BatchField::redacted_by), 8u64 => Ok(BatchField::copyright), 9u64 => Ok(BatchField::theme), 10u64 => Ok(BatchField::kind), 11u64 => Ok(BatchField::source), 12u64 => Ok(BatchField::rating), 13u64 => Ok(BatchField::_flags), _ => Ok(BatchField::__ignore), } } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: serde::de::Error, { match value { "filename" => Ok(BatchField::filename), "description" => Ok(BatchField::description), "author" => Ok(BatchField::author), "comment" => Ok(BatchField::comment), "url" => Ok(BatchField::url), "date" => Ok(BatchField::date), "processed_by" => Ok(BatchField::processed_by), "redacted_by" => Ok(BatchField::redacted_by), "copyright" => Ok(BatchField::copyright), "theme" => Ok(BatchField::theme), "kind" => Ok(BatchField::kind), "source" => Ok(BatchField::source), "rating" => Ok(BatchField::rating), "_flags" => Ok(BatchField::_flags), _ => Ok(BatchField::__ignore), } } fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E> where E: serde::de::Error, { match value { b"filename" => Ok(BatchField::filename), b"description" => Ok(BatchField::description), b"author" => Ok(BatchField::author), b"comment" => Ok(BatchField::comment), b"url" => Ok(BatchField::url), b"date" => Ok(BatchField::date), b"processed_by" => Ok(BatchField::processed_by), b"redacted_by" => Ok(BatchField::redacted_by), b"copyright" => Ok(BatchField::copyright), b"theme" => Ok(BatchField::theme), b"kind" => Ok(BatchField::kind), b"source" => Ok(BatchField::source), b"rating" => Ok(BatchField::rating), b"_flags" => Ok(BatchField::_flags), _ => Ok(BatchField::__ignore), } } } impl<'de> Deserialize<'de> for BatchField { #[inline] fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { Deserializer::deserialize_identifier(deserializer, BatchFieldVisitor) } } struct BatchInfoVisitor<'de> { marker: PhantomData<BatchInfo>, lifetime: PhantomData<&'de ()>, } impl<'de> Visitor<'de> for BatchInfoVisitor<'de> { type Value = BatchInfo; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { Formatter::write_str(formatter, "struct BatchInfo") } #[inline] fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error> where V: SeqAccess<'de>, { let flags = BatchFlags::from_bits( SeqAccess::next_element::<u16>(&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::<String>(&mut $seq)?.unwrap_or_default() } else { Default::default() }; )+} } 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 }); Ok(BatchInfo { filename, description, author, comment, url, date, processed_by, redacted_by, copyright, theme, kind, source, rating, }) } #[inline] fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> where V: MapAccess<'de>, { let mut filename: Option<String> = None; let mut description: Option<String> = None; let mut author: Option<String> = None; let mut comment: Option<String> = None; let mut url: Option<String> = None; let mut date: Option<String> = None; let mut processed_by: Option<String> = None; let mut redacted_by: Option<String> = None; let mut copyright: Option<String> = None; let mut theme: Option<String> = None; let mut kind: Option<String> = None; let mut source: Option<String> = None; let mut rating: Option<String> = None; macro_rules! match_map_fields { (($map:ident, $key:ident, $FieldType:ident) <- {$($field:ident),+} ) => { match $key { $( $FieldType::$field => { if $field.is_some() { return Err(<V::Error as serde::de::Error>::duplicate_field( std::stringify!($field), )); } $field = Some(MapAccess::next_value::<String>(&mut $map)?); }, )+ _ => { let _ = MapAccess::next_value::<serde::de::IgnoredAny>(&mut $map)?; } } } } while let Some(key) = MapAccess::next_key::<BatchField>(&mut map)? { match_map_fields!((map, key, BatchField) <- { filename, description, author, comment, url, date, processed_by, redacted_by, copyright, theme, kind, source, rating }); } let filename = filename.unwrap_or_default(); let description = description.unwrap_or_default(); let author = author.unwrap_or_default(); let comment = comment.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(); Ok(BatchInfo { filename, description, author, comment, url, date, processed_by, redacted_by, copyright, theme, kind, source, rating, }) } } const FIELDS: &[&str] = &[ "filename", "description", "author", "comment", "url", "date", "processed_by", "redacted_by", "copyright", "theme", "kind", "source", "rating", ]; Deserializer::deserialize_struct( deserializer, "BatchInfo", FIELDS, BatchInfoVisitor { marker: PhantomData::<BatchInfo>, lifetime: PhantomData, }, ) } } impl<'de> Deserialize<'de> for Question { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 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<E>(self, value: u64) -> Result<Self::Value, E> 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<E>(self, value: &str) -> Result<Self::Value, E> 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<E>(self, value: &[u8]) -> Result<Self::Value, E> 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<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { Deserializer::deserialize_identifier(deserializer, QuestionFieldVisitor) } } #[doc(hidden)] struct QuestionVisitor<'de> { marker: PhantomData<Question>, 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<V>(self, mut seq: V) -> Result<Self::Value, V::Error> where V: SeqAccess<'de>, { let _flags = QuestionFlags::from_bits( SeqAccess::next_element::<u16>(&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::<String>(&mut $seq)?.unwrap_or_default() } else { Default::default() }; )+} } let num: u32 = if _flags.intersects(QuestionFlags::NUM) { SeqAccess::next_element::<u32>(&mut seq)?.unwrap_or_default() } else { Default::default() }; let id = match SeqAccess::next_element::<String>(&mut seq)? { Some(value) => value, _ => return Err(<V::Error as serde::de::Error>::missing_field("id")), }; let description = match SeqAccess::next_element::<String>(&mut seq)? { Some(value) => value, _ => return Err(<V::Error as serde::de::Error>::missing_field("description")), }; let answer = match SeqAccess::next_element::<String>(&mut seq)? { Some(value) => value, _ => return Err(<V::Error as serde::de::Error>::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::<BatchInfo>(&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<V>(self, mut map: V) -> Result<Self::Value, V::Error> where V: MapAccess<'de>, { let mut num: Option<u32> = None; let mut id: Option<String> = None; let mut description: Option<String> = None; let mut answer: Option<String> = None; let mut author: Option<String> = None; let mut comment: Option<String> = None; let mut comment1: Option<String> = None; let mut tour: Option<String> = None; let mut url: Option<String> = None; let mut date: Option<String> = None; let mut processed_by: Option<String> = None; let mut redacted_by: Option<String> = None; let mut copyright: Option<String> = None; let mut theme: Option<String> = None; let mut kind: Option<String> = None; let mut source: Option<String> = None; let mut rating: Option<String> = None; let mut batch_info: Option<BatchInfo> = 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(<V::Error as serde::de::Error>::duplicate_field( std::stringify!($field), )); } $field = Some(MapAccess::next_value::<$FieldValueType>(&mut $map)?); }, )+ _ => { let _ = MapAccess::next_value::<serde::de::IgnoredAny>(&mut $map)?; } } } } while let Some(key) = MapAccess::next_key::<QuestionField>(&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(<V::Error as serde::de::Error>::missing_field("id")), }; let description = match description { Some(value) => value, _ => return Err(<V::Error as serde::de::Error>::missing_field("description")), }; let answer = match answer { Some(value) => value, _ => return Err(<V::Error as serde::de::Error>::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::<Question>, lifetime: PhantomData, }, ) } } #[cfg(any(feature = "convert", feature = "convert_async"))] pub mod convert_common { use super::{BatchInfo, Question}; use crate::source::{SourceQuestion, SourceQuestionsBatch}; macro_rules! make { ($Target:ident; by {$($field:ident),+}; from $src:expr) => {$Target {$( $field: $src.$field ),+}}; ($Target:ident; with defaults and by {$($field:ident),+}; from $src:expr) => {$Target {$( $field: $src.$field ),+ ,..$Target::default()}} } impl From<SourceQuestion> for Question { fn from(src: SourceQuestion) -> Self { make! {Self; with defaults and by { num, id, description, answer, author, comment, comment1, tour, url, date, processed_by, redacted_by, copyright, theme, kind, source, rating }; from src} } } impl From<SourceQuestionsBatch> for BatchInfo { fn from(src: SourceQuestionsBatch) -> Self { make! {Self; by { filename, description, author, comment, url, date, processed_by, redacted_by, copyright, theme, kind, source, rating }; from src} } } impl From<SourceQuestionsBatch> for Vec<Question> { fn from(src: SourceQuestionsBatch) -> Self { let mut src = src; let mut questions: Vec<SourceQuestion> = vec![]; std::mem::swap(&mut src.questions, &mut questions); let mut result: Vec<Question> = questions.into_iter().map(|item| item.into()).collect(); let batch_info = BatchInfo::from(src); result.iter_mut().for_each(|question| { question.batch_info = batch_info.clone(); }); result } } } #[cfg(feature = "convert")] pub mod convert { use super::Question; use crate::source::SourceQuestionsBatch; pub trait QuestionsConverter { fn convert<'a>(&'a mut self) -> Box<dyn Iterator<Item = Question> + 'a>; } impl<T> QuestionsConverter for T where T: Iterator<Item = (String, Result<SourceQuestionsBatch, serde_json::Error>)>, { fn convert<'a>(&'a mut self) -> Box<dyn Iterator<Item = Question> + 'a> { let iter = self .filter(|(_, data)| data.is_ok()) .flat_map(|(filename, data)| { let mut batch = data.unwrap(); batch.filename = filename; let questions: Vec<Question> = batch.into(); questions }); Box::new(iter) } } #[cfg(test)] mod test { use crate::questions::test::convert_common::sample_batch; use super::*; use insta::assert_yaml_snapshot; use std::iter; #[test] fn test_convert() { let mut source = iter::once(( String::from("test.json"), Ok::<SourceQuestionsBatch, serde_json::Error>(sample_batch()), )); let converted: Vec<_> = source.convert().collect(); assert_yaml_snapshot!(converted, @r#" --- - id: Вопрос 1 description: Сколько будет (2 * 2 * 2 + 2) * 2 * 2 + 2 answer: "42" batch_info: filename: test.json description: Тестовый date: 00-000-2000 - id: Вопрос 2 description: Зимой и летом одним цветом answer: ёлка batch_info: filename: test.json description: Тестовый date: 00-000-2000 "#); } } } #[cfg(feature = "convert")] pub use convert::QuestionsConverter; #[cfg(feature = "convert_async")] pub mod convert_async { use futures::stream; use futures_core::stream::Stream; use futures_util::StreamExt; use super::Question; use crate::source::SourceQuestionsBatch; pub struct QuestionsConverterAsync<T> where T: Stream<Item = (String, Result<SourceQuestionsBatch, serde_json::Error>)> + std::marker::Unpin, { inner: T, } impl<T> From<T> for QuestionsConverterAsync<T> where T: Stream<Item = (String, Result<SourceQuestionsBatch, serde_json::Error>)> + std::marker::Unpin, { fn from(inner: T) -> Self { Self { inner } } } pub trait QuestionsConverterAsyncForStream<T> where T: Stream<Item = (String, Result<SourceQuestionsBatch, serde_json::Error>)> + std::marker::Unpin, { fn converter(&mut self) -> QuestionsConverterAsync<&mut T>; } impl<T> QuestionsConverterAsyncForStream<T> for T where T: Stream<Item = (String, Result<SourceQuestionsBatch, serde_json::Error>)> + std::marker::Unpin, { fn converter(&mut self) -> QuestionsConverterAsync<&mut T> { QuestionsConverterAsync::from(self) } } impl<T> QuestionsConverterAsync<T> where T: Stream<Item = (String, Result<SourceQuestionsBatch, serde_json::Error>)> + std::marker::Unpin, { pub fn convert(self) -> impl Stream<Item = Question> { self.inner .filter_map(|(name, res)| async move { if let Ok(item) = res { Some((name, item)) } else { None } }) .flat_map(|(filename, batch)| { stream::iter({ let mut batch = batch; batch.filename = filename; let questions: Vec<Question> = batch.into(); questions }) }) } } #[cfg(test)] mod test { use crate::questions::test::convert_common::sample_batch; use super::*; use futures_util::{pin_mut, StreamExt}; use insta::assert_yaml_snapshot; #[tokio::test] async fn test_convert_stream() { let source = futures::stream::once(async { ( String::from("test.json"), Ok::<SourceQuestionsBatch, serde_json::Error>(sample_batch()), ) }); pin_mut!(source); let converter = source.converter(); let converter = converter.convert(); let converted: Vec<_> = converter.collect().await; assert_yaml_snapshot!(converted, @r#" --- - id: Вопрос 1 description: Сколько будет (2 * 2 * 2 + 2) * 2 * 2 + 2 answer: "42" batch_info: filename: test.json description: Тестовый date: 00-000-2000 - id: Вопрос 2 description: Зимой и летом одним цветом answer: ёлка batch_info: filename: test.json description: Тестовый date: 00-000-2000 "#); } } } #[cfg(feature = "convert_async")] pub use convert_async::{QuestionsConverterAsync, QuestionsConverterAsyncForStream}; #[cfg(test)] mod test { use super::*; use insta::assert_yaml_snapshot; use serde_json::json; #[cfg(any(feature = "convert", feature = "convert_async"))] pub mod convert_common { use crate::source::{SourceQuestion, SourceQuestionsBatch}; pub fn sample_batch() -> SourceQuestionsBatch { SourceQuestionsBatch { description: "Тестовый".into(), date: "00-000-2000".into(), questions: vec![ SourceQuestion { id: "Вопрос 1".into(), description: "Сколько будет (2 * 2 * 2 + 2) * 2 * 2 + 2".into(), answer: "42".into(), ..Default::default() }, SourceQuestion { id: "Вопрос 2".into(), description: "Зимой и летом одним цветом".into(), answer: "ёлка".into(), ..Default::default() }, ], ..Default::default() } } } pub fn sample_question() -> Question { Question { id: "Вопрос 1".into(), description: "Сколько будет (2 * 2 * 2 + 2) * 2 * 2 + 2".into(), answer: "42".into(), batch_info: BatchInfo { description: "Тестовый".into(), date: "00-000-2000".into(), ..Default::default() }, ..Default::default() } } #[test] fn test_question_ser() { assert_yaml_snapshot!(sample_question(), @r#" --- id: Вопрос 1 description: Сколько будет (2 * 2 * 2 + 2) * 2 * 2 + 2 answer: "42" batch_info: description: Тестовый date: 00-000-2000 "#); } #[test] fn test_question_serde_postcard() { use postcard::{from_bytes, to_slice}; let original = sample_question(); let mut buff: Vec<u8> = 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<Question, _> = serde_json::from_value(json!({ "id": "Вопрос 1", "description": "Сколько будет (2 * 2 * 2 + 2) * 2 * 2 + 2", "answer": "42", "batch_info": { "description": "Тестовый", "date": "00-000-2000" } })); assert!(question_from_json.is_ok()); assert_yaml_snapshot!(question_from_json.unwrap(), @r#" --- id: Вопрос 1 description: Сколько будет (2 * 2 * 2 + 2) * 2 * 2 + 2 answer: "42" batch_info: description: Тестовый date: 00-000-2000 "#); } }