fmp_rs/endpoints/news.rs
1//! News endpoints
2
3use crate::Result;
4use crate::client::FmpClient;
5use crate::models::news::{NewsArticle, PressRelease};
6use serde::Serialize;
7
8/// News API endpoints
9pub struct News {
10 client: FmpClient,
11}
12
13#[derive(Debug, Clone, Serialize)]
14struct NewsQuery {
15 #[serde(skip_serializing_if = "Option::is_none")]
16 page: Option<u32>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 limit: Option<u32>,
19}
20
21impl News {
22 pub(crate) fn new(client: FmpClient) -> Self {
23 Self { client }
24 }
25
26 /// Get general stock news
27 ///
28 /// # Arguments
29 /// * `page` - Page number (optional)
30 /// * `limit` - Number of results per page (optional)
31 ///
32 /// # Example
33 /// ```no_run
34 /// # use fmp_rs::FmpClient;
35 /// # #[tokio::main]
36 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
37 /// let client = FmpClient::new()?;
38 /// let news = client.news().get_stock_news(Some(0), Some(50)).await?;
39 /// for article in news {
40 /// println!("{}: {}", article.symbol.unwrap_or_default(), article.title);
41 /// }
42 /// # Ok(())
43 /// # }
44 /// ```
45 pub async fn get_stock_news(
46 &self,
47 page: Option<u32>,
48 limit: Option<u32>,
49 ) -> Result<Vec<NewsArticle>> {
50 let query = NewsQuery { page, limit };
51 self.client.get_with_query("v3/fmp/articles", &query).await
52 }
53
54 /// Get news for a specific symbol
55 ///
56 /// # Arguments
57 /// * `symbol` - Stock symbol (e.g., "AAPL")
58 /// * `page` - Page number (optional)
59 /// * `limit` - Number of results per page (optional)
60 ///
61 /// # Example
62 /// ```no_run
63 /// # use fmp_rs::FmpClient;
64 /// # #[tokio::main]
65 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
66 /// let client = FmpClient::new()?;
67 /// let news = client.news().get_symbol_news("AAPL", Some(0), Some(10)).await?;
68 /// for article in news {
69 /// println!("{}: {}", article.published_date, article.title);
70 /// }
71 /// # Ok(())
72 /// # }
73 /// ```
74 pub async fn get_symbol_news(
75 &self,
76 symbol: &str,
77 page: Option<u32>,
78 limit: Option<u32>,
79 ) -> Result<Vec<NewsArticle>> {
80 let query = NewsQuery { page, limit };
81 self.client
82 .get_with_query(&format!("v3/stock_news?tickers={}", symbol), &query)
83 .await
84 }
85
86 /// Get news for multiple symbols
87 ///
88 /// # Arguments
89 /// * `symbols` - List of stock symbols
90 /// * `page` - Page number (optional)
91 /// * `limit` - Number of results per page (optional)
92 ///
93 /// # Example
94 /// ```no_run
95 /// # use fmp_rs::FmpClient;
96 /// # #[tokio::main]
97 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
98 /// let client = FmpClient::new()?;
99 /// let symbols = vec!["AAPL", "MSFT", "GOOGL"];
100 /// let news = client.news().get_symbols_news(&symbols, Some(0), Some(20)).await?;
101 /// for article in news {
102 /// println!("{}: {}", article.symbol.unwrap_or_default(), article.title);
103 /// }
104 /// # Ok(())
105 /// # }
106 /// ```
107 pub async fn get_symbols_news(
108 &self,
109 symbols: &[&str],
110 page: Option<u32>,
111 limit: Option<u32>,
112 ) -> Result<Vec<NewsArticle>> {
113 let tickers = symbols.join(",");
114 let query = NewsQuery { page, limit };
115 self.client
116 .get_with_query(&format!("v3/stock_news?tickers={}", tickers), &query)
117 .await
118 }
119
120 /// Get cryptocurrency news
121 ///
122 /// # Arguments
123 /// * `page` - Page number (optional)
124 /// * `limit` - Number of results per page (optional)
125 ///
126 /// # Example
127 /// ```no_run
128 /// # use fmp_rs::FmpClient;
129 /// # #[tokio::main]
130 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
131 /// let client = FmpClient::new()?;
132 /// let news = client.news().get_crypto_news(Some(0), Some(20)).await?;
133 /// for article in news {
134 /// println!("{}: {}", article.symbol.unwrap_or_default(), article.title);
135 /// }
136 /// # Ok(())
137 /// # }
138 /// ```
139 pub async fn get_crypto_news(
140 &self,
141 page: Option<u32>,
142 limit: Option<u32>,
143 ) -> Result<Vec<NewsArticle>> {
144 let query = NewsQuery { page, limit };
145 self.client.get_with_query("v4/crypto_news", &query).await
146 }
147
148 /// Get forex news
149 ///
150 /// # Arguments
151 /// * `page` - Page number (optional)
152 /// * `limit` - Number of results per page (optional)
153 ///
154 /// # Example
155 /// ```no_run
156 /// # use fmp_rs::FmpClient;
157 /// # #[tokio::main]
158 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
159 /// let client = FmpClient::new()?;
160 /// let news = client.news().get_forex_news(Some(0), Some(20)).await?;
161 /// for article in news {
162 /// println!("{}: {}", article.symbol.unwrap_or_default(), article.title);
163 /// }
164 /// # Ok(())
165 /// # }
166 /// ```
167 pub async fn get_forex_news(
168 &self,
169 page: Option<u32>,
170 limit: Option<u32>,
171 ) -> Result<Vec<NewsArticle>> {
172 let query = NewsQuery { page, limit };
173 self.client.get_with_query("v4/forex_news", &query).await
174 }
175
176 /// Get general news (all types)
177 ///
178 /// # Arguments
179 /// * `page` - Page number (optional)
180 /// * `limit` - Number of results per page (optional)
181 ///
182 /// # Example
183 /// ```no_run
184 /// # use fmp_rs::FmpClient;
185 /// # #[tokio::main]
186 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
187 /// let client = FmpClient::new()?;
188 /// let news = client.news().get_general_news(Some(0), Some(50)).await?;
189 /// for article in news {
190 /// println!("{}: {}", article.published_date, article.title);
191 /// }
192 /// # Ok(())
193 /// # }
194 /// ```
195 pub async fn get_general_news(
196 &self,
197 page: Option<u32>,
198 limit: Option<u32>,
199 ) -> Result<Vec<NewsArticle>> {
200 let query = NewsQuery { page, limit };
201 self.client.get_with_query("v4/general_news", &query).await
202 }
203
204 /// Get press releases for a symbol
205 ///
206 /// # Arguments
207 /// * `symbol` - Stock symbol (e.g., "AAPL")
208 /// * `page` - Page number (optional)
209 /// * `limit` - Number of results per page (optional)
210 ///
211 /// # Example
212 /// ```no_run
213 /// # use fmp_rs::FmpClient;
214 /// # #[tokio::main]
215 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
216 /// let client = FmpClient::new()?;
217 /// let releases = client.news().get_press_releases("AAPL", Some(0), Some(10)).await?;
218 /// for release in releases {
219 /// println!("{}: {}", release.date, release.title);
220 /// }
221 /// # Ok(())
222 /// # }
223 /// ```
224 pub async fn get_press_releases(
225 &self,
226 symbol: &str,
227 page: Option<u32>,
228 limit: Option<u32>,
229 ) -> Result<Vec<PressRelease>> {
230 let query = NewsQuery { page, limit };
231 self.client
232 .get_with_query(&format!("v3/press-releases/{}", symbol), &query)
233 .await
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_new() {
243 let client = FmpClient::builder().api_key("test_key").build().unwrap();
244 let news = News::new(client);
245 assert!(std::ptr::addr_of!(news.client).is_null() == false);
246 }
247
248 #[tokio::test]
249 #[ignore] // Requires API key
250 async fn test_get_stock_news() {
251 let client = FmpClient::new().unwrap();
252 let result = client.news().get_stock_news(Some(0), Some(5)).await;
253 assert!(result.is_ok());
254 let news = result.unwrap();
255 assert!(!news.is_empty());
256 }
257
258 #[tokio::test]
259 #[ignore] // Requires API key
260 async fn test_get_symbol_news() {
261 let client = FmpClient::new().unwrap();
262 let result = client
263 .news()
264 .get_symbol_news("AAPL", Some(0), Some(5))
265 .await;
266 assert!(result.is_ok());
267 let news = result.unwrap();
268 assert!(!news.is_empty());
269 }
270
271 #[tokio::test]
272 #[ignore] // Requires API key
273 async fn test_get_crypto_news() {
274 let client = FmpClient::new().unwrap();
275 let result = client.news().get_crypto_news(Some(0), Some(5)).await;
276 assert!(result.is_ok());
277 }
278
279 #[tokio::test]
280 #[ignore] // Requires API key
281 async fn test_get_forex_news() {
282 let client = FmpClient::new().unwrap();
283 let result = client.news().get_forex_news(Some(0), Some(5)).await;
284 assert!(result.is_ok());
285 }
286
287 #[tokio::test]
288 #[ignore] // Requires API key
289 async fn test_get_press_releases() {
290 let client = FmpClient::new().unwrap();
291 let result = client
292 .news()
293 .get_press_releases("AAPL", Some(0), Some(5))
294 .await;
295 assert!(result.is_ok());
296 }
297
298 #[tokio::test]
299 #[ignore] // Requires API key
300 async fn test_get_symbols_news() {
301 let client = FmpClient::new().unwrap();
302 let result = client
303 .news()
304 .get_symbols_news(&["AAPL", "MSFT"], Some(0), Some(10))
305 .await;
306 assert!(result.is_ok());
307 let news = result.unwrap();
308 // Should return news for the specified symbols
309 if !news.is_empty() {
310 assert!(news.iter().any(|n| {
311 n.symbol
312 .as_ref()
313 .map_or(false, |s| s == "AAPL" || s == "MSFT")
314 }));
315 }
316 }
317
318 #[tokio::test]
319 #[ignore] // Requires API key
320 async fn test_get_general_news() {
321 let client = FmpClient::new().unwrap();
322 let result = client.news().get_general_news(Some(0), Some(20)).await;
323 assert!(result.is_ok());
324 let news = result.unwrap();
325 assert!(!news.is_empty());
326 // General news should have titles and dates
327 assert!(news[0].title.len() > 0);
328 assert!(news[0].published_date.len() > 0);
329 }
330
331 // Edge case tests
332 #[tokio::test]
333 #[ignore] // Requires API key
334 async fn test_get_symbol_news_invalid_symbol() {
335 let client = FmpClient::new().unwrap();
336 let result = client
337 .news()
338 .get_symbol_news("INVALID_SYMBOL_12345", Some(0), Some(5))
339 .await;
340 // Should handle gracefully - either empty result or error
341 match result {
342 Ok(news) => assert!(news.is_empty()),
343 Err(_) => {} // API may return error for invalid symbols
344 }
345 }
346
347 #[tokio::test]
348 #[ignore] // Requires API key
349 async fn test_get_news_pagination_limits() {
350 let client = FmpClient::new().unwrap();
351
352 // Test large page number
353 let result = client.news().get_stock_news(Some(1000), Some(5)).await;
354 match result {
355 Ok(news) => assert!(news.is_empty()), // Should return empty for high page numbers
356 Err(_) => {} // API may return error
357 }
358
359 // Test large limit
360 let result = client.news().get_stock_news(Some(0), Some(1000)).await;
361 assert!(result.is_ok()); // Should handle large limits
362 }
363
364 #[tokio::test]
365 #[ignore] // Requires API key
366 async fn test_get_symbols_news_empty_array() {
367 let client = FmpClient::new().unwrap();
368 let result = client.news().get_symbols_news(&[], Some(0), Some(5)).await;
369 // Should handle empty symbols array
370 match result {
371 Ok(news) => assert!(news.is_empty()),
372 Err(_) => {} // API may return error for empty input
373 }
374 }
375
376 #[tokio::test]
377 #[ignore] // Requires API key
378 async fn test_get_symbols_news_mixed_valid_invalid() {
379 let client = FmpClient::new().unwrap();
380 let result = client
381 .news()
382 .get_symbols_news(&["AAPL", "INVALID_SYM", "MSFT"], Some(0), Some(10))
383 .await;
384 assert!(result.is_ok());
385 let news = result.unwrap();
386 // Should return news for valid symbols, may skip invalid ones
387 if !news.is_empty() {
388 assert!(news.iter().any(|n| {
389 n.symbol
390 .as_ref()
391 .map_or(false, |s| s == "AAPL" || s == "MSFT")
392 }));
393 }
394 }
395
396 #[tokio::test]
397 #[ignore] // Requires API key
398 async fn test_get_news_zero_page_limit() {
399 let client = FmpClient::new().unwrap();
400
401 // Test zero limit
402 let result = client.news().get_stock_news(Some(0), Some(0)).await;
403 match result {
404 Ok(news) => assert!(news.is_empty()),
405 Err(_) => {} // API may return error for zero limit
406 }
407 }
408
409 #[tokio::test]
410 #[ignore] // Requires API key
411 async fn test_get_press_releases_invalid_symbol() {
412 let client = FmpClient::new().unwrap();
413 let result = client
414 .news()
415 .get_press_releases("INVALID_SYMBOL_12345", Some(0), Some(5))
416 .await;
417 // Should handle invalid symbols gracefully
418 match result {
419 Ok(releases) => assert!(releases.is_empty()),
420 Err(_) => {} // API may return error for invalid symbols
421 }
422 }
423
424 #[tokio::test]
425 #[ignore] // Requires API key
426 async fn test_news_data_validation() {
427 let client = FmpClient::new().unwrap();
428 let result = client.news().get_stock_news(Some(0), Some(5)).await;
429 assert!(result.is_ok());
430 let news = result.unwrap();
431
432 if !news.is_empty() {
433 let article = &news[0];
434 // Validate required fields are present
435 assert!(!article.title.is_empty());
436 assert!(!article.published_date.is_empty());
437 // URL should be valid if present
438 if !article.url.is_empty() {
439 let url = &article.url;
440 assert!(url.starts_with("http://") || url.starts_with("https://"));
441 }
442 }
443 }
444}