use std::io;
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use serde_xml_rs::SerdeXml;
use crate::comicinfo::{
age_rating::AgeRating, comic_page_info::ComicPageInfo, manga::Manga, rating::Rating,
yes_no::YesNo,
};
pub mod age_rating;
pub mod comic_page_info;
pub mod comic_page_type;
pub mod manga;
pub mod rating;
pub mod yes_no;
pub const COMIC_INFO_XML: &str = "ComicInfo.xml";
#[derive(Debug, Builder, Clone, PartialEq, Serialize, Deserialize, Default)]
#[non_exhaustive]
#[serde(rename_all = "PascalCase", default)]
#[builder(setter(strip_option), default, derive(Debug))]
pub struct ComicInfo {
pub title: Option<String>,
pub series: Option<String>,
pub number: Option<String>,
pub count: Option<usize>,
pub volume: Option<usize>,
pub alternate_series: Option<String>,
pub alternate_number: Option<String>,
pub alternate_count: Option<usize>,
pub summary: Option<String>,
pub notes: Option<String>,
pub year: Option<usize>,
pub month: Option<usize>,
pub day: Option<usize>,
pub writer: Option<String>,
pub penciller: Option<String>,
pub inker: Option<String>,
pub colorist: Option<String>,
pub letterer: Option<String>,
pub cover_artist: Option<String>,
pub editor: Option<String>,
pub translator: Option<String>,
pub publisher: Option<String>,
pub imprint: Option<String>,
pub genre: Option<String>,
pub tags: Option<String>,
pub web: Option<url::Url>,
pub page_count: Option<usize>,
#[serde(rename = "LanguageISO")]
pub language_iso: Option<String>,
pub format: Option<String>,
#[serde(with = "serde_from_to_str_enum")]
pub black_and_white: Option<YesNo>,
#[serde(with = "serde_from_to_str_enum")]
pub manga: Option<Manga>,
pub characters: Option<String>,
pub teams: Option<String>,
pub locations: Option<String>,
pub scan_information: Option<String>,
pub story_arc: Option<String>,
pub story_arc_number: Option<String>,
pub series_group: Option<String>,
#[serde(with = "serde_from_to_str_enum")]
pub age_rating: Option<AgeRating>,
#[builder(setter(each = "add_page"))]
#[serde(with = "serde_pages", skip_serializing_if = "Vec::is_empty")]
pub pages: Vec<ComicPageInfo>,
pub community_rating: Option<Rating>,
pub main_character_or_team: Option<String>,
pub review: Option<String>,
#[serde(rename = "GTIN")]
pub gtin: Option<String>,
}
impl ComicInfo {
pub fn to_xml_string(&self) -> Result<String, serde_xml_rs::Error> {
get_comicinfo_serde_xml().to_string(self)
}
pub fn to_xml_writer<W>(&self, writer: W) -> Result<(), serde_xml_rs::Error>
where
W: io::Write,
{
get_comicinfo_serde_xml().to_writer(writer, self)
}
}
pub fn default_xsi() -> &'static str {
"http://www.w3.org/2001/XMLSchema-instance"
}
pub fn default_xsd() -> &'static str {
"http://www.w3.org/2001/XMLSchema"
}
pub fn get_comicinfo_serde_xml() -> SerdeXml {
SerdeXml::new()
.namespace("xsi", default_xsi())
.namespace("xsd", default_xsd())
}
mod serde_from_to_str_enum {
use std::borrow::{Borrow, Cow};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<V, S>(value: &Option<V>, serializer: S) -> Result<S::Ok, S::Error>
where
V: ToString,
S: Serializer,
{
value.as_ref().map(|v| v.to_string()).serialize(serializer)
}
pub fn deserialize<'de, V, D>(deserializer: D) -> Result<Option<V>, D::Error>
where
D: Deserializer<'de>,
V: for<'a> From<&'a str>,
{
let str_: Option<Cow<'de, str>> = Deserialize::<'de>::deserialize(deserializer)?;
match str_ {
Some(str_) => {
let inner_str = Borrow::<str>::borrow(&str_);
Ok(Some(inner_str.into()))
}
None => Ok(None),
}
}
}
mod serde_pages {
use std::borrow::Cow;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::comicinfo::comic_page_info::ComicPageInfo;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Pages<'a> {
page: Cow<'a, [ComicPageInfo]>,
}
pub fn serialize<S>(pages: &[ComicPageInfo], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Pages {
page: Cow::Borrowed(pages),
}
.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<ComicPageInfo>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Pages::deserialize(deserializer)?.page.into_owned())
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, num::NonZeroUsize};
use super::*;
#[test]
fn test_black_and_white() -> anyhow::Result<()> {
let val = ComicInfoBuilder::create_empty()
.black_and_white(YesNo::Yes)
.build()?;
let val_xml = get_comicinfo_serde_xml().to_string(&val)?;
assert_eq!(
val_xml.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", ""),
format!(
"<ComicInfo xmlns:xsd=\"{}\" xmlns:xsi=\"{}\"><BlackAndWhite>Yes</BlackAndWhite></ComicInfo>",
default_xsd(),
default_xsi()
)
);
Ok(())
}
#[test]
fn test_age_rating() -> anyhow::Result<()> {
let val = ComicInfoBuilder::create_empty()
.age_rating(AgeRating::Teen)
.build()?;
let val_xml = get_comicinfo_serde_xml().to_string(&val)?;
assert_eq!(
val_xml.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", ""),
format!(
"<ComicInfo xmlns:xsd=\"{}\" xmlns:xsi=\"{}\"><AgeRating>Teen</AgeRating></ComicInfo>",
default_xsd(),
default_xsi()
)
);
Ok(())
}
#[test]
fn test_pages() -> anyhow::Result<()> {
let val = ComicInfoBuilder::create_empty()
.add_page(ComicPageInfo::builder().image(1).build()?)
.build()?;
let val_xml = get_comicinfo_serde_xml().to_string(&val)?;
assert_eq!(
val_xml.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", ""),
format!(
"<ComicInfo xmlns:xsd=\"{}\" xmlns:xsi=\"{}\"><Pages><Page Image=\"1\" Type=\"Story\" DoublePage=\"false\" ImageSize=\"0\" Key=\"\" Bookmark=\"\" /></Pages></ComicInfo>",
default_xsd(),
default_xsi()
)
);
Ok(())
}
#[test]
fn test_deserialize() -> anyhow::Result<()> {
let xml_val: ComicInfo = {
let mut file = BufReader::new(File::open("test-data/xml/TestComicInfo1.xml")?);
serde_xml_rs::from_reader(&mut file)?
};
assert!(xml_val.title.is_none());
assert_eq!(
xml_val.series.unwrap().trim(),
"Nonderi Kubo wa Genjitsu no Koi ga Shiritai"
);
assert_eq!(xml_val.number.unwrap().trim(), "1");
assert_eq!(
xml_val.summary.unwrap().trim(),
"Kubo-kun tries to learn about romance for the sake of writing light novels, but he turns out to be the most tactless and clueless person ever!?"
);
assert_eq!(xml_val.language_iso.unwrap().trim(), "en");
assert_eq!(xml_val.age_rating.unwrap(), AgeRating::Teen);
assert!(xml_val.alternate_count.is_none());
assert_eq!(xml_val.pages.len(), 3);
assert_eq!(
xml_val.pages[0],
ComicPageInfo::builder()
.image(1)
.image_width(NonZeroUsize::new(800))
.image_height(NonZeroUsize::new(1300))
.build()?
);
assert_eq!(
xml_val.pages[1],
ComicPageInfo::builder()
.image(2)
.double_page(true)
.image_width(NonZeroUsize::new(1600))
.image_height(NonZeroUsize::new(900))
.build()?
);
assert_eq!(
xml_val.pages[2],
ComicPageInfo::builder()
.image(3)
.image_width(NonZeroUsize::new(800))
.image_height(NonZeroUsize::new(1300))
.build()?
);
Ok(())
}
}