1use super::constants;
2use super::error::NewsApiError;
3use chrono::prelude::*;
4use serde::de::DeserializeOwned;
5use std::collections::HashMap;
6
7use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
8
9#[derive(Debug)]
10pub struct NewsAPIClient {
11 api_key: String,
12 parameters: HashMap<String, String>,
13 url: Option<String>,
14}
15
16impl NewsAPIClient {
17 pub fn new(api_key: String) -> NewsAPIClient {
24 NewsAPIClient {
25 api_key,
26 parameters: HashMap::new(),
27 url: None,
28 }
29 }
30
31 pub fn everything(&mut self) -> &mut NewsAPIClient {
33 let allowed_params = vec![
34 "q",
35 "sources",
36 "domains",
37 "excludeDomains",
38 "from",
39 "to",
40 "language",
41 "sortBy",
42 "pageSize",
43 "page",
44 ];
45
46 self.url = Some(self.build_url(constants::EVERYTHING_URL, allowed_params));
47 self
48 }
49
50 pub fn top_headlines(&mut self) -> &mut NewsAPIClient {
52 let allowed_params = vec!["q", "country", "category", "sources", "pageSize", "page"];
53 self.url = Some(self.build_url(constants::TOP_HEADLINES_URL, allowed_params));
54 self
55 }
56
57 pub fn sources(&mut self) -> &mut NewsAPIClient {
59 let allowed_params = vec!["category", "language", "country"];
60 self.url = Some(self.build_url(constants::SOURCES_URL, allowed_params));
61 self
62 }
63
64 pub async fn send_async<T>(&self) -> Result<T, NewsApiError>
66 where
67 T: DeserializeOwned,
68 {
69 if self.invalid_arguments_specified() {
70 return Err(NewsApiError::InvalidParameterCombinationError);
71 }
72
73 match &self.url {
74 Some(url) => {
75 let body = NewsAPIClient::fetch_resource_async(url, &self.api_key).await?;
76 Ok(serde_json::from_str::<T>(&body)?)
77 }
78 None => Err(NewsApiError::UndefinedUrlError),
79 }
80 }
81
82 pub fn send_sync<T>(&self) -> Result<T, NewsApiError>
84 where
85 T: DeserializeOwned,
86 {
87 if self.invalid_arguments_specified() {
88 return Err(NewsApiError::InvalidParameterCombinationError);
89 }
90
91 match &self.url {
92 Some(url) => {
93 let body = NewsAPIClient::fetch_resource_sync(url, &self.api_key)?;
94 Ok(serde_json::from_str::<T>(&body)?)
95 }
96 None => Err(NewsApiError::UndefinedUrlError),
97 }
98 }
99
100 fn build_url(&self, base_url: &str, allowed_params: Vec<&str>) -> String {
101 let mut params: Vec<String> = vec![];
102 for field in allowed_params {
103 if let Some(value) = self.parameters.get(field) {
104 params.push(format!("{}={}", field, value))
105 }
106 }
107
108 let mut url = base_url.to_owned();
109
110 if params.is_empty() {
111 url
112 } else {
113 url.push('?');
114 url.push_str(¶ms.join("&"));
115 url
116 }
117 }
118
119 fn invalid_arguments_specified(&self) -> bool {
120 (self.parameters.contains_key("country") || self.parameters.contains_key("category"))
121 && self.parameters.contains_key("sources")
122 }
123
124 fn handle_api_error(error_code: u16, error_string: String) -> NewsApiError {
125 match error_code {
126 400 => NewsApiError::BadRequest {
127 code: error_code,
128 message: error_string,
129 },
130 401 => NewsApiError::Unauthorized {
131 code: error_code,
132 message: error_string,
133 },
134 429 => NewsApiError::TooManyRequests {
135 code: error_code,
136 message: error_string,
137 },
138 500 => NewsApiError::ServerError {
139 code: error_code,
140 message: error_string,
141 },
142 _ => NewsApiError::GenericError {
143 code: error_code,
144 message: error_string,
145 },
146 }
147 }
148
149 fn create_user_agent() -> String {
150 concat!(
151 "rust-",
152 env!("CARGO_PKG_NAME"),
153 "/",
154 env!("CARGO_PKG_VERSION"),
155 )
156 .to_owned()
157 }
158
159 async fn fetch_resource_async(url: &str, api_key: &str) -> Result<String, NewsApiError> {
160 let client = reqwest::Client::builder()
162 .user_agent(NewsAPIClient::create_user_agent())
163 .build()?;
164
165 let resp = client.get(url).header("X-Api-Key", api_key).send().await?;
166
167 if resp.status().is_success() {
168 Ok(resp.text().await?)
169 } else {
170 Err(NewsAPIClient::handle_api_error(
171 resp.status().as_u16(),
172 resp.text().await?,
173 ))
174 }
175 }
176
177 fn fetch_resource_sync(url: &str, api_key: &str) -> Result<String, NewsApiError> {
178 let client = reqwest::blocking::Client::builder()
180 .user_agent(NewsAPIClient::create_user_agent())
181 .build()?;
182
183 let resp = client.get(url).header("X-Api-Key", api_key).send()?;
184
185 if resp.status().is_success() {
186 Ok(resp.text()?)
187 } else {
188 Err(NewsAPIClient::handle_api_error(
189 resp.status().as_u16(),
190 resp.text()?,
191 ))
192 }
193 }
194
195 pub fn from(&mut self, from: &DateTime<Utc>) -> &mut NewsAPIClient {
197 self.chronological_specification("from", from);
198 self
199 }
200
201 pub fn to(&mut self, to: &DateTime<Utc>) -> &mut NewsAPIClient {
203 self.chronological_specification("to", to);
204 self
205 }
206
207 fn chronological_specification(
208 &mut self,
209 operation: &str,
210 dt_val: &DateTime<Utc>,
211 ) -> &mut NewsAPIClient {
212 let dt_format = "%Y-%m-%dT%H:%M:%S";
213 self.parameters
214 .insert(operation.to_owned(), dt_val.format(dt_format).to_string());
215 self
216 }
217
218 pub fn domains(&mut self, domains: Vec<&str>) -> &mut NewsAPIClient {
221 self.manage_domains("domains", domains);
222 self
223 }
224
225 pub fn exclude_domains(&mut self, domains: Vec<&str>) -> &mut NewsAPIClient {
229 self.manage_domains("excludeDomains", domains);
230 self
231 }
232
233 fn manage_domains(&mut self, operation: &str, domains: Vec<&str>) -> &mut NewsAPIClient {
234 self.parameters
235 .insert(operation.to_owned(), domains.join(","));
236 self
237 }
238
239 pub fn country(&mut self, country: constants::Country) -> &mut NewsAPIClient {
241 self.parameters.insert(
242 "country".to_owned(),
243 constants::COUNTRY_LOOKUP[country].to_string(),
244 );
245 self
246 }
247
248 pub fn category(&mut self, category: constants::Category) -> &mut NewsAPIClient {
250 let fmtd_category = format!("{:?}", category).to_lowercase();
251 self.parameters.insert("category".to_owned(), fmtd_category);
252 self
253 }
254
255 pub fn with_sources(&mut self, sources: String) -> &mut NewsAPIClient {
259 self.parameters.insert("sources".to_owned(), sources);
260 self
261 }
262
263 pub fn query(&mut self, query: &str) -> &mut NewsAPIClient {
271 self.parameters.insert(
272 "q".to_owned(),
273 utf8_percent_encode(query, NON_ALPHANUMERIC).to_string(),
274 );
275 self
276 }
277
278 pub fn page(&mut self, page: u32) -> &mut NewsAPIClient {
279 self.parameters.insert("page".to_owned(), page.to_string());
280 self
281 }
282
283 pub fn page_size(&mut self, size: u32) -> &mut NewsAPIClient {
284 if (1..=100).contains(&size) {
285 self.parameters
286 .insert("pageSize".to_owned(), size.to_string());
287 }
288 self
289 }
290
291 pub fn language(&mut self, language: constants::Language) -> &mut NewsAPIClient {
292 self.parameters.insert(
293 "language".to_owned(),
294 constants::LANG_LOOKUP[language].to_string(),
295 );
296 self
297 }
298
299 pub fn sort_by(&mut self, sort_by: constants::SortMethod) -> &mut NewsAPIClient {
300 self.parameters.insert(
301 "sort_by".to_owned(),
302 constants::SORT_METHOD_LOOKUP[sort_by].to_string(),
303 );
304 self
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn handle_api_error() {
314 let bad_request = NewsAPIClient::handle_api_error(400, "BadRequest".into());
315 assert_eq!(bad_request.to_string(), "BadRequest: 400 => BadRequest");
316
317 let generic_error =
318 NewsAPIClient::handle_api_error(418, "Hyper Text Coffee Pot Control Protocol".into());
319 assert_eq!(
320 generic_error.to_string(),
321 "GenericError: 418 => Hyper Text Coffee Pot Control Protocol"
322 );
323 }
324
325 #[test]
326 fn query() {
327 let mut api = NewsAPIClient::new("123".to_owned());
328 api.query("Ali loves the hoff NOT Baywatch");
329 let encoded_param = api.parameters.get("q");
330 assert_eq!(
331 encoded_param,
332 Some(&"Ali%20loves%20the%20hoff%20NOT%20Baywatch".to_string())
333 );
334 }
335
336 #[test]
337 fn build_url() {
338 let mut api = NewsAPIClient::new("123".to_owned());
339 api.language(constants::Language::English);
340 api.country(constants::Country::UnitedStatesofAmerica);
341 let expected = "https://newsapi.org/v2/sources?language=en&country=us".to_owned();
342 let allowed_params = vec!["category", "language", "country"];
343 let url = api.build_url(constants::SOURCES_URL, allowed_params);
344 assert_eq!(expected, url);
345 }
346
347 #[test]
348 fn domains() {
349 let mut api = NewsAPIClient::new("123".to_owned());
350
351 assert_eq!(api.parameters.get("domains"), None);
352 assert_eq!(api.parameters.get("excludeDomains"), None);
353
354 api.domains(vec!["www.bbc.co.uk"]);
355
356 api.exclude_domains(vec!["www.facebook.com", "www.brexitbart.com"]);
357
358 assert_eq!(
359 api.parameters.get("domains"),
360 Some(&"www.bbc.co.uk".to_owned())
361 );
362
363 assert_eq!(
364 api.parameters.get("excludeDomains"),
365 Some(&"www.facebook.com,www.brexitbart.com".to_owned())
366 );
367 }
368
369 #[test]
370 fn category() {
371 let mut api = NewsAPIClient::new("123".to_owned());
372 assert_eq!(api.parameters.get("category"), None);
373 api.category(constants::Category::Science);
374 assert_eq!(api.parameters.get("category"), Some(&"science".to_owned()));
375 }
376
377 #[test]
378 fn country() {
379 let mut api = NewsAPIClient::new("123".to_owned());
380 assert_eq!(api.parameters.get("country"), None);
381 api.country(constants::Country::Germany);
382 assert_eq!(api.parameters.get("country"), Some(&"de".to_owned()));
383 }
384
385 #[test]
386 fn language() {
387 let mut api = NewsAPIClient::new("123".to_owned());
388 api.language(constants::Language::English);
389 assert_eq!(api.parameters.get("language"), Some(&"en".to_owned()));
390 }
391
392 #[test]
393 fn to_and_from() {
394 let mut api = NewsAPIClient::new("123".to_owned());
395
396 let from = Utc.ymd(2019, 7, 8).and_hms(9, 10, 11);
397 let to = Utc.ymd(2019, 7, 9).and_hms(9, 10, 11);
398
399 api.to(&to).from(&from);
400
401 assert_eq!(
402 api.parameters.get("from"),
403 Some(&"2019-07-08T09:10:11".to_owned())
404 );
405 assert_eq!(
406 api.parameters.get("to"),
407 Some(&"2019-07-09T09:10:11".to_owned())
408 );
409 }
410
411 #[test]
412 fn new() {
413 let api = NewsAPIClient::new("123".to_string());
414 assert_eq!(api.api_key, "123".to_string());
415 }
416
417 #[test]
418 fn page() {
419 let mut api = NewsAPIClient::new("123".to_owned());
420 api.page(20);
421 assert_eq!(api.parameters.get("page"), Some(&"20".to_owned()));
422 }
423
424 #[test]
425 fn page_size() {
426 let mut api = NewsAPIClient::new("123".to_owned());
427 assert_eq!(api.parameters.get("pageSize"), None);
428 api.page_size(30);
429 assert_eq!(api.parameters.get("pageSize"), Some(&"30".to_owned()));
430 api.page_size(400);
431 assert_eq!(api.parameters.get("pageSize"), Some(&"30".to_owned()));
432 }
433}