use super::Md5Digest;
use std::num::NonZeroU64;
use time::OffsetDateTime;
use url::Url;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct PostList {
#[serde(rename = "@count")]
pub count: u64,
#[serde(rename = "@offset")]
pub offset: u64,
#[serde(rename = "post", default)]
pub posts: Box<[Post]>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Post {
#[serde(rename = "@height")]
pub height: NonZeroU64,
#[serde(rename = "@score")]
pub score: i64,
#[serde(rename = "@file_url")]
pub file_url: Url,
#[serde(rename = "@parent_id", with = "serde_optional_str_non_zero_u64")]
pub parent_id: Option<NonZeroU64>,
#[serde(rename = "@sample_url")]
pub sample_url: Url,
#[serde(rename = "@sample_width")]
pub sample_width: NonZeroU64,
#[serde(rename = "@sample_height")]
pub sample_height: NonZeroU64,
#[serde(rename = "@preview_url")]
pub preview_url: Url,
#[serde(rename = "@rating")]
pub rating: Rating,
#[serde(rename = "@tags")]
pub tags: Box<str>,
#[serde(rename = "@id")]
pub id: NonZeroU64,
#[serde(rename = "@width")]
pub width: NonZeroU64,
#[serde(rename = "@change", with = "time::serde::timestamp")]
pub change: OffsetDateTime,
#[serde(rename = "@md5")]
pub md5: Md5Digest,
#[serde(rename = "@creator_id")]
pub creator_id: NonZeroU64,
#[serde(rename = "@has_children")]
pub has_children: bool,
#[serde(rename = "@created_at", with = "crate::util::asctime_with_offset")]
pub created_at: OffsetDateTime,
#[serde(rename = "@status")]
pub status: PostStatus,
#[serde(rename = "@source", with = "serde_empty_str_is_none")]
pub source: Option<Box<str>>,
#[serde(rename = "@has_notes")]
pub has_notes: bool,
#[serde(rename = "@has_comments")]
pub has_comments: bool,
#[serde(rename = "@preview_width")]
pub preview_width: NonZeroU64,
#[serde(rename = "@preview_height")]
pub preview_height: NonZeroU64,
}
impl Post {
pub fn iter_tags(&self) -> impl Iterator<Item = &str> {
self.tags
.split(' ')
.map(|tag| tag.trim())
.filter(|tag| !tag.is_empty())
}
pub fn get_html_post_url(&self) -> Url {
crate::post_id_to_html_post_url(self.id)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Rating {
#[serde(rename = "q")]
Questionable,
#[serde(rename = "e")]
Explicit,
#[serde(rename = "s")]
Safe,
}
impl Rating {
pub fn as_char(self) -> char {
match self {
Self::Questionable => 'q',
Self::Explicit => 'e',
Self::Safe => 's',
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Questionable => "q",
Self::Explicit => "e",
Self::Safe => "s",
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub enum PostStatus {
#[serde(rename = "active")]
Active,
#[serde(rename = "pending")]
Pending,
#[serde(rename = "deleted")]
Deleted,
#[serde(rename = "flagged")]
Flagged,
}
mod serde_empty_str_is_none {
pub(super) fn serialize<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: AsRef<str>,
{
match value.as_ref().map(|value| value.as_ref()) {
Some(value) if value.is_empty() => serializer.serialize_none(),
Some(value) => serializer.serialize_str(value.as_ref()),
None => serializer.serialize_none(),
}
}
pub(super) fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de> + AsRef<str>,
{
let string = T::deserialize(deserializer)?;
if string.as_ref().is_empty() {
Ok(None)
} else {
Ok(Some(string))
}
}
}
mod serde_optional_str_non_zero_u64 {
use serde::de::Error;
use std::{
borrow::Cow,
num::NonZeroU64,
str::FromStr,
};
pub(super) fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: FromStr,
<T as FromStr>::Err: std::fmt::Display,
{
let data: Cow<'_, str> = serde::Deserialize::deserialize(deserializer)?;
if data.is_empty() {
return Ok(None);
}
Ok(Some(data.parse().map_err(D::Error::custom)?))
}
pub(super) fn serialize<S>(value: &Option<NonZeroU64>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match value {
Some(value) => {
let mut buffer = itoa::Buffer::new();
serializer.serialize_str(buffer.format(value.get()))
}
None => serializer.serialize_str(""),
}
}
}
#[cfg(test)]
mod test {
use super::*;
const AOKURO_XML: &str = include_str!("../../test_data/aokuro.xml");
#[test]
fn aokuro() {
let mut deserializer = quick_xml::de::Deserializer::from_str(AOKURO_XML);
let _post_list: PostList =
serde_path_to_error::deserialize(&mut deserializer).expect("failed to parse");
}
}