diff --git a/Cargo.lock b/Cargo.lock index 90000fc..8ccc828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,9 +215,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "block-buffer" @@ -330,6 +330,7 @@ dependencies = [ "async-compression 0.4.1", "async-stream", "async_zip", + "bitflags 2.4.0", "fmmap", "futures", "futures-core", @@ -1403,7 +1404,7 @@ version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno 0.3.1", "libc", "linux-raw-sys 0.4.3", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index ad81701..1feabaf 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -47,6 +47,7 @@ zstd = { version = "^0.12", default-features = false, optional = true } memmap = { version = "0.7.0", optional = true } pin-project = { version = "1.1.3", optional = true } postcard = { version = "1.0.6", default-features = false } +bitflags = "2.4.0" [dev-dependencies] insta = { version = "1.31.0", features = ["yaml"] } diff --git a/lib/src/questions.rs b/lib/src/questions.rs index 29a0745..de96d11 100644 --- a/lib/src/questions.rs +++ b/lib/src/questions.rs @@ -1,32 +1,77 @@ use serde_derive::{Deserialize, Serialize}; +use serde::ser::SerializeStruct; -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] +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, Deserialize, PartialEq)] pub struct BatchInfo { - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub filename: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub description: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub author: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub comment: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub url: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub date: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub processed_by: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub redacted_by: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub copyright: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub theme: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub kind: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub source: String, - #[serde(default, skip_serializing_if = "String::is_empty")] + #[serde(default)] pub rating: String, } @@ -79,6 +124,56 @@ impl BatchInfo { } } +macro_rules! count_string_fields { + (($self:ident, $flags:ident, $len:ident, $FlagsType:ident) <- {$($field:ident:$flag:ident),+} ) => {$( + $len += 1; + 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(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let is_human_readable = serializer.is_human_readable(); + let mut flags: BatchFlags = Default::default(); + let mut len = 1; + + count_string_fields!((self, flags, len, BatchFlags) <- { + filename: FILENAME, description: DESCRIPTION, author: AUTHOR, comment: COMMENT, url: URL, date: DATE, + processed_by: PROCESSED, 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, copyright: COPYRIGHT, theme: THEME, kind: KIND, source: SOURCE, rating: RATING + }); + + state.end() + } +} + #[cfg(any(feature = "convert", feature = "convert_async"))] pub mod convert_common { use super::{BatchInfo, Question};