1use crate::{serialization::*, ItemAuthor, ItemVideo, PocketImage, PocketItemHas};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use url::Url;
5
6#[derive(Default, Serialize)]
7#[serde(rename_all = "camelCase")]
8pub struct PocketGetRequest<'a> {
9 search: Option<&'a str>,
10 domain: Option<&'a str>,
11 tag: Option<PocketGetTag<'a>>,
12 state: Option<PocketGetState>,
13 content_type: Option<PocketGetType>,
14 detail_type: Option<PocketGetDetail>,
15 #[serde(serialize_with = "optional_bool_to_int")]
16 favorite: Option<bool>,
17 #[serde(serialize_with = "optional_datetime_to_int")]
18 since: Option<DateTime<Utc>>,
19 sort: Option<PocketGetSort>,
20 #[serde(serialize_with = "optional_to_string")]
21 count: Option<usize>,
22 #[serde(serialize_with = "optional_to_string")]
23 offset: Option<usize>,
24}
25
26impl<'a> PocketGetRequest<'a> {
27 pub fn new() -> PocketGetRequest<'a> {
28 Default::default()
29 }
30
31 pub fn search<'b>(&'b mut self, search: &'a str) -> &'b mut PocketGetRequest<'a> {
32 self.search = Some(search);
33 self
34 }
35
36 pub fn domain<'b>(&'b mut self, domain: &'a str) -> &'b mut PocketGetRequest<'a> {
37 self.domain = Some(domain);
38 self
39 }
40
41 pub fn tag<'b>(&'b mut self, tag: PocketGetTag<'a>) -> &'b mut PocketGetRequest<'a> {
42 self.tag = Some(tag);
43 self
44 }
45
46 pub fn state<'b>(&'b mut self, state: PocketGetState) -> &'b mut PocketGetRequest<'a> {
47 self.state = Some(state);
48 self
49 }
50
51 pub fn content_type<'b>(
52 &'b mut self,
53 content_type: PocketGetType,
54 ) -> &'b mut PocketGetRequest<'a> {
55 self.content_type = Some(content_type);
56 self
57 }
58
59 pub fn detail_type<'b>(
60 &'b mut self,
61 detail_type: PocketGetDetail,
62 ) -> &'b mut PocketGetRequest<'a> {
63 self.detail_type = Some(detail_type);
64 self
65 }
66
67 pub fn complete<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
68 self.detail_type(PocketGetDetail::Complete)
69 }
70
71 pub fn simple<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
72 self.detail_type(PocketGetDetail::Simple)
73 }
74
75 pub fn archived<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
76 self.state(PocketGetState::Archive)
77 }
78
79 pub fn unread<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
80 self.state(PocketGetState::Unread)
81 }
82
83 pub fn articles<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
84 self.content_type(PocketGetType::Article)
85 }
86
87 pub fn videos<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
88 self.content_type(PocketGetType::Video)
89 }
90
91 pub fn images<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
92 self.content_type(PocketGetType::Image)
93 }
94
95 pub fn favorite<'b>(&'b mut self, fav: bool) -> &'b mut PocketGetRequest<'a> {
96 self.favorite = Some(fav);
97 self
98 }
99
100 pub fn since<'b>(&'b mut self, since: DateTime<Utc>) -> &'b mut PocketGetRequest<'a> {
101 self.since = Some(since);
102 self
103 }
104
105 pub fn sort<'b>(&'b mut self, sort: PocketGetSort) -> &'b mut PocketGetRequest<'a> {
106 self.sort = Some(sort);
107 self
108 }
109
110 pub fn sort_by_newest<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
111 self.sort(PocketGetSort::Newest)
112 }
113
114 pub fn sort_by_oldest<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
115 self.sort(PocketGetSort::Oldest)
116 }
117
118 pub fn sort_by_title<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
119 self.sort(PocketGetSort::Title)
120 }
121
122 pub fn sort_by_site<'b>(&'b mut self) -> &'b mut PocketGetRequest<'a> {
123 self.sort(PocketGetSort::Site)
124 }
125
126 pub fn offset<'b>(&'b mut self, offset: usize) -> &'b mut PocketGetRequest<'a> {
127 self.offset = Some(offset);
128 self
129 }
130
131 pub fn count<'b>(&'b mut self, count: usize) -> &'b mut PocketGetRequest<'a> {
132 self.count = Some(count);
133 self
134 }
135
136 pub fn slice<'b>(&'b mut self, offset: usize, count: usize) -> &'b mut PocketGetRequest<'a> {
137 self.offset(offset).count(count)
138 }
139}
140
141#[derive(Serialize, Debug, Clone, Copy)]
142#[serde(rename_all = "lowercase")]
143pub enum PocketGetDetail {
144 Simple,
145 Complete,
146}
147
148#[derive(Serialize, Debug, Clone, Copy)]
149#[serde(rename_all = "lowercase")]
150pub enum PocketGetSort {
151 Newest,
152 Oldest,
153 Title,
154 Site,
155}
156
157#[derive(Serialize, Debug, Clone, Copy)]
158#[serde(rename_all = "lowercase")]
159pub enum PocketGetState {
160 Unread,
161 Archive,
162 All,
163}
164
165#[derive(Serialize, Debug)]
166#[serde(untagged)]
167pub enum PocketGetTag<'a> {
168 #[serde(serialize_with = "untagged_to_str")]
169 Untagged,
170 Tagged(&'a str),
171}
172
173#[derive(Serialize, Debug, Clone, Copy)]
174#[serde(rename_all = "lowercase")]
175pub enum PocketGetType {
176 Article,
177 Video,
178 Image,
179}
180
181#[derive(Deserialize, Debug, PartialEq)]
182pub struct PocketGetResponse {
183 #[serde(deserialize_with = "vec_from_map")]
184 pub list: Vec<PocketItem>,
185 pub status: u16,
186 #[serde(deserialize_with = "bool_from_int")]
187 pub complete: bool,
188 pub error: Option<String>,
189 pub search_meta: PocketSearchMeta,
190 #[serde(deserialize_with = "int_date_unix_timestamp_format")]
191 pub since: DateTime<Utc>,
192}
193
194#[derive(Deserialize, Debug, PartialEq, Clone)]
195pub struct PocketItem {
196 #[serde(deserialize_with = "from_str")]
197 pub item_id: u64,
198 #[serde(default, deserialize_with = "try_url_from_string")]
199 pub given_url: Option<Url>,
200 pub given_title: String,
201 #[serde(deserialize_with = "from_str")]
202 pub word_count: usize,
203 pub excerpt: String,
204 #[serde(with = "string_date_unix_timestamp_format")]
205 pub time_added: DateTime<Utc>,
206 #[serde(deserialize_with = "option_string_date_unix_timestamp_format")]
207 pub time_read: Option<DateTime<Utc>>,
208 #[serde(with = "string_date_unix_timestamp_format")]
209 pub time_updated: DateTime<Utc>,
210 #[serde(deserialize_with = "option_string_date_unix_timestamp_format")]
211 pub time_favorited: Option<DateTime<Utc>>,
212 #[serde(deserialize_with = "bool_from_int_string")]
213 pub favorite: bool,
214 #[serde(deserialize_with = "bool_from_int_string")]
215 pub is_index: bool,
216 #[serde(deserialize_with = "bool_from_int_string")]
217 pub is_article: bool,
218 pub has_image: PocketItemHas,
219 pub has_video: PocketItemHas,
220 #[serde(deserialize_with = "from_str")]
221 pub resolved_id: u64,
222 pub resolved_title: String,
223 #[serde(default, deserialize_with = "try_url_from_string")]
224 pub resolved_url: Option<Url>,
225 pub sort_id: u64,
226 pub status: PocketItemStatus,
227 #[serde(default, deserialize_with = "optional_vec_from_map")]
228 pub tags: Option<Vec<ItemTag>>,
229 #[serde(default, deserialize_with = "optional_vec_from_map")]
230 pub images: Option<Vec<PocketImage>>,
231 #[serde(default, deserialize_with = "optional_vec_from_map")]
232 pub videos: Option<Vec<ItemVideo>>,
233 #[serde(default, deserialize_with = "optional_vec_from_map")]
234 pub authors: Option<Vec<ItemAuthor>>,
235 pub lang: String,
236 pub time_to_read: Option<u64>,
237 pub domain_metadata: Option<DomainMetaData>,
238 pub listen_duration_estimate: Option<u64>,
239 pub image: Option<ItemImage>,
240 #[serde(default, deserialize_with = "try_url_from_string")]
241 pub amp_url: Option<Url>,
242 #[serde(default, deserialize_with = "try_url_from_string")]
243 pub top_image_url: Option<Url>,
244}
245#[derive(Deserialize, Debug, PartialEq, Clone)]
246pub struct ItemImage {
247 #[serde(deserialize_with = "from_str")]
248 pub item_id: u64,
249 #[serde(default, deserialize_with = "try_url_from_string")]
250 pub src: Option<Url>,
251 #[serde(deserialize_with = "from_str")]
252 pub width: u16,
253 #[serde(deserialize_with = "from_str")]
254 pub height: u16,
255}
256
257#[derive(Deserialize, Debug, PartialEq, Clone)]
258pub struct DomainMetaData {
259 pub name: Option<String>,
260 pub logo: String,
261 pub greyscale_logo: String,
262}
263#[derive(Deserialize, Debug, PartialEq)]
264pub struct PocketSearchMeta {
265 search_type: String,
266}
267
268#[derive(Deserialize, Debug, PartialEq, Clone, Copy)]
269pub enum PocketItemStatus {
270 #[serde(rename = "0")]
271 Normal,
272 #[serde(rename = "1")]
273 Archived,
274 #[serde(rename = "2")]
275 Deleted,
276}
277
278#[derive(Deserialize, Debug, PartialEq, Clone)]
279pub struct ItemTag {
280 #[serde(deserialize_with = "from_str")]
281 pub item_id: u64,
282 pub tag: String,
283}
284
285#[cfg(test)]
286mod test {
287 use super::*;
288 use crate::utils::remove_whitespace;
289 use chrono::TimeZone;
290
291 #[test]
294 fn test_serialize_get_request() {
295 let request = &PocketGetRequest {
296 search: Some("search"),
297 domain: Some("domain"),
298
299 tag: Some(PocketGetTag::Untagged),
300 state: Some(PocketGetState::All),
301 content_type: Some(PocketGetType::Article),
302 detail_type: Some(PocketGetDetail::Complete),
303 favorite: Some(false),
304 since: Some(Utc::now()),
305
306 sort: Some(PocketGetSort::Newest),
307 count: Some(1),
308 offset: Some(2),
309 };
310
311 let actual = serde_json::to_string(request).unwrap();
312
313 let expected = remove_whitespace(&format!(
314 r#"
315 {{
316 "search": "{search}",
317 "domain": "{domain}",
318 "tag": "{tag}",
319 "state": "{state}",
320 "contentType": "{content_type}",
321 "detailType": "{detail_type}",
322 "favorite": "{favorite}",
323 "since": "{since}",
324 "sort": "{sort}",
325 "count": "{count}",
326 "offset": "{offset}"
327 }}
328 "#,
329 search = request.search.unwrap(),
330 domain = request.domain.unwrap(),
331 tag = to_inner_json_string(&request.tag.as_ref()),
332 state = to_inner_json_string(&request.state.unwrap()),
333 content_type = to_inner_json_string(&request.content_type.unwrap()),
334 detail_type = to_inner_json_string(&request.detail_type.unwrap()),
335 favorite = if request.favorite.unwrap() { 1 } else { 0 },
336 since = request.since.unwrap().timestamp().to_string(),
337 sort = to_inner_json_string(&request.sort.unwrap()),
338 count = request.count.unwrap(),
339 offset = request.offset.unwrap(),
340 ));
341
342 assert_eq!(actual, expected);
343 }
344
345 fn to_inner_json_string<T: Serialize>(value: T) -> String {
346 serde_json::to_value(value)
347 .unwrap()
348 .as_str()
349 .unwrap()
350 .trim_matches('\"')
351 .to_string()
352 }
353
354 #[test]
356 fn test_deserialize_get_response_with_list_map() {
357 let expected = PocketGetResponse {
358 list: vec![],
359 status: 1,
360 complete: true,
361 error: None,
362 search_meta: PocketSearchMeta {
363 search_type: "normal".to_string(),
364 },
365 since: Utc.timestamp(1584221353, 0),
366 };
367 let response = remove_whitespace(&format!(
368 r#"
369 {{
370 "status": {status},
371 "complete": {complete},
372 "list": {{}},
373 "error": null,
374 "search_meta": {{
375 "search_type": "{search_type}"
376 }},
377 "since": {since}
378 }}
379 "#,
380 status = expected.status,
381 complete = if expected.complete { 1 } else { 0 },
382 search_type = expected.search_meta.search_type,
383 since = expected.since.timestamp(),
384 ));
385
386 let actual: PocketGetResponse = serde_json::from_str(&response).unwrap();
387
388 assert_eq!(actual, expected);
389 }
390
391 #[test]
392 fn test_deserialize_get_response_with_list_array() {
393 let expected = PocketGetResponse {
394 list: vec![],
395 status: 2,
396 complete: true,
397 error: None,
398 search_meta: PocketSearchMeta {
399 search_type: "normal".to_string(),
400 },
401 since: Utc.timestamp(1584221353, 0),
402 };
403 let response = remove_whitespace(&format!(
404 r#"
405 {{
406 "status": {status},
407 "complete": {complete},
408 "list": [],
409 "error": null,
410 "search_meta": {{
411 "search_type": "{search_type}"
412 }},
413 "since": {since}
414 }}
415 "#,
416 status = expected.status,
417 complete = if expected.complete { 1 } else { 0 },
418 search_type = expected.search_meta.search_type,
419 since = expected.since.timestamp(),
420 ));
421
422 let actual: PocketGetResponse = serde_json::from_str(&response).unwrap();
423
424 assert_eq!(actual, expected);
425 }
426}