use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
pub struct Article {
pub uuid: String,
pub title: String,
pub description: String,
pub keywords: Option<String>,
pub snippet: String,
pub url: String,
pub image_url: Option<String>,
pub language: String,
pub published_at: String,
pub source: String,
pub categories: Vec<String>,
pub locale: Option<String>,
pub similar: Option<Vec<SimilarArticle>>,
}
#[derive(Deserialize, Debug)]
pub struct SimilarArticle {
pub uuid: String,
pub title: String,
pub description: String,
pub keywords: Option<String>,
pub snippet: String,
pub url: String,
pub image_url: Option<String>,
pub language: String,
pub published_at: String,
pub source: String,
pub categories: Vec<String>,
pub locale: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct Meta {
pub found: usize,
pub returned: usize,
pub limit: usize,
pub page: usize,
}
#[derive(Deserialize, Debug)]
pub struct HeadlinesResponse {
pub data: HashMap<String, Vec<Article>>,
}
#[derive(Deserialize, Debug)]
pub struct TopStoriesResponse {
pub meta: Meta,
pub data: Vec<Article>,
}
#[derive(Deserialize, Debug)]
pub struct AllNewsResponse {
pub meta: Meta,
pub data: Vec<Article>,
}
#[derive(Deserialize, Debug)]
pub struct SimilarNewsResponse {
pub meta: Meta,
pub data: Vec<Article>,
}
#[derive(Deserialize, Debug)]
pub struct ArticleByUuidResponse {
pub uuid: String,
pub title: String,
pub description: String,
pub keywords: Option<String>,
pub snippet: String,
pub url: String,
pub image_url: Option<String>,
pub language: String,
pub published_at: String,
pub source: String,
pub categories: Vec<String>,
}
#[derive(Deserialize, Debug)]
pub struct Source {
pub source_id: String,
pub domain: String,
pub language: String,
pub locale: Option<String>,
pub categories: Vec<String>,
}
#[derive(Deserialize, Debug)]
pub struct SourcesResponse {
pub meta: Meta,
pub data: Vec<Source>,
}
#[derive(Serialize, Default)]
pub struct HeadlinesParams<'a> {
pub locale: Option<&'a str>,
pub domains: Option<&'a str>,
pub exclude_domains: Option<&'a str>,
pub source_ids: Option<&'a str>,
pub exclude_source_ids: Option<&'a str>,
pub language: Option<&'a str>,
pub published_on: Option<&'a str>,
pub headlines_per_category: Option<usize>,
pub include_similar: Option<bool>,
}
#[derive(Serialize, Default)]
pub struct TopStoriesParams<'a> {
pub search: Option<&'a str>,
pub search_fields: Option<&'a str>,
pub locale: Option<&'a str>,
pub categories: Option<&'a str>,
pub exclude_categories: Option<&'a str>,
pub domains: Option<&'a str>,
pub exclude_domains: Option<&'a str>,
pub source_ids: Option<&'a str>,
pub exclude_source_ids: Option<&'a str>,
pub language: Option<&'a str>,
pub published_before: Option<&'a str>,
pub published_after: Option<&'a str>,
pub published_on: Option<&'a str>,
pub sort: Option<&'a str>,
pub limit: Option<usize>,
pub page: Option<usize>,
}
#[derive(Serialize, Default)]
pub struct AllNewsParams<'a> {
pub search: Option<&'a str>,
pub search_fields: Option<&'a str>,
pub locale: Option<&'a str>,
pub categories: Option<&'a str>,
pub exclude_categories: Option<&'a str>,
pub domains: Option<&'a str>,
pub exclude_domains: Option<&'a str>,
pub source_ids: Option<&'a str>,
pub exclude_source_ids: Option<&'a str>,
pub language: Option<&'a str>,
pub published_before: Option<&'a str>,
pub published_after: Option<&'a str>,
pub published_on: Option<&'a str>,
pub sort: Option<&'a str>,
pub limit: Option<usize>,
pub page: Option<usize>,
}
#[derive(Serialize, Default)]
pub struct SimilarNewsParams<'a> {
pub categories: Option<&'a str>,
pub exclude_categories: Option<&'a str>,
pub domains: Option<&'a str>,
pub exclude_domains: Option<&'a str>,
pub source_ids: Option<&'a str>,
pub exclude_source_ids: Option<&'a str>,
pub language: Option<&'a str>,
pub published_before: Option<&'a str>,
pub published_after: Option<&'a str>,
pub published_on: Option<&'a str>,
pub limit: Option<usize>,
pub page: Option<usize>,
}
#[derive(Serialize, Default)]
pub struct SourcesParams<'a> {
pub categories: Option<&'a str>,
pub exclude_categories: Option<&'a str>,
pub language: Option<&'a str>,
pub page: Option<usize>,
}
pub struct Client {
client: reqwest::Client,
api_token: String,
}
impl Client {
pub fn new(api_token: &str) -> Self {
Client {
client: reqwest::Client::new(),
api_token: api_token.to_string(),
}
}
pub async fn get_headlines(&self, params: HeadlinesParams<'_>) -> Result<HeadlinesResponse> {
self.get("https://api.thenewsapi.com/v1/news/headlines", params)
.await
}
pub async fn get_top_stories(
&self,
params: TopStoriesParams<'_>,
) -> Result<TopStoriesResponse> {
self.get("https://api.thenewsapi.com/v1/news/top", params)
.await
}
pub async fn get_all_news(&self, params: AllNewsParams<'_>) -> Result<AllNewsResponse> {
self.get("https://api.thenewsapi.com/v1/news/all", params)
.await
}
pub async fn get_similar_news(
&self,
uuid: &str,
params: SimilarNewsParams<'_>,
) -> Result<SimilarNewsResponse> {
self.get(
&format!("https://api.thenewsapi.com/v1/news/similar/{}", uuid),
params,
)
.await
}
pub async fn get_article_by_uuid(&self, uuid: &str) -> Result<ArticleByUuidResponse> {
self.get(
&format!("https://api.thenewsapi.com/v1/news/uuid/{}", uuid),
(),
)
.await
}
pub async fn get_sources(&self, params: SourcesParams<'_>) -> Result<SourcesResponse> {
self.get("https://api.thenewsapi.com/v1/sources", params)
.await
}
async fn get<T: Serialize, U: for<'de> Deserialize<'de>>(
&self,
url: &str,
params: T,
) -> Result<U> {
let query = self.build_query(params);
let response = self.client.get(url).query(&query).send().await?;
if response.status().is_success() {
let parsed_response = response.json::<U>().await?;
Ok(parsed_response)
} else {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| String::from("Failed to read response text"));
Err(anyhow!("HTTP {}: {}", status, error_text))
}
}
fn build_query<T: Serialize>(&self, params: T) -> HashMap<String, String> {
let mut query = serde_json::to_value(params)
.unwrap_or(json!({}))
.as_object()
.unwrap()
.clone();
query.insert("api_token".to_string(), json!(self.api_token));
query
.into_iter()
.filter(|(_, v)| !v.is_null())
.map(|(k, v)| {
let value_str =
if k == "published_on" || k == "published_before" || k == "published_after" {
format!("{}", v.as_str().unwrap_or("").to_string())
} else {
v.as_str().unwrap_or("").to_string()
};
(k, value_str)
})
.collect()
}
}