use std::{
borrow::Cow,
convert::{TryFrom, TryInto},
fmt::Debug,
str::FromStr,
};
use anyhow::anyhow;
use reqwest::Method;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::{
serde_qs,
types::{
Post, RecipientsList, SearchResults, Source, SourceTypeData, SuggestionEngine, Topic,
TopicGroup, User,
},
};
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetProfileRequest {
pub short_name: Option<String>,
pub id: Option<String>,
pub get_stats: bool,
pub get_tags: bool,
pub curated: Option<u32>,
pub curable: Option<u32>,
pub ncomments: Option<u32>,
pub get_followed_topics: bool,
pub get_curated_topics: bool,
pub filter_curated_topics_by_creation_date_from: Option<u64>,
pub filter_curated_topics_by_creation_date_to: Option<u64>,
pub get_creator: bool,
}
impl Default for GetProfileRequest {
fn default() -> Self {
Self {
short_name: None,
id: None,
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,
filter_curated_topics_by_creation_date_to: None,
get_creator: false,
}
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetTopicRequest {
pub id: Option<u64>,
pub url_name: Option<String>,
pub curated: Option<u32>,
pub page: Option<u32>,
pub curable: Option<u32>,
pub curable_page: Option<u32>,
pub order: Option<GetTopicOrder>,
pub tag: Option<Vec<String>>,
pub q: Option<String>,
pub since: Option<i64>,
pub to: Option<i64>,
pub ncomments: Option<u32>,
pub show_scheduled: bool,
}
#[derive(Serialize, Debug)]
pub enum GetTopicOrder {
#[serde(rename = "tag")]
Tag,
#[serde(rename = "search")]
Search,
#[serde(rename = "curationDate")]
CurationDate,
#[serde(rename = "user")]
User,
}
impl Default for GetTopicRequest {
fn default() -> Self {
Self {
id: None,
url_name: None,
curated: Some(30),
page: None,
curable: Some(0),
curable_page: None,
order: None,
tag: None,
q: None,
since: None,
to: None,
ncomments: Some(100),
show_scheduled: false,
}
}
}
pub trait GetRequest: Serialize + Debug {
type Response: TryInto<Self::Output, Error = anyhow::Error> + DeserializeOwned;
type Output;
fn endpoint(&self) -> Cow<'static, str>;
}
pub trait UpdateRequest: Serialize + Debug {
type Response: TryInto<Self::Output, Error = anyhow::Error> + DeserializeOwned;
type Output;
fn endpoint(&self) -> Cow<'static, str>;
fn content_type() -> &'static str {
"application/x-www-form-urlencoded; charset=utf-8"
}
fn body(&self) -> anyhow::Result<Vec<u8>> {
Ok(serde_qs::to_string(&self)?.into_bytes())
}
fn method(&self) -> Method {
Method::POST
}
}
impl GetRequest for GetTopicRequest {
type Response = TopicResponse;
type Output = Topic;
fn endpoint(&self) -> Cow<'static, str> {
"topic".into()
}
}
impl GetRequest for GetProfileRequest {
type Response = UserResponse;
type Output = User;
fn endpoint(&self) -> Cow<'static, str> {
"profile".into()
}
}
#[derive(Deserialize)]
pub struct TopicResponse {
pub topic: Option<Topic>,
pub error: Option<String>,
}
#[derive(Deserialize)]
pub struct UserResponse {
pub user: Option<User>,
pub error: Option<String>,
}
impl TryFrom<UserResponse> for User {
type Error = anyhow::Error;
fn try_from(value: UserResponse) -> Result<Self, Self::Error> {
if let Some(error) = value.error {
Err(anyhow::anyhow!("Server returned an error: {}", error))
} else {
value
.user
.ok_or(anyhow::anyhow!("No user nor error in response body!"))
}
}
}
impl TryFrom<TopicResponse> for Topic {
type Error = anyhow::Error;
fn try_from(value: TopicResponse) -> Result<Self, Self::Error> {
if let Some(error) = value.error {
Err(anyhow::anyhow!("Server returned an error: {}", error))
} else {
value
.topic
.ok_or(anyhow::anyhow!("No user no topic in response body!"))
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum SearchRequestType {
User,
Topic,
Post,
}
impl FromStr for SearchRequestType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"user" => Ok(SearchRequestType::User),
"topic" => Ok(SearchRequestType::Topic),
"post" => Ok(SearchRequestType::Post),
other => Err(anyhow::anyhow!("Invalid request type: {}", other)),
}
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SearchRequest {
#[serde(rename = "type")]
pub search_type: SearchRequestType,
pub query: String,
pub count: Option<u32>,
pub page: Option<u32>,
pub lang: Option<String>,
pub topic_id: Option<u32>,
pub get_tags: bool,
pub get_creator: bool,
pub get_stats: bool,
pub get_tags_for_topic: bool,
pub get_stats_for_topic: bool,
pub include_empty_topics: bool,
}
impl Default for SearchRequest {
fn default() -> Self {
Self {
search_type: SearchRequestType::Post,
query: "".to_string(),
count: Some(50),
page: None,
lang: None,
topic_id: None,
get_tags: false,
get_creator: true,
get_stats: false,
get_tags_for_topic: false,
get_stats_for_topic: false,
include_empty_topics: false,
}
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SearchResponse {
pub users: Option<Vec<User>>,
pub topics: Option<Vec<Topic>>,
pub posts: Option<Vec<Post>>,
pub total_found: i32,
}
impl GetRequest for SearchRequest {
type Response = SearchResponse;
type Output = SearchResults;
fn endpoint(&self) -> Cow<'static, str> {
"search".into()
}
}
impl TryFrom<SearchResponse> for SearchResults {
type Error = anyhow::Error;
fn try_from(value: SearchResponse) -> Result<Self, Self::Error> {
let SearchResponse {
users,
topics,
posts,
total_found,
} = value;
Ok(SearchResults {
users,
topics,
posts,
total_found,
})
}
}
#[derive(Serialize, Debug, Default)]
pub struct GetRecipientsListRequest {
_dummy: (),
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetRecipientsListResponse {
list: Vec<RecipientsList>,
}
impl GetRequest for GetRecipientsListRequest {
type Response = GetRecipientsListResponse;
type Output = Vec<RecipientsList>;
fn endpoint(&self) -> Cow<'static, str> {
"recipients-list".into()
}
}
impl TryFrom<GetRecipientsListResponse> for Vec<RecipientsList> {
type Error = anyhow::Error;
fn try_from(value: GetRecipientsListResponse) -> Result<Self, Self::Error> {
Ok(value.list)
}
}
#[derive(Serialize, Debug, Default)]
pub struct TestRequest {
_dummy: (),
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TestResponse {
connected_user: Option<String>,
error: Option<String>,
}
impl GetRequest for TestRequest {
type Response = TestResponse;
type Output = Option<String>;
fn endpoint(&self) -> Cow<'static, str> {
"test".into()
}
}
impl TryFrom<TestResponse> for Option<String> {
type Error = anyhow::Error;
fn try_from(value: TestResponse) -> Result<Self, Self::Error> {
if let Some(error) = value.error {
Err(anyhow::anyhow!("Server returned an error: {}", error))
} else {
Ok(value.connected_user)
}
}
}
#[derive(Debug, Serialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
impl UpdateRequest for LoginRequest {
type Response = LoginResponse;
type Output = LoginAccessToken;
fn endpoint(&self) -> Cow<'static, str> {
"login".into()
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum LoginResponse {
Ok {
#[serde(rename = "accessToken")]
access_token: LoginAccessToken,
},
Err {
errors: Vec<String>,
},
}
#[derive(Debug, Deserialize)]
pub struct LoginAccessToken {
pub oauth_token: String,
pub oauth_token_secret: String,
}
impl TryFrom<LoginResponse> for LoginAccessToken {
type Error = anyhow::Error;
fn try_from(value: LoginResponse) -> Result<Self, Self::Error> {
match value {
LoginResponse::Ok { access_token } => Ok(access_token),
LoginResponse::Err { errors } => Err(anyhow!(
"Unable to login with errors: {}",
errors.join(", ")
)),
}
}
}
#[derive(Debug, Default, Serialize)]
pub struct GetSuggestionEnginesRequest {
_dummy: (),
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum GetSuggestionEnginesResponse {
Ok {
suggestion_engines: Vec<SuggestionEngine>,
},
Err {
error: String,
},
}
impl GetRequest for GetSuggestionEnginesRequest {
type Response = GetSuggestionEnginesResponse;
type Output = Vec<SuggestionEngine>;
fn endpoint(&self) -> Cow<'static, str> {
"se".into()
}
}
impl TryFrom<GetSuggestionEnginesResponse> for Vec<SuggestionEngine> {
type Error = anyhow::Error;
fn try_from(value: GetSuggestionEnginesResponse) -> Result<Self, Self::Error> {
match value {
GetSuggestionEnginesResponse::Ok { suggestion_engines } => Ok(suggestion_engines),
GetSuggestionEnginesResponse::Err { error } => {
Err(anyhow!("Server returned an error: {error}"))
}
}
}
}
#[derive(Debug, Serialize)]
pub struct GetSuggestionEngineSourcesRequest {
#[serde(skip)]
pub suggestion_engine_id: i64,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum GetSuggestionEngineSourcesResponse {
Ok { sources: Vec<Source> },
Err { error: String },
}
impl GetRequest for GetSuggestionEngineSourcesRequest {
type Response = GetSuggestionEngineSourcesResponse;
type Output = Vec<Source>;
fn endpoint(&self) -> Cow<'static, str> {
format!("se/{}/sources", self.suggestion_engine_id).into()
}
}
impl TryFrom<GetSuggestionEngineSourcesResponse> for Vec<Source> {
type Error = anyhow::Error;
fn try_from(value: GetSuggestionEngineSourcesResponse) -> Result<Self, Self::Error> {
match value {
GetSuggestionEngineSourcesResponse::Ok { sources } => Ok(sources),
GetSuggestionEngineSourcesResponse::Err { error } => {
Err(anyhow!("Server returned an error: {error}"))
}
}
}
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum EmptyUpdateResponse {
Err { error: String },
Ok {},
}
impl EmptyUpdateResponse {
pub fn is_ok(&self) -> bool {
if let EmptyUpdateResponse::Ok {} = &self {
true
} else {
false
}
}
pub fn is_err(&self) -> bool {
!self.is_ok()
}
}
impl TryFrom<EmptyUpdateResponse> for () {
type Error = anyhow::Error;
fn try_from(value: EmptyUpdateResponse) -> Result<Self, Self::Error> {
match value {
EmptyUpdateResponse::Err { error } => Err(anyhow!("Server returned an error: {error}")),
EmptyUpdateResponse::Ok {} => Ok(()),
}
}
}
#[derive(Serialize, Debug)]
pub struct DeleteSuggestionEngineSourceRequest {
#[serde(skip)]
pub suggestion_engine_id: i64,
#[serde(skip)]
pub source_id: i64,
}
impl UpdateRequest for DeleteSuggestionEngineSourceRequest {
type Response = EmptyUpdateResponse;
type Output = ();
fn endpoint(&self) -> Cow<'static, str> {
format!(
"se/{}/sources/{}",
self.suggestion_engine_id, self.source_id
)
.into()
}
fn method(&self) -> Method {
Method::DELETE
}
}
#[derive(Serialize, Debug)]
pub struct UpdateSuggestionEngineSourceRequest {
#[serde(skip)]
pub suggestion_engine_id: i64,
#[serde(skip)]
pub source_id: i64,
pub name: Option<String>,
}
impl UpdateRequest for UpdateSuggestionEngineSourceRequest {
type Response = EmptyUpdateResponse;
type Output = ();
fn endpoint(&self) -> Cow<'static, str> {
format!(
"se/{}/sources/{}",
self.suggestion_engine_id, self.source_id
)
.into()
}
}
#[derive(Serialize, Debug)]
pub struct CreateSuggestionEngineSourceRequest {
#[serde(skip)]
pub suggestion_engine_id: i64,
pub name: Option<String>,
#[serde(flatten)]
pub source_data: SourceTypeData,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CreateSuggestionEngineSourceResponse {
Ok { source: Source },
Err { error: String },
}
impl UpdateRequest for CreateSuggestionEngineSourceRequest {
type Response = CreateSuggestionEngineSourceResponse;
type Output = Source;
fn endpoint(&self) -> Cow<'static, str> {
format!("se/{}/sources", self.suggestion_engine_id).into()
}
fn method(&self) -> Method {
Method::PUT
}
}
impl TryFrom<CreateSuggestionEngineSourceResponse> for Source {
type Error = anyhow::Error;
fn try_from(value: CreateSuggestionEngineSourceResponse) -> Result<Self, Self::Error> {
match value {
CreateSuggestionEngineSourceResponse::Ok { source } => Ok(source),
CreateSuggestionEngineSourceResponse::Err { error } => {
Err(anyhow!("Server returned an error: {error}"))
}
}
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetTopicGroupRequest {
pub url_name: String,
pub company_id: Option<i64>,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum GetTopicGroupResponse {
#[serde(rename_all = "camelCase")]
Ok {
topic_group: TopicGroup,
},
Err {
error: String,
},
}
impl GetRequest for GetTopicGroupRequest {
type Response = GetTopicGroupResponse;
type Output = TopicGroup;
fn endpoint(&self) -> Cow<'static, str> {
"topic-group".into()
}
}
impl TryFrom<GetTopicGroupResponse> for TopicGroup {
type Error = anyhow::Error;
fn try_from(value: GetTopicGroupResponse) -> Result<Self, Self::Error> {
match value {
GetTopicGroupResponse::Ok { topic_group } => Ok(topic_group),
GetTopicGroupResponse::Err { error } => {
Err(anyhow!("Server returned an error: {error}"))
}
}
}
}
#[derive(Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct GetCompilationRequest {
pub sort: Option<GetCompilationSort>,
pub topic_ids: Option<Vec<i64>>,
pub topic_group_id: Option<i64>,
pub since: Option<i64>,
pub count: Option<u32>,
pub page: Option<u32>,
pub ncomments: Option<u32>,
pub get_tags: Option<bool>,
pub get_tags_for_topic: Option<bool>,
pub get_stats_for_topic: Option<bool>,
}
#[derive(Serialize, Debug)]
pub enum GetCompilationSort {
#[serde(rename = "rss")]
Rss,
#[serde(rename = "timeline")]
Timeline,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase", untagged)]
pub enum GetCompilationResponse {
Ok { posts: Vec<Post> },
Err { error: String },
}
impl GetRequest for GetCompilationRequest {
type Response = GetCompilationResponse;
type Output = Vec<Post>;
fn endpoint(&self) -> Cow<'static, str> {
"compilation".into()
}
}
impl TryFrom<GetCompilationResponse> for Vec<Post> {
type Error = anyhow::Error;
fn try_from(value: GetCompilationResponse) -> Result<Self, Self::Error> {
match value {
GetCompilationResponse::Ok { posts } => Ok(posts),
GetCompilationResponse::Err { error } => {
Err(anyhow!("Server returned an error: {error}"))
}
}
}
}