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#[derive(Serialize, Debug)]
28#[serde(rename_all = "camelCase")]
29pub struct GetProfileRequest {
30 pub short_name: Option<String>,
32 pub id: Option<String>,
34 pub get_stats: bool,
36 pub get_tags: bool,
38 pub curated: Option<u32>,
40 pub curable: Option<u32>,
42 pub ncomments: Option<u32>,
44 pub get_followed_topics: bool,
46 pub get_curated_topics: bool,
48 pub filter_curated_topics_by_creation_date_from: Option<u64>,
50 pub filter_curated_topics_by_creation_date_to: Option<u64>,
52 pub get_creator: bool,
54}
55
56impl Default for GetProfileRequest {
57 fn default() -> Self {
58 Self {
60 short_name: None,
61 id: None,
62 get_stats: false, get_tags: false, curated: Some(0), curable: Some(0), ncomments: Some(0), get_followed_topics: false, get_curated_topics: true, 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#[derive(Serialize, Debug)]
84#[serde(rename_all = "camelCase")]
85pub struct GetTopicRequest {
86 pub id: Option<u64>,
88 pub url_name: Option<String>,
90 pub curated: Option<u32>,
92 pub page: Option<u32>,
94 pub curable: Option<u32>,
96 pub curable_page: Option<u32>,
98 pub order: Option<GetTopicOrder>,
100 pub tag: Option<Vec<String>>,
102 pub q: Option<String>,
104 pub since: Option<i64>,
106 pub to: Option<i64>,
108 pub ncomments: Option<u32>,
110 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
145pub trait GetRequest: Serialize + Debug {
147 type Response: TryInto<Self::Output, Error = anyhow::Error> + DeserializeOwned;
151 type Output;
153
154 fn endpoint(&self) -> Cow<'static, str>;
155}
156
157pub trait UpdateRequest: Serialize + Debug {
160 type Response: TryInto<Self::Output, Error = anyhow::Error> + DeserializeOwned;
164 type Output;
166
167 fn endpoint(&self) -> Cow<'static, str>;
168
169 fn content_type() -> &'static str {
171 "application/x-www-form-urlencoded; charset=utf-8"
172 }
173
174 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#[derive(Serialize, Debug)]
269#[serde(rename_all = "camelCase")]
270pub struct SearchRequest {
271 #[serde(rename = "type")]
273 pub search_type: SearchRequestType,
274 pub query: String,
276 pub count: Option<u32>,
278 pub page: Option<u32>,
280 pub lang: Option<String>,
282 pub topic_id: Option<u32>,
284 pub get_tags: bool,
286 pub get_creator: bool,
288 pub get_stats: bool,
290 pub get_tags_for_topic: bool,
292 pub get_stats_for_topic: bool,
294 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#[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#[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#[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#[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#[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#[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#[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#[derive(Serialize, Debug)]
680#[serde(rename_all = "camelCase")]
681pub struct GetTopicGroupRequest {
682 pub url_name: String,
683 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#[derive(Serialize, Debug, Default)]
726#[serde(rename_all = "camelCase")]
727pub struct GetCompilationRequest {
728 pub sort: Option<GetCompilationSort>,
730 pub topic_ids: Option<Vec<i64>>,
732 pub topic_group_id: Option<i64>,
734 pub since: Option<i64>,
736 pub count: Option<u32>,
738 pub page: Option<u32>,
740 pub ncomments: Option<u32>,
742 pub get_tags: Option<bool>,
744 pub get_tags_for_topic: Option<bool>,
746 pub get_stats_for_topic: Option<bool>,
748}
749
750#[derive(Serialize, Debug)]
751pub enum GetCompilationSort {
752 #[serde(rename = "rss")]
754 Rss,
755 #[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}