pub mod enums;
pub mod error;
pub mod structs;
mod tests;
use crate::enums::*;
use crate::error::Error;
use crate::structs::*;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{Client, Url};
use std::collections::HashMap;
use std::string::ToString;
const GUARDIAN_CONTENT_API_URL: &str = "https://content.guardianapis.com";
#[derive(Clone, Debug)]
pub struct GuardianContentClient {
http_client: reqwest::Client,
api_key: String,
base_url: Url,
}
#[derive(Debug, Clone)]
pub struct GuardianRequestBuilder {
http_client: Client,
base_url: Url,
api_key: String,
request: HashMap<String, String>,
endpoint: Endpoint,
}
impl GuardianRequestBuilder {
pub fn endpoint(mut self, endpoint: Endpoint) -> Self {
self.endpoint = endpoint;
self
}
pub fn search(mut self, q: &str) -> Self {
self.request.insert(String::from("q"), q.to_string());
self
}
pub fn page(mut self, page: u32) -> GuardianRequestBuilder {
self.request.insert(String::from("page"), page.to_string());
self
}
pub fn page_size(mut self, page: u8) -> GuardianRequestBuilder {
self.request
.insert(String::from("page-size"), page.to_string());
self
}
pub fn order_by(mut self, order_by: OrderBy) -> GuardianRequestBuilder {
self.request
.insert(String::from("order-by"), order_by.to_string());
self
}
pub fn order_date(mut self, order_date: OrderDate) -> GuardianRequestBuilder {
self.request
.insert(String::from("order-date"), order_date.to_string());
self
}
pub fn show_fields(mut self, show_fields: Vec<Field>) -> GuardianRequestBuilder {
let field_sequence = crate::helpers::generate_sequence(show_fields);
self.request
.insert(String::from("show-fields"), field_sequence);
self
}
pub fn show_tags(mut self, show_tags: Vec<enums::Tag>) -> GuardianRequestBuilder {
let tag_sequence = crate::helpers::generate_sequence(show_tags);
self.request.insert(String::from("show-tags"), tag_sequence);
self
}
pub fn query_fields(mut self, query_fields: Vec<Field>) -> GuardianRequestBuilder {
let field_sequence = crate::helpers::generate_sequence(query_fields);
self.request
.insert(String::from("query-fields"), field_sequence);
self
}
pub fn date_from(mut self, year: i32, month: u32, day: u32) -> GuardianRequestBuilder {
self.request
.insert(String::from("from-date"), format!("{year}-{month}-{day}"));
self
}
#[allow(clippy::too_many_arguments)]
pub fn datetime_from(
mut self,
year: i32,
month: u32,
day: u32,
hour: u32,
min: u32,
sec: u32,
timezone: i32,
) -> GuardianRequestBuilder {
let formatted_datetime =
crate::helpers::datetime(year, month, day, hour, min, sec, timezone);
if !formatted_datetime.is_empty() {
self.request
.insert(String::from("from-date"), formatted_datetime);
}
self
}
pub fn date_to(mut self, year: i32, month: u32, day: u32) -> GuardianRequestBuilder {
self.request
.insert(String::from("to-date"), format!("{year}-{month}-{day}"));
self
}
#[allow(clippy::too_many_arguments)]
pub fn datetime_to(
mut self,
year: i32,
month: u32,
day: u32,
hour: u32,
min: u32,
sec: u32,
timezone: i32,
) -> GuardianRequestBuilder {
let formatted_datetime =
crate::helpers::datetime(year, month, day, hour, min, sec, timezone);
if !formatted_datetime.is_empty() {
self.request
.insert(String::from("to-date"), formatted_datetime);
}
self
}
pub fn use_date(mut self, use_date: UseDate) -> GuardianRequestBuilder {
self.request
.insert(String::from("use-date"), use_date.to_string());
self
}
pub fn show_section(mut self, show_section: bool) -> GuardianRequestBuilder {
self.request
.insert(String::from("show-section"), show_section.to_string());
self
}
pub fn section(mut self, section: &str) -> GuardianRequestBuilder {
self.request
.insert(String::from("section"), section.to_string());
self
}
pub fn reference(mut self, reference: &str) -> GuardianRequestBuilder {
self.request
.insert(String::from("reference"), reference.to_string());
self
}
pub fn reference_type(mut self, reference_type: &str) -> GuardianRequestBuilder {
self.request
.insert(String::from("reference-type"), reference_type.to_string());
self
}
pub fn tag(mut self, tag: &str) -> GuardianRequestBuilder {
self.request.insert(String::from("tag"), tag.to_string());
self
}
pub fn ids(mut self, ids: &str) -> GuardianRequestBuilder {
self.request.insert(String::from("ids"), ids.to_string());
self
}
pub fn production_office(mut self, production_office: &str) -> GuardianRequestBuilder {
self.request.insert(
String::from("production-office"),
production_office.to_string(),
);
self
}
pub fn lang(mut self, lang: &str) -> GuardianRequestBuilder {
self.request.insert(String::from("lang"), lang.to_string());
self
}
pub fn star_rating(mut self, star_rating: u8) -> GuardianRequestBuilder {
self.request
.insert(String::from("star-rating"), star_rating.to_string());
self
}
pub fn tag_type(mut self, r#type: &str) -> GuardianRequestBuilder {
self.request
.insert(String::from("type"), r#type.to_string());
self
}
pub fn show_blocks(mut self, show_blocks: Vec<enums::Block>) -> GuardianRequestBuilder {
let block_sequence = crate::helpers::generate_blocks(show_blocks);
self.request
.insert(String::from("show-blocks"), block_sequence);
self
}
pub async fn send(&mut self) -> Result<SearchResponse, Error> {
let mut headers = HeaderMap::new();
if !self.api_key.is_empty() {
headers.insert("api-key", HeaderValue::from_str(&self.api_key).unwrap());
}
let endpoint = match self.endpoint {
Endpoint::Content => String::from("search"),
Endpoint::Tags => self.endpoint.to_string(),
Endpoint::Sections => self.endpoint.to_string(),
Endpoint::Editions => self.endpoint.to_string(),
Endpoint::SingleItem => self
.request
.get("q")
.ok_or(Error::MissingQueryParameter("q"))?
.to_owned(),
};
let queries = Vec::from_iter(self.request.iter());
let mut url = self.base_url.clone();
url.path_segments_mut().unwrap().push(&endpoint);
let search = self
.http_client
.get(url)
.headers(headers)
.query(&queries)
.send()
.await?
.json::<Response>()
.await?;
if let Some(err) = search.message {
return Err(Error::ApiError(err));
}
if let Some(response_content) = &search.response {
if response_content.status.as_deref() == Some("error") {
if let Some(message) = &response_content.message {
return Err(Error::ApiError(message.to_owned()));
}
}
}
self.request.clear();
match search.response {
Some(r) => Ok(r),
None => Ok(crate::helpers::mock_response()),
}
}
}
impl GuardianContentClient {
pub fn new(api_key: &str) -> GuardianContentClient {
Self {
http_client: Client::new(),
base_url: Url::parse(GUARDIAN_CONTENT_API_URL).unwrap(),
api_key: String::from(api_key),
}
}
pub fn build_request(&self) -> GuardianRequestBuilder {
GuardianRequestBuilder {
http_client: self.http_client.clone(),
base_url: self.base_url.clone(),
api_key: self.api_key.clone(),
request: HashMap::new(),
endpoint: Endpoint::default(),
}
}
}
mod helpers {
use crate::enums::{Block, IsAll};
use crate::SearchResponse;
use chrono::{FixedOffset, LocalResult, TimeZone};
use std::fmt::Display;
pub(crate) fn generate_sequence<T>(items: Vec<T>) -> String
where
T: Display + IsAll,
{
let mut sequence = String::new();
let mut first = true;
for item in items {
if item.is_all() {
return "all".to_owned();
}
if !first {
sequence.push(',');
} else {
first = false;
}
sequence.push_str(&item.to_string());
}
sequence
}
pub(crate) fn generate_blocks(items: Vec<Block>) -> String {
if items.contains(&Block::All) {
return "all".to_owned();
}
let items_to_strings = items
.into_iter()
.map(|item| match item {
Block::Main => item.to_string(),
Block::Body => item.to_string(),
Block::All => item.to_string(),
Block::BodyLatest => String::from("body:latest"),
Block::BodyLatestWith(n) => format!("body:latest:{n}"),
Block::BodyOldest => String::from("body:latest"),
Block::BodyOldestWith(n) => format!("body:oldest:{n}"),
Block::BodyBlockId(id) => format!("body:{id}"),
Block::BodyAroundBlockId(id) => format!("body:around:{id}"),
Block::BodyAroundBlockIdWith(id, n) => {
format!("body:around:{id}:{n}")
}
Block::BodyKeyEvents => String::from("body:key-events"),
Block::BodyPublishedSince(n) => format!("body:published-since:{n}"),
})
.collect::<Vec<String>>();
items_to_strings.join(",")
}
pub(crate) fn datetime(
year: i32,
month: u32,
day: u32,
hour: u32,
min: u32,
sec: u32,
timezone: i32,
) -> String {
let offset: fn(i32) -> Option<FixedOffset> = if timezone >= 0 {
FixedOffset::east_opt
} else {
FixedOffset::west_opt
};
let offset =
offset(timezone.abs() * 3600).unwrap_or_else(|| FixedOffset::west_opt(0).unwrap());
if let LocalResult::Single(date) = offset.with_ymd_and_hms(year, month, day, hour, min, sec)
{
date.to_rfc3339()
} else {
String::new()
}
}
pub(crate) fn mock_response() -> SearchResponse {
SearchResponse {
status: None,
user_tier: None,
total: None,
start_index: None,
page_size: None,
current_page: None,
pages: None,
order_by: None,
results: None,
message: None,
content: None,
}
}
}