Skip to main content

other_pocket/
get.rs

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    // Get
292    // PocketGetRequest
293    #[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    // PocketGetResponse
355    #[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}