scoopit_api/
requests.rs

1use std::{
2    borrow::Cow,
3    convert::{TryFrom, TryInto},
4    fmt::Debug,
5    str::FromStr,
6};
7
8use anyhow::anyhow;
9use reqwest::Method;
10use serde::{de::DeserializeOwned, Deserialize, Serialize};
11
12use crate::{
13    serde_qs,
14    types::{
15        Post, RecipientsList, SearchResults, Source, SourceTypeData, SuggestionEngine, Topic,
16        TopicGroup, User,
17    },
18};
19
20/// Get the profile of a user.
21///
22/// Maps parameters of https://www.scoop.it/dev/api/1/urls#user
23///
24/// Documentation of each field comes from the page above. Default values documented are used only
25/// ff the field is not present (`None`), `Default` implementation for this struct may differ from
26/// Scoop.it defaults to avoid retrieving the world while only looking at the user profile.
27#[derive(Serialize, Debug)]
28#[serde(rename_all = "camelCase")]
29pub struct GetProfileRequest {
30    /// string optional - the shortName of the user to lookup - defaults to the current user
31    pub short_name: Option<String>,
32    /// long optional - the id of the user to lookup - defaults to the current user
33    pub id: Option<String>,
34    /// bool optional - default to false. returns or not stats for each returned topic
35    pub get_stats: bool,
36    /// bool optional - default to true. returns or not list of tags for each returned topic
37    pub get_tags: bool,
38    /// int optional - default to 0, number of curated posts to retrieve for each topic present in user data
39    pub curated: Option<u32>,
40    /// int optional - default to 0, number of curable posts to retrieve for each topic the current user is the curator (so it should not be specified if the "id" parameter is specified)
41    pub curable: Option<u32>,
42    /// int optional - default to 0, the maximum number of comments to retrieve for each curated post found in each topic present in user data
43    pub ncomments: Option<u32>,
44    /// bool optional - default to true. returns or not list of followed topics
45    pub get_followed_topics: bool,
46    /// bool optional - default to true. returns or not list of curated topics
47    pub get_curated_topics: bool,
48    ///timestamp optional - default to 0 (unix epoch). Filter curated topics by creation date.
49    pub filter_curated_topics_by_creation_date_from: Option<u64>,
50    ///timestamp optional - default to 2^63. Filter curated topics by creation date.
51    pub filter_curated_topics_by_creation_date_to: Option<u64>,
52    /// bool optional - default to true. returns or not creator of each returned topic
53    pub get_creator: bool,
54}
55
56impl Default for GetProfileRequest {
57    fn default() -> Self {
58        // sane defaults
59        Self {
60            short_name: None,
61            id: None,
62            get_stats: false,           // no stats by default
63            get_tags: false,            // no tags by default
64            curated: Some(0),           // do not retrieve posts on curated topics
65            curable: Some(0),           // do not retrieve suggestion on curated topics
66            ncomments: Some(0),         // force no comments
67            get_followed_topics: false, // no followed topics by default
68            get_curated_topics: true,   // get curated topics by default
69            filter_curated_topics_by_creation_date_from: None,
70            filter_curated_topics_by_creation_date_to: None,
71            get_creator: false,
72        }
73    }
74}
75
76/// Get a Topic.
77///
78/// Maps parameters of https://www.scoop.it/dev/api/1/urls#topic
79///
80/// Documentation of each field comes from the page above. Default values documented are used only
81/// ff the field is not present (`None`), `Default` implementation for this struct may differ from
82/// Scoop.it defaults to avoid retrieving the world while only looking at the user profile.
83#[derive(Serialize, Debug)]
84#[serde(rename_all = "camelCase")]
85pub struct GetTopicRequest {
86    /// long required, unless 'urlName' is provided - the id of the topic to lookup
87    pub id: Option<u64>,
88    /// string required, unless 'id' is provided - the urlName of the topic to lookup
89    pub url_name: Option<String>,
90    /// int optional, default to 30 - number of curated posts to retrieve for this topic
91    pub curated: Option<u32>,
92    /// int optional, default to 0
93    pub page: Option<u32>,
94    /// int optional, default to 0 - for this topic, this parameter is ignored if the current user is not the curator of this topic
95    pub curable: Option<u32>,
96    /// int optional, default to 0 - for this topic, this parameter is ignored if the current user is not the curator of this topic - get a given page of curable posts
97    pub curable_page: Option<u32>,
98    /// string mandatory if "since" parameter is not specified - sort order of the curated posts, can be "tag" (see below), "search" (filter result on query "q" mandatory - see below), "curationDate", "user" (same order as seen in the scoop.it website)
99    pub order: Option<GetTopicOrder>,
100    /// string[] mandatory if "order"=="tag"
101    pub tag: Option<Vec<String>>,
102    ///  string mandatory if "order"=="search" - the query to use to search in the topic
103    pub q: Option<String>,
104    ///timestamp - only retrieve curated post newer than this timestamp
105    pub since: Option<i64>,
106    /// timestamp optional - used with "since" parameter, retrieve curated posts posts older then this timestamp
107    pub to: Option<i64>,
108    /// int optional, default to 100 - each curated post found in this topic
109    pub ncomments: Option<u32>,
110    /// boolean optional, default to false - if true, the response will include the scheduled posts
111    pub show_scheduled: bool,
112}
113#[derive(Serialize, Debug)]
114pub enum GetTopicOrder {
115    #[serde(rename = "tag")]
116    Tag,
117    #[serde(rename = "search")]
118    Search,
119    #[serde(rename = "curationDate")]
120    CurationDate,
121    #[serde(rename = "user")]
122    User,
123}
124
125impl Default for GetTopicRequest {
126    fn default() -> Self {
127        Self {
128            id: None,
129            url_name: None,
130            curated: Some(30),
131            page: None,
132            curable: Some(0),
133            curable_page: None,
134            order: None,
135            tag: None,
136            q: None,
137            since: None,
138            to: None,
139            ncomments: Some(100),
140            show_scheduled: false,
141        }
142    }
143}
144
145/// Represents a `GET` request.
146pub trait GetRequest: Serialize + Debug {
147    /// The type returned by the Scoop.it API.
148    ///
149    /// It must be converible to this trait Output type.
150    type Response: TryInto<Self::Output, Error = anyhow::Error> + DeserializeOwned;
151    /// The type returned by the client
152    type Output;
153
154    fn endpoint(&self) -> Cow<'static, str>;
155}
156
157/// A request that does an update, by default the body is serialized as
158/// `application/x-www-form-urlencoded` and the method is `POST`
159pub trait UpdateRequest: Serialize + Debug {
160    /// The type returned by the Scoop.it API.
161    ///
162    /// It must be convertible to this trait Output type.
163    type Response: TryInto<Self::Output, Error = anyhow::Error> + DeserializeOwned;
164    /// The type returned by the client
165    type Output;
166
167    fn endpoint(&self) -> Cow<'static, str>;
168
169    /// the content type of the post request, by default `application/x-www-form-urlencoded`
170    fn content_type() -> &'static str {
171        "application/x-www-form-urlencoded; charset=utf-8"
172    }
173
174    /// The body as bytes, by default the type implementing this trait is serialized using serde_qs.
175    fn body(&self) -> anyhow::Result<Vec<u8>> {
176        Ok(serde_qs::to_string(&self)?.into_bytes())
177    }
178
179    fn method(&self) -> Method {
180        Method::POST
181    }
182}
183
184impl GetRequest for GetTopicRequest {
185    type Response = TopicResponse;
186    type Output = Topic;
187
188    fn endpoint(&self) -> Cow<'static, str> {
189        "topic".into()
190    }
191}
192impl GetRequest for GetProfileRequest {
193    type Response = UserResponse;
194    type Output = User;
195
196    fn endpoint(&self) -> Cow<'static, str> {
197        "profile".into()
198    }
199}
200
201#[derive(Deserialize)]
202pub struct TopicResponse {
203    pub topic: Option<Topic>,
204    pub error: Option<String>,
205}
206
207#[derive(Deserialize)]
208pub struct UserResponse {
209    pub user: Option<User>,
210    pub error: Option<String>,
211}
212
213impl TryFrom<UserResponse> for User {
214    type Error = anyhow::Error;
215
216    fn try_from(value: UserResponse) -> Result<Self, Self::Error> {
217        if let Some(error) = value.error {
218            Err(anyhow::anyhow!("Server returned an error: {}", error))
219        } else {
220            value
221                .user
222                .ok_or(anyhow::anyhow!("No user nor error in response body!"))
223        }
224    }
225}
226impl TryFrom<TopicResponse> for Topic {
227    type Error = anyhow::Error;
228
229    fn try_from(value: TopicResponse) -> Result<Self, Self::Error> {
230        if let Some(error) = value.error {
231            Err(anyhow::anyhow!("Server returned an error: {}", error))
232        } else {
233            value
234                .topic
235                .ok_or(anyhow::anyhow!("No user no topic in response body!"))
236        }
237    }
238}
239
240#[derive(Serialize, Deserialize, Debug)]
241#[serde(rename_all = "camelCase")]
242pub enum SearchRequestType {
243    User,
244    Topic,
245    Post,
246}
247
248impl FromStr for SearchRequestType {
249    type Err = anyhow::Error;
250
251    fn from_str(s: &str) -> Result<Self, Self::Err> {
252        match s {
253            "user" => Ok(SearchRequestType::User),
254            "topic" => Ok(SearchRequestType::Topic),
255            "post" => Ok(SearchRequestType::Post),
256            other => Err(anyhow::anyhow!("Invalid request type: {}", other)),
257        }
258    }
259}
260
261/// Perform a search.
262///
263/// Maps parameters of https://www.scoop.it/dev/api/1/urls#search
264///
265/// Documentation of each field comes from the page above. Default values documented are used only
266/// ff the field is not present (`None`), `Default` implementation for this struct may differ from
267/// Scoop.it defaults to avoid retrieving the world while only looking at the user profile.
268#[derive(Serialize, Debug)]
269#[serde(rename_all = "camelCase")]
270pub struct SearchRequest {
271    ///string - type of object searched: "user", "topic" or "post"
272    #[serde(rename = "type")]
273    pub search_type: SearchRequestType,
274    /// string - the search query
275    pub query: String,
276    /// int optional, default to 50 - the number of item per page
277    pub count: Option<u32>,
278    /// int optional, default to 0 -the page number to return, the first page is 0
279    pub page: Option<u32>,
280    /// string optional, default to "en" - the language of the content to search into
281    pub lang: Option<String>,
282    /// long optional - the id of the topic to search posts into
283    pub topic_id: Option<u32>,
284    /// bool optional, default to true - returns or not list of tags for each returned topic / post. only for type="topic" or type="post"
285    pub get_tags: bool,
286    /// bool optional, default to true - returns or not creator of each returned topic. only for type="topic"
287    pub get_creator: bool,
288    /// bool optional, default to true - returns or not stats for each returned topic. only for type="topic"
289    pub get_stats: bool,
290    /// bool optional, default to true - returns or not tags for topic of each returned post. only for type="post"
291    pub get_tags_for_topic: bool,
292    /// bool optional, default to true - returns or not stats for topic of each returned post. only for type="post"
293    pub get_stats_for_topic: bool,
294    /// bool optional, default to false - also returns topics having no posts. only for type="topic"
295    pub include_empty_topics: bool,
296}
297impl Default for SearchRequest {
298    fn default() -> Self {
299        Self {
300            search_type: SearchRequestType::Post,
301            query: "".to_string(),
302            count: Some(50),
303            page: None,
304            lang: None,
305            topic_id: None,
306            get_tags: false,
307            get_creator: true,
308            get_stats: false,
309            get_tags_for_topic: false,
310            get_stats_for_topic: false,
311            include_empty_topics: false,
312        }
313    }
314}
315#[derive(Deserialize, Debug)]
316#[serde(rename_all = "camelCase")]
317pub struct SearchResponse {
318    pub users: Option<Vec<User>>,
319    pub topics: Option<Vec<Topic>>,
320    pub posts: Option<Vec<Post>>,
321    pub total_found: i32,
322}
323
324impl GetRequest for SearchRequest {
325    type Response = SearchResponse;
326
327    type Output = SearchResults;
328
329    fn endpoint(&self) -> Cow<'static, str> {
330        "search".into()
331    }
332}
333
334impl TryFrom<SearchResponse> for SearchResults {
335    type Error = anyhow::Error;
336
337    fn try_from(value: SearchResponse) -> Result<Self, Self::Error> {
338        let SearchResponse {
339            users,
340            topics,
341            posts,
342            total_found,
343        } = value;
344        Ok(SearchResults {
345            users,
346            topics,
347            posts,
348            total_found,
349        })
350    }
351}
352
353/// Get the list of recipients lists
354///
355/// See https://www.scoop.it/dev/api/1/urls#recipients-list
356///
357#[derive(Serialize, Debug, Default)]
358pub struct GetRecipientsListRequest {
359    _dummy: (),
360}
361#[derive(Deserialize, Debug)]
362#[serde(rename_all = "camelCase")]
363pub struct GetRecipientsListResponse {
364    list: Vec<RecipientsList>,
365}
366
367impl GetRequest for GetRecipientsListRequest {
368    type Response = GetRecipientsListResponse;
369    type Output = Vec<RecipientsList>;
370
371    fn endpoint(&self) -> Cow<'static, str> {
372        "recipients-list".into()
373    }
374}
375impl TryFrom<GetRecipientsListResponse> for Vec<RecipientsList> {
376    type Error = anyhow::Error;
377
378    fn try_from(value: GetRecipientsListResponse) -> Result<Self, Self::Error> {
379        Ok(value.list)
380    }
381}
382
383/// Test authentication credentials.
384///
385/// https://www.scoop.it/dev/api/1/urls#test
386#[derive(Serialize, Debug, Default)]
387pub struct TestRequest {
388    _dummy: (),
389}
390
391#[derive(Deserialize, Debug)]
392#[serde(rename_all = "camelCase")]
393pub struct TestResponse {
394    connected_user: Option<String>,
395    error: Option<String>,
396}
397
398impl GetRequest for TestRequest {
399    type Response = TestResponse;
400    type Output = Option<String>;
401
402    fn endpoint(&self) -> Cow<'static, str> {
403        "test".into()
404    }
405}
406impl TryFrom<TestResponse> for Option<String> {
407    type Error = anyhow::Error;
408
409    fn try_from(value: TestResponse) -> Result<Self, Self::Error> {
410        if let Some(error) = value.error {
411            Err(anyhow::anyhow!("Server returned an error: {}", error))
412        } else {
413            Ok(value.connected_user)
414        }
415    }
416}
417
418#[derive(Debug, Serialize)]
419pub struct LoginRequest {
420    pub email: String,
421    pub password: String,
422}
423
424impl UpdateRequest for LoginRequest {
425    type Response = LoginResponse;
426
427    type Output = LoginAccessToken;
428
429    fn endpoint(&self) -> Cow<'static, str> {
430        "login".into()
431    }
432}
433
434#[derive(Debug, Deserialize)]
435#[serde(untagged)]
436pub enum LoginResponse {
437    Ok {
438        #[serde(rename = "accessToken")]
439        access_token: LoginAccessToken,
440    },
441    Err {
442        errors: Vec<String>,
443    },
444}
445
446#[derive(Debug, Deserialize)]
447pub struct LoginAccessToken {
448    pub oauth_token: String,
449    pub oauth_token_secret: String,
450}
451
452impl TryFrom<LoginResponse> for LoginAccessToken {
453    type Error = anyhow::Error;
454
455    fn try_from(value: LoginResponse) -> Result<Self, Self::Error> {
456        match value {
457            LoginResponse::Ok { access_token } => Ok(access_token),
458            LoginResponse::Err { errors } => Err(anyhow!(
459                "Unable to login with errors: {}",
460                errors.join(", ")
461            )),
462        }
463    }
464}
465/// Get the list of available suggestion engines
466///
467/// https://www.scoop.it/dev/api/1/urls#se
468#[derive(Debug, Default, Serialize)]
469pub struct GetSuggestionEnginesRequest {
470    _dummy: (),
471}
472
473#[derive(Debug, Deserialize)]
474#[serde(untagged)]
475pub enum GetSuggestionEnginesResponse {
476    Ok {
477        suggestion_engines: Vec<SuggestionEngine>,
478    },
479    Err {
480        error: String,
481    },
482}
483
484impl GetRequest for GetSuggestionEnginesRequest {
485    type Response = GetSuggestionEnginesResponse;
486
487    type Output = Vec<SuggestionEngine>;
488
489    fn endpoint(&self) -> Cow<'static, str> {
490        "se".into()
491    }
492}
493
494impl TryFrom<GetSuggestionEnginesResponse> for Vec<SuggestionEngine> {
495    type Error = anyhow::Error;
496
497    fn try_from(value: GetSuggestionEnginesResponse) -> Result<Self, Self::Error> {
498        match value {
499            GetSuggestionEnginesResponse::Ok { suggestion_engines } => Ok(suggestion_engines),
500            GetSuggestionEnginesResponse::Err { error } => {
501                Err(anyhow!("Server returned an error: {error}"))
502            }
503        }
504    }
505}
506
507/// Get manual user sources of a suggestion engine.
508///
509/// https://www.scoop.it/dev/api/1/urls#se_sources
510#[derive(Debug, Serialize)]
511pub struct GetSuggestionEngineSourcesRequest {
512    #[serde(skip)]
513    pub suggestion_engine_id: i64,
514}
515
516#[derive(Debug, Deserialize)]
517#[serde(untagged)]
518pub enum GetSuggestionEngineSourcesResponse {
519    Ok { sources: Vec<Source> },
520    Err { error: String },
521}
522
523impl GetRequest for GetSuggestionEngineSourcesRequest {
524    type Response = GetSuggestionEngineSourcesResponse;
525
526    type Output = Vec<Source>;
527
528    fn endpoint(&self) -> Cow<'static, str> {
529        format!("se/{}/sources", self.suggestion_engine_id).into()
530    }
531}
532
533impl TryFrom<GetSuggestionEngineSourcesResponse> for Vec<Source> {
534    type Error = anyhow::Error;
535
536    fn try_from(value: GetSuggestionEngineSourcesResponse) -> Result<Self, Self::Error> {
537        match value {
538            GetSuggestionEngineSourcesResponse::Ok { sources } => Ok(sources),
539            GetSuggestionEngineSourcesResponse::Err { error } => {
540                Err(anyhow!("Server returned an error: {error}"))
541            }
542        }
543    }
544}
545
546#[derive(Deserialize, Debug)]
547#[serde(untagged)]
548pub enum EmptyUpdateResponse {
549    Err { error: String },
550    Ok {},
551}
552
553impl EmptyUpdateResponse {
554    pub fn is_ok(&self) -> bool {
555        if let EmptyUpdateResponse::Ok {} = &self {
556            true
557        } else {
558            false
559        }
560    }
561    pub fn is_err(&self) -> bool {
562        !self.is_ok()
563    }
564}
565
566impl TryFrom<EmptyUpdateResponse> for () {
567    type Error = anyhow::Error;
568
569    fn try_from(value: EmptyUpdateResponse) -> Result<Self, Self::Error> {
570        match value {
571            EmptyUpdateResponse::Err { error } => Err(anyhow!("Server returned an error: {error}")),
572            EmptyUpdateResponse::Ok {} => Ok(()),
573        }
574    }
575}
576
577/// Delete a manually source from a suggestion engine
578///
579/// https://www.scoop.it/dev/api/1/urls#se_sources_id
580#[derive(Serialize, Debug)]
581pub struct DeleteSuggestionEngineSourceRequest {
582    #[serde(skip)]
583    pub suggestion_engine_id: i64,
584    #[serde(skip)]
585    pub source_id: i64,
586}
587
588impl UpdateRequest for DeleteSuggestionEngineSourceRequest {
589    type Response = EmptyUpdateResponse;
590
591    type Output = ();
592
593    fn endpoint(&self) -> Cow<'static, str> {
594        format!(
595            "se/{}/sources/{}",
596            self.suggestion_engine_id, self.source_id
597        )
598        .into()
599    }
600
601    fn method(&self) -> Method {
602        Method::DELETE
603    }
604}
605
606/// Update a manually source from a suggestion engine
607///
608/// https://www.scoop.it/dev/api/1/urls#se_sources_id
609#[derive(Serialize, Debug)]
610pub struct UpdateSuggestionEngineSourceRequest {
611    #[serde(skip)]
612    pub suggestion_engine_id: i64,
613    #[serde(skip)]
614    pub source_id: i64,
615    pub name: Option<String>,
616}
617
618impl UpdateRequest for UpdateSuggestionEngineSourceRequest {
619    type Response = EmptyUpdateResponse;
620
621    type Output = ();
622
623    fn endpoint(&self) -> Cow<'static, str> {
624        format!(
625            "se/{}/sources/{}",
626            self.suggestion_engine_id, self.source_id
627        )
628        .into()
629    }
630}
631
632/// Create a suggestion engine source
633///
634/// https://www.scoop.it/dev/api/1/urls#se_sources_id
635#[derive(Serialize, Debug)]
636pub struct CreateSuggestionEngineSourceRequest {
637    #[serde(skip)]
638    pub suggestion_engine_id: i64,
639    pub name: Option<String>,
640    #[serde(flatten)]
641    pub source_data: SourceTypeData,
642}
643
644#[derive(Debug, Deserialize)]
645#[serde(untagged)]
646pub enum CreateSuggestionEngineSourceResponse {
647    Ok { source: Source },
648    Err { error: String },
649}
650impl UpdateRequest for CreateSuggestionEngineSourceRequest {
651    type Response = CreateSuggestionEngineSourceResponse;
652
653    type Output = Source;
654
655    fn endpoint(&self) -> Cow<'static, str> {
656        format!("se/{}/sources", self.suggestion_engine_id).into()
657    }
658
659    fn method(&self) -> Method {
660        Method::PUT
661    }
662}
663impl TryFrom<CreateSuggestionEngineSourceResponse> for Source {
664    type Error = anyhow::Error;
665
666    fn try_from(value: CreateSuggestionEngineSourceResponse) -> Result<Self, Self::Error> {
667        match value {
668            CreateSuggestionEngineSourceResponse::Ok { source } => Ok(source),
669            CreateSuggestionEngineSourceResponse::Err { error } => {
670                Err(anyhow!("Server returned an error: {error}"))
671            }
672        }
673    }
674}
675
676/// Get the data about a topic group
677///
678/// https://www.scoop.it/dev/api/1/urls#topicGroup
679#[derive(Serialize, Debug)]
680#[serde(rename_all = "camelCase")]
681pub struct GetTopicGroupRequest {
682    pub url_name: String,
683    /// Some apps may be able to specify company id. (privileged apps)
684    pub company_id: Option<i64>,
685}
686
687#[derive(Deserialize, Debug)]
688#[serde(untagged)]
689pub enum GetTopicGroupResponse {
690    #[serde(rename_all = "camelCase")]
691    Ok {
692        topic_group: TopicGroup,
693    },
694    Err {
695        error: String,
696    },
697}
698
699impl GetRequest for GetTopicGroupRequest {
700    type Response = GetTopicGroupResponse;
701
702    type Output = TopicGroup;
703
704    fn endpoint(&self) -> Cow<'static, str> {
705        "topic-group".into()
706    }
707}
708
709impl TryFrom<GetTopicGroupResponse> for TopicGroup {
710    type Error = anyhow::Error;
711
712    fn try_from(value: GetTopicGroupResponse) -> Result<Self, Self::Error> {
713        match value {
714            GetTopicGroupResponse::Ok { topic_group } => Ok(topic_group),
715            GetTopicGroupResponse::Err { error } => {
716                Err(anyhow!("Server returned an error: {error}"))
717            }
718        }
719    }
720}
721
722/// Get the data about a topic group
723///
724/// https://www.scoop.it/dev/api/1/urls#compilation
725#[derive(Serialize, Debug, Default)]
726#[serde(rename_all = "camelCase")]
727pub struct GetCompilationRequest {
728    ///  method used for sorting posts (GetCompilationSort::Rss if not specified)
729    pub sort: Option<GetCompilationSort>,
730    ///  a list of topic ids that will be used to create the compilation
731    pub topic_ids: Option<Vec<i64>>,
732    /// create the compilation from topics in this topic group
733    pub topic_group_id: Option<i64>,
734    /// no posts older than this timestamp will be returned (in millis from unix epoch)
735    pub since: Option<i64>,
736    ///maximum number of posts to return
737    pub count: Option<u32>,
738    /// page number of posts to retrieve
739    pub page: Option<u32>,
740    /// the maximum number of comments to retrieve for each returned post
741    pub ncomments: Option<u32>,
742    // return the list of tags for each returned post.
743    pub get_tags: Option<bool>,
744    /// return tags for topic of each returned post
745    pub get_tags_for_topic: Option<bool>,
746    /// return stats for topic of each returned post
747    pub get_stats_for_topic: Option<bool>,
748}
749
750#[derive(Serialize, Debug)]
751pub enum GetCompilationSort {
752    /// posts are ordered like in the RSS feed
753    #[serde(rename = "rss")]
754    Rss,
755    /// posts are ordered like in the "My followed scoops" tab in a scoop.it user profile
756    #[serde(rename = "timeline")]
757    Timeline,
758}
759
760#[derive(Deserialize, Debug)]
761#[serde(rename_all = "camelCase", untagged)]
762pub enum GetCompilationResponse {
763    Ok { posts: Vec<Post> },
764    Err { error: String },
765}
766
767impl GetRequest for GetCompilationRequest {
768    type Response = GetCompilationResponse;
769
770    type Output = Vec<Post>;
771
772    fn endpoint(&self) -> Cow<'static, str> {
773        "compilation".into()
774    }
775}
776
777impl TryFrom<GetCompilationResponse> for Vec<Post> {
778    type Error = anyhow::Error;
779
780    fn try_from(value: GetCompilationResponse) -> Result<Self, Self::Error> {
781        match value {
782            GetCompilationResponse::Ok { posts } => Ok(posts),
783            GetCompilationResponse::Err { error } => {
784                Err(anyhow!("Server returned an error: {error}"))
785            }
786        }
787    }
788}