use serde_derive::{Deserialize, Serialize}; #[derive( Debug, Default, Clone, Serialize, Deserialize, bincode::Decode, bincode::Encode, PartialEq, )] pub struct BatchInfo { #[serde(default, skip_serializing_if = "String::is_empty")] pub filename: String, #[serde(default, skip_serializing_if = "String::is_empty")] pub description: 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 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, } #[derive( Debug, Default, Clone, Serialize, Deserialize, bincode::Decode, bincode::Encode, PartialEq, )] pub struct Question { #[serde(default, skip_serializing_if = "u32_is_zero")] pub num: u32, 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, } fn u32_is_zero(num: &u32) -> bool { *num == 0 } impl BatchInfo { pub fn is_default(&self) -> bool { *self == BatchInfo::default() } } #[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 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 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 for Vec { fn from(src: SourceQuestionsBatch) -> Self { let mut src = src; let mut questions: Vec = vec![]; std::mem::swap(&mut src.questions, &mut questions); let mut result: Vec = 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 + 'a>; } impl QuestionsConverter for T where T: Iterator)>, { fn convert<'a>(&'a mut self) -> Box + 'a> { let iter = self .filter(|(_, data)| data.is_ok()) .flat_map(|(filename, data)| { let mut batch = data.unwrap(); batch.filename = filename; let questions: Vec = 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::(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 where T: Stream)> + std::marker::Unpin, { inner: T, } impl From for QuestionsConverterAsync where T: Stream)> + std::marker::Unpin, { fn from(inner: T) -> Self { Self { inner } } } pub trait QuestionsConverterAsyncForStream where T: Stream)> + std::marker::Unpin, { fn converter(&mut self) -> QuestionsConverterAsync<&mut T>; } impl QuestionsConverterAsyncForStream for T where T: Stream)> + std::marker::Unpin, { fn converter(&mut self) -> QuestionsConverterAsync<&mut T> { QuestionsConverterAsync::from(self) } } impl QuestionsConverterAsync where T: Stream)> + std::marker::Unpin, { pub fn convert(self) -> impl Stream { 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 = 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::(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_de() { let question_from_json: Result = 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 "#); } }