1use super::Md5Digest;
2use std::num::NonZeroU64;
3use time::OffsetDateTime;
4use url::Url;
5
6#[derive(Debug, serde::Deserialize, serde::Serialize)]
8pub struct PostList {
9 #[serde(rename = "@count")]
13 pub count: u64,
14
15 #[serde(rename = "@offset")]
17 pub offset: u64,
18
19 #[serde(rename = "post", default)]
21 pub posts: Box<[Post]>,
22}
23
24#[derive(Debug, serde::Deserialize, serde::Serialize)]
26pub struct Post {
27 #[serde(rename = "@height")]
29 pub height: NonZeroU64,
30
31 #[serde(rename = "@score")]
33 pub score: i64,
34
35 #[serde(rename = "@file_url")]
37 pub file_url: Url,
38
39 #[serde(rename = "@parent_id", with = "serde_optional_str_non_zero_u64")]
41 pub parent_id: Option<NonZeroU64>,
42
43 #[serde(rename = "@sample_url")]
45 pub sample_url: Url,
46
47 #[serde(rename = "@sample_width")]
49 pub sample_width: NonZeroU64,
50
51 #[serde(rename = "@sample_height")]
53 pub sample_height: NonZeroU64,
54
55 #[serde(rename = "@preview_url")]
57 pub preview_url: Url,
58
59 #[serde(rename = "@rating")]
61 pub rating: Rating,
62
63 #[serde(rename = "@tags")]
69 pub tags: Box<str>,
70
71 #[serde(rename = "@id")]
73 pub id: NonZeroU64,
74
75 #[serde(rename = "@width")]
77 pub width: NonZeroU64,
78
79 #[serde(rename = "@change", with = "time::serde::timestamp")]
84 pub change: OffsetDateTime,
85
86 #[serde(rename = "@md5")]
88 pub md5: Md5Digest,
89
90 #[serde(rename = "@creator_id")]
92 pub creator_id: NonZeroU64,
93
94 #[serde(rename = "@has_children")]
96 pub has_children: bool,
97
98 #[serde(rename = "@created_at", with = "crate::util::asctime_with_offset")]
100 pub created_at: OffsetDateTime,
101
102 #[serde(rename = "@status")]
104 pub status: PostStatus,
105
106 #[serde(rename = "@source", with = "serde_empty_str_is_none")]
110 pub source: Option<Box<str>>,
111
112 #[serde(rename = "@has_notes")]
114 pub has_notes: bool,
115
116 #[serde(rename = "@has_comments")]
118 pub has_comments: bool,
119
120 #[serde(rename = "@preview_width")]
122 pub preview_width: NonZeroU64,
123
124 #[serde(rename = "@preview_height")]
126 pub preview_height: NonZeroU64,
127}
128
129impl Post {
130 pub fn iter_tags(&self) -> impl Iterator<Item = &str> {
134 self.tags
135 .split(' ')
136 .map(|tag| tag.trim())
137 .filter(|tag| !tag.is_empty())
138 }
139
140 pub fn get_html_post_url(&self) -> Url {
144 crate::post_id_to_html_post_url(self.id)
145 }
146}
147
148#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
150pub enum Rating {
151 #[serde(rename = "q")]
153 Questionable,
154
155 #[serde(rename = "e")]
157 Explicit,
158
159 #[serde(rename = "s")]
161 Safe,
162}
163
164impl Rating {
165 pub fn as_char(self) -> char {
167 match self {
168 Self::Questionable => 'q',
169 Self::Explicit => 'e',
170 Self::Safe => 's',
171 }
172 }
173
174 pub fn as_str(self) -> &'static str {
176 match self {
177 Self::Questionable => "q",
178 Self::Explicit => "e",
179 Self::Safe => "s",
180 }
181 }
182}
183
184#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
186pub enum PostStatus {
187 #[serde(rename = "active")]
189 Active,
190
191 #[serde(rename = "pending")]
193 Pending,
194
195 #[serde(rename = "deleted")]
197 Deleted,
198
199 #[serde(rename = "flagged")]
201 Flagged,
202}
203
204mod serde_empty_str_is_none {
205 pub(super) fn serialize<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
206 where
207 S: serde::Serializer,
208 T: AsRef<str>,
209 {
210 match value.as_ref().map(|value| value.as_ref()) {
211 Some("") => serializer.serialize_none(),
212 Some(value) => serializer.serialize_str(value.as_ref()),
213 None => serializer.serialize_none(),
214 }
215 }
216
217 pub(super) fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
218 where
219 D: serde::Deserializer<'de>,
220 T: serde::Deserialize<'de> + AsRef<str>,
221 {
222 let string = T::deserialize(deserializer)?;
223 if string.as_ref().is_empty() {
224 Ok(None)
225 } else {
226 Ok(Some(string))
227 }
228 }
229}
230
231mod serde_optional_str_non_zero_u64 {
232 use serde::de::Error;
233 use std::{
234 borrow::Cow,
235 num::NonZeroU64,
236 str::FromStr,
237 };
238
239 pub(super) fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
240 where
241 D: serde::Deserializer<'de>,
242 T: FromStr,
243 <T as FromStr>::Err: std::fmt::Display,
244 {
245 let data: Cow<'_, str> = serde::Deserialize::deserialize(deserializer)?;
246 if data.is_empty() {
247 return Ok(None);
248 }
249
250 Ok(Some(data.parse().map_err(D::Error::custom)?))
251 }
252
253 pub(super) fn serialize<S>(value: &Option<NonZeroU64>, serializer: S) -> Result<S::Ok, S::Error>
254 where
255 S: serde::Serializer,
256 {
257 match value {
258 Some(value) => {
259 let mut buffer = itoa::Buffer::new();
260 serializer.serialize_str(buffer.format(value.get()))
261 }
262 None => serializer.serialize_str(""),
263 }
264 }
265}
266
267#[cfg(test)]
268mod test {
269 use super::*;
270
271 const AOKURO_XML: &str = include_str!("../../test_data/aokuro.xml");
272
273 #[test]
274 fn aokuro() {
275 let mut deserializer = quick_xml::de::Deserializer::from_str(AOKURO_XML);
276 let _post_list: PostList =
277 serde_path_to_error::deserialize(&mut deserializer).expect("failed to parse");
278 }
279}