finance_query/adapters/fmp/
news.rs1use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct StockNews {
18 pub symbol: Option<String>,
20 #[serde(rename = "publishedDate")]
22 pub published_date: Option<String>,
23 pub title: Option<String>,
25 pub image: Option<String>,
27 pub site: Option<String>,
29 pub text: Option<String>,
31 pub url: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37#[non_exhaustive]
38pub struct FmpArticle {
39 pub title: Option<String>,
41 pub date: Option<String>,
43 pub content: Option<String>,
45 pub tickers: Option<String>,
47 pub image: Option<String>,
49 pub link: Option<String>,
51 pub author: Option<String>,
53 pub site: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59#[non_exhaustive]
60pub struct FmpArticlesResponse {
61 pub content: Option<Vec<FmpArticle>>,
63 pub page: Option<u32>,
65 pub size: Option<u32>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71#[non_exhaustive]
72pub struct PressRelease {
73 pub symbol: Option<String>,
75 pub date: Option<String>,
77 pub title: Option<String>,
79 pub text: Option<String>,
81}
82
83pub async fn stock_news(tickers: &str, limit: u32) -> Result<Vec<StockNews>> {
92 let client = build_client()?;
93 let limit_str = limit.to_string();
94 client
95 .get(
96 "/api/v3/stock_news",
97 &[("tickers", tickers), ("limit", &limit_str)],
98 )
99 .await
100}
101
102pub async fn fmp_articles(page: u32, size: u32) -> Result<FmpArticlesResponse> {
107 let client = build_client()?;
108 let page_str = page.to_string();
109 let size_str = size.to_string();
110 client
111 .get(
112 "/api/v3/fmp/articles",
113 &[("page", &*page_str), ("size", &*size_str)],
114 )
115 .await
116}
117
118pub async fn press_releases(symbol: &str, limit: u32) -> Result<Vec<PressRelease>> {
120 let client = build_client()?;
121 let path = format!("/api/v3/press-releases/{}", encode_path_segment(symbol));
122 let limit_str = limit.to_string();
123 client.get(&path, &[("limit", &*limit_str)]).await
124}
125
126pub async fn crypto_news(limit: u32) -> Result<Vec<StockNews>> {
128 let client = build_client()?;
129 let size_str = limit.to_string();
130 client
131 .get("/api/v4/crypto_news", &[("page", "0"), ("size", &size_str)])
132 .await
133}
134
135pub async fn forex_news(limit: u32) -> Result<Vec<StockNews>> {
137 let client = build_client()?;
138 let size_str = limit.to_string();
139 client
140 .get("/api/v4/forex_news", &[("page", "0"), ("size", &size_str)])
141 .await
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[tokio::test]
149 async fn test_stock_news_mock() {
150 let mut server = mockito::Server::new_async().await;
151 let _mock = server
152 .mock("GET", "/api/v3/stock_news")
153 .match_query(mockito::Matcher::AllOf(vec![
154 mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
155 mockito::Matcher::UrlEncoded("tickers".into(), "AAPL".into()),
156 mockito::Matcher::UrlEncoded("limit".into(), "5".into()),
157 ]))
158 .with_status(200)
159 .with_body(
160 serde_json::json!([
161 {
162 "symbol": "AAPL",
163 "publishedDate": "2024-01-15 12:00:00",
164 "title": "Apple Reports Record Quarter",
165 "image": "https://example.com/image.jpg",
166 "site": "Reuters",
167 "text": "Apple Inc. reported record quarterly earnings...",
168 "url": "https://example.com/article"
169 }
170 ])
171 .to_string(),
172 )
173 .create_async()
174 .await;
175
176 let client = super::super::build_test_client(&server.url()).unwrap();
177 let resp: Vec<StockNews> = client
178 .get("/api/v3/stock_news", &[("tickers", "AAPL"), ("limit", "5")])
179 .await
180 .unwrap();
181 assert_eq!(resp.len(), 1);
182 assert_eq!(resp[0].symbol.as_deref(), Some("AAPL"));
183 assert_eq!(resp[0].site.as_deref(), Some("Reuters"));
184 }
185
186 #[tokio::test]
187 async fn test_press_releases_mock() {
188 let mut server = mockito::Server::new_async().await;
189 let _mock = server
190 .mock("GET", "/api/v3/press-releases/AAPL")
191 .match_query(mockito::Matcher::AllOf(vec![
192 mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
193 mockito::Matcher::UrlEncoded("limit".into(), "10".into()),
194 ]))
195 .with_status(200)
196 .with_body(
197 serde_json::json!([
198 {
199 "symbol": "AAPL",
200 "date": "2024-01-15",
201 "title": "Apple Announces New Product",
202 "text": "Cupertino, CA -- Apple today announced..."
203 }
204 ])
205 .to_string(),
206 )
207 .create_async()
208 .await;
209
210 let client = super::super::build_test_client(&server.url()).unwrap();
211 let resp: Vec<PressRelease> = client
212 .get("/api/v3/press-releases/AAPL", &[("limit", "10")])
213 .await
214 .unwrap();
215 assert_eq!(resp.len(), 1);
216 assert_eq!(
217 resp[0].title.as_deref(),
218 Some("Apple Announces New Product")
219 );
220 }
221}