rule34/types/
post_list.rs

1use super::Md5Digest;
2use std::num::NonZeroU64;
3use time::OffsetDateTime;
4use url::Url;
5
6/// A list of posts
7#[derive(Debug, serde::Deserialize, serde::Serialize)]
8pub struct PostList {
9    /// The # of posts.
10    ///
11    /// This is the total # of posts, not the # in this list.
12    #[serde(rename = "@count")]
13    pub count: u64,
14
15    /// The current offset
16    #[serde(rename = "@offset")]
17    pub offset: u64,
18
19    /// The posts
20    #[serde(rename = "post", default)]
21    pub posts: Box<[Post]>,
22}
23
24/// A Post
25#[derive(Debug, serde::Deserialize, serde::Serialize)]
26pub struct Post {
27    /// The height of the original file.
28    #[serde(rename = "@height")]
29    pub height: NonZeroU64,
30
31    /// The number of up-votes minus the number of down-votes.
32    #[serde(rename = "@score")]
33    pub score: i64,
34
35    /// The main file url
36    #[serde(rename = "@file_url")]
37    pub file_url: Url,
38
39    /// The parent post id
40    #[serde(rename = "@parent_id", with = "serde_optional_str_non_zero_u64")]
41    pub parent_id: Option<NonZeroU64>,
42
43    /// The sample url
44    #[serde(rename = "@sample_url")]
45    pub sample_url: Url,
46
47    /// The sample width
48    #[serde(rename = "@sample_width")]
49    pub sample_width: NonZeroU64,
50
51    /// The sample height
52    #[serde(rename = "@sample_height")]
53    pub sample_height: NonZeroU64,
54
55    /// The preview url
56    #[serde(rename = "@preview_url")]
57    pub preview_url: Url,
58
59    /// The image rating
60    #[serde(rename = "@rating")]
61    pub rating: Rating,
62
63    /// A list of tag names.
64    ///
65    /// Tag names are separated by one or more spaces.
66    /// There may or may not be a leading or trailing space.
67    /// Tag names are always lowercase.
68    #[serde(rename = "@tags")]
69    pub tags: Box<str>,
70
71    /// The id the post
72    #[serde(rename = "@id")]
73    pub id: NonZeroU64,
74
75    /// image width
76    #[serde(rename = "@width")]
77    pub width: NonZeroU64,
78
79    /// The time of the last change.
80    ///
81    /// This tracks at least the date of posting and tag edits.
82    /// This is a unix timestamp.
83    #[serde(rename = "@change", with = "time::serde::timestamp")]
84    pub change: OffsetDateTime,
85
86    /// The md5 hash of the file.
87    #[serde(rename = "@md5")]
88    pub md5: Md5Digest,
89
90    /// The creator id.
91    #[serde(rename = "@creator_id")]
92    pub creator_id: NonZeroU64,
93
94    /// Whether this has children.
95    #[serde(rename = "@has_children")]
96    pub has_children: bool,
97
98    /// The creation date of the post.
99    #[serde(rename = "@created_at", with = "crate::util::asctime_with_offset")]
100    pub created_at: OffsetDateTime,
101
102    /// The status of the post.
103    #[serde(rename = "@status")]
104    pub status: PostStatus,
105
106    /// The original source.
107    ///
108    /// May or may not be a url, it is filled manually by users.
109    #[serde(rename = "@source", with = "serde_empty_str_is_none")]
110    pub source: Option<Box<str>>,
111
112    /// Whether the post has notes.
113    #[serde(rename = "@has_notes")]
114    pub has_notes: bool,
115
116    /// Whether this post has comments.
117    #[serde(rename = "@has_comments")]
118    pub has_comments: bool,
119
120    /// The preview image width.
121    #[serde(rename = "@preview_width")]
122    pub preview_width: NonZeroU64,
123
124    /// The preview image height.
125    #[serde(rename = "@preview_height")]
126    pub preview_height: NonZeroU64,
127}
128
129impl Post {
130    /// Iter over the tags in this object.
131    ///
132    /// There may be duplicate tags included.
133    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    /// Get the html post url for this post.
141    ///
142    /// This allocates, so cache the result.
143    pub fn get_html_post_url(&self) -> Url {
144        crate::post_id_to_html_post_url(self.id)
145    }
146}
147
148/// A post rating
149#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
150pub enum Rating {
151    /// Questionable
152    #[serde(rename = "q")]
153    Questionable,
154
155    /// Explicit
156    #[serde(rename = "e")]
157    Explicit,
158
159    /// Safe
160    #[serde(rename = "s")]
161    Safe,
162}
163
164impl Rating {
165    /// Get this as a char
166    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    /// Get this as a &str
175    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/// A Post Status
185#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
186pub enum PostStatus {
187    /// Active, the default state.
188    #[serde(rename = "active")]
189    Active,
190
191    /// Pending, probably waiting for moderator approval.
192    #[serde(rename = "pending")]
193    Pending,
194
195    /// Deleted, the post has been deleted and metadata will soon be purged.
196    #[serde(rename = "deleted")]
197    Deleted,
198
199    /// Flagged, the post is has been flagged for review by a moderator.
200    #[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}