finance_query/adapters/fmp/
company.rs1use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct CompanyProfile {
16 pub symbol: Option<String>,
18 pub price: Option<f64>,
20 pub beta: Option<f64>,
22 #[serde(rename = "volAvg")]
24 pub vol_avg: Option<f64>,
25 #[serde(rename = "mktCap")]
27 pub mkt_cap: Option<f64>,
28 #[serde(rename = "lastDiv")]
30 pub last_div: Option<f64>,
31 pub range: Option<String>,
33 pub changes: Option<f64>,
35 #[serde(rename = "companyName")]
37 pub company_name: Option<String>,
38 pub currency: Option<String>,
40 pub cik: Option<String>,
42 pub isin: Option<String>,
44 pub cusip: Option<String>,
46 pub exchange: Option<String>,
48 #[serde(rename = "exchangeShortName")]
50 pub exchange_short_name: Option<String>,
51 pub industry: Option<String>,
53 pub website: Option<String>,
55 pub description: Option<String>,
57 pub ceo: Option<String>,
59 pub sector: Option<String>,
61 pub country: Option<String>,
63 #[serde(rename = "fullTimeEmployees")]
65 pub full_time_employees: Option<String>,
66 pub phone: Option<String>,
68 pub address: Option<String>,
70 pub city: Option<String>,
72 pub state: Option<String>,
74 pub zip: Option<String>,
76 #[serde(rename = "dcfDiff")]
78 pub dcf_diff: Option<f64>,
79 pub dcf: Option<f64>,
81 pub image: Option<String>,
83 #[serde(rename = "ipoDate")]
85 pub ipo_date: Option<String>,
86 #[serde(rename = "defaultImage")]
88 pub default_image: Option<bool>,
89 #[serde(rename = "isEtf")]
91 pub is_etf: Option<bool>,
92 #[serde(rename = "isActivelyTrading")]
94 pub is_actively_trading: Option<bool>,
95 #[serde(rename = "isAdr")]
97 pub is_adr: Option<bool>,
98 #[serde(rename = "isFund")]
100 pub is_fund: Option<bool>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105#[non_exhaustive]
106pub struct KeyExecutive {
107 pub title: Option<String>,
109 pub name: Option<String>,
111 pub pay: Option<f64>,
113 #[serde(rename = "currencyPay")]
115 pub currency_pay: Option<String>,
116 pub gender: Option<String>,
118 #[serde(rename = "yearBorn")]
120 pub year_born: Option<i32>,
121 #[serde(rename = "titleSince")]
123 pub title_since: Option<String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128#[non_exhaustive]
129pub struct MarketCap {
130 pub symbol: Option<String>,
132 pub date: Option<String>,
134 #[serde(rename = "marketCap")]
136 pub market_cap: Option<f64>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141#[non_exhaustive]
142pub struct CompanyOutlook {
143 pub profile: Option<CompanyProfile>,
145 pub metrics: Option<serde_json::Value>,
147 pub ratios: Option<Vec<serde_json::Value>>,
149 #[serde(rename = "insideTrades")]
151 pub inside_trades: Option<Vec<serde_json::Value>>,
152 #[serde(rename = "keyExecutives")]
154 pub key_executives: Option<Vec<KeyExecutive>>,
155 #[serde(rename = "stockNews")]
157 pub stock_news: Option<Vec<serde_json::Value>>,
158 pub rating: Option<Vec<serde_json::Value>>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164#[non_exhaustive]
165pub struct StockPeers {
166 pub symbol: Option<String>,
168 #[serde(rename = "peersList")]
170 pub peers_list: Option<Vec<String>>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175#[non_exhaustive]
176pub struct DelistedCompany {
177 pub symbol: Option<String>,
179 #[serde(rename = "companyName")]
181 pub company_name: Option<String>,
182 pub exchange: Option<String>,
184 #[serde(rename = "ipoDate")]
186 pub ipo_date: Option<String>,
187 #[serde(rename = "delistedDate")]
189 pub delisted_date: Option<String>,
190}
191
192pub async fn company_profile(symbol: &str) -> Result<Vec<CompanyProfile>> {
198 let client = super::build_client()?;
199 client
200 .get(
201 &format!("/api/v3/profile/{}", encode_path_segment(symbol)),
202 &[],
203 )
204 .await
205}
206
207pub async fn key_executives(symbol: &str) -> Result<Vec<KeyExecutive>> {
209 let client = super::build_client()?;
210 client
211 .get(
212 &format!("/api/v3/key-executives/{}", encode_path_segment(symbol)),
213 &[],
214 )
215 .await
216}
217
218pub async fn market_cap(symbol: &str) -> Result<Vec<MarketCap>> {
220 let client = super::build_client()?;
221 client
222 .get(
223 &format!(
224 "/api/v3/market-capitalization/{}",
225 encode_path_segment(symbol)
226 ),
227 &[],
228 )
229 .await
230}
231
232pub async fn historical_market_cap(symbol: &str, limit: Option<u32>) -> Result<Vec<MarketCap>> {
234 let client = super::build_client()?;
235 let limit_str = limit.unwrap_or(100).to_string();
236 client
237 .get(
238 &format!(
239 "/api/v3/historical-market-capitalization/{}",
240 encode_path_segment(symbol)
241 ),
242 &[("limit", &limit_str)],
243 )
244 .await
245}
246
247pub async fn company_outlook(symbol: &str) -> Result<CompanyOutlook> {
249 let client = super::build_client()?;
250 client
251 .get("/api/v4/company-outlook", &[("symbol", symbol)])
252 .await
253}
254
255pub async fn stock_peers(symbol: &str) -> Result<Vec<StockPeers>> {
257 let client = super::build_client()?;
258 client
259 .get("/api/v4/stock_peers", &[("symbol", symbol)])
260 .await
261}
262
263pub async fn delisted_companies(limit: Option<u32>) -> Result<Vec<DelistedCompany>> {
265 let client = super::build_client()?;
266 let limit_str = limit.unwrap_or(100).to_string();
267 client
268 .get("/api/v3/delisted-companies", &[("limit", &limit_str)])
269 .await
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[tokio::test]
277 async fn test_company_profile_mock() {
278 let mut server = mockito::Server::new_async().await;
279 let _mock = server
280 .mock("GET", "/api/v3/profile/AAPL")
281 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
282 "apikey".into(),
283 "test-key".into(),
284 )]))
285 .with_status(200)
286 .with_body(
287 serde_json::json!([{
288 "symbol": "AAPL",
289 "price": 178.72,
290 "beta": 1.286,
291 "volAvg": 58405568,
292 "mktCap": 2794000000000_f64,
293 "companyName": "Apple Inc.",
294 "currency": "USD",
295 "exchange": "NASDAQ Global Select",
296 "exchangeShortName": "NASDAQ",
297 "industry": "Consumer Electronics",
298 "sector": "Technology",
299 "country": "US",
300 "ceo": "Mr. Timothy D. Cook",
301 "isEtf": false,
302 "isActivelyTrading": true
303 }])
304 .to_string(),
305 )
306 .create_async()
307 .await;
308
309 let client = super::super::build_test_client(&server.url()).unwrap();
310 let result: Vec<CompanyProfile> = client.get("/api/v3/profile/AAPL", &[]).await.unwrap();
311
312 assert_eq!(result.len(), 1);
313 assert_eq!(result[0].symbol.as_deref(), Some("AAPL"));
314 assert_eq!(result[0].company_name.as_deref(), Some("Apple Inc."));
315 assert_eq!(result[0].sector.as_deref(), Some("Technology"));
316 assert_eq!(result[0].is_etf, Some(false));
317 }
318
319 #[tokio::test]
320 async fn test_key_executives_mock() {
321 let mut server = mockito::Server::new_async().await;
322 let _mock = server
323 .mock("GET", "/api/v3/key-executives/AAPL")
324 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
325 "apikey".into(),
326 "test-key".into(),
327 )]))
328 .with_status(200)
329 .with_body(
330 serde_json::json!([
331 {
332 "title": "Chief Executive Officer",
333 "name": "Mr. Timothy D. Cook",
334 "pay": 16425933,
335 "currencyPay": "USD",
336 "gender": "male",
337 "yearBorn": 1960
338 },
339 {
340 "title": "Chief Financial Officer",
341 "name": "Mr. Luca Maestri",
342 "pay": 5019783,
343 "currencyPay": "USD",
344 "gender": "male",
345 "yearBorn": 1963
346 }
347 ])
348 .to_string(),
349 )
350 .create_async()
351 .await;
352
353 let client = super::super::build_test_client(&server.url()).unwrap();
354 let result: Vec<KeyExecutive> = client
355 .get("/api/v3/key-executives/AAPL", &[])
356 .await
357 .unwrap();
358
359 assert_eq!(result.len(), 2);
360 assert_eq!(result[0].name.as_deref(), Some("Mr. Timothy D. Cook"));
361 assert_eq!(result[0].pay, Some(16425933.0));
362 }
363
364 #[tokio::test]
365 async fn test_fmp_rate_limit_returns_rate_limited_error() {
366 let mut server = mockito::Server::new_async().await;
367 let _mock = server
368 .mock("GET", mockito::Matcher::Any)
369 .with_status(429)
370 .with_body("{}")
371 .create_async()
372 .await;
373
374 let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
375 let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
376
377 assert!(matches!(
378 result,
379 Err(crate::error::FinanceError::RateLimited { .. })
380 ));
381 }
382
383 #[tokio::test]
384 async fn test_fmp_401_returns_authentication_failed() {
385 let mut server = mockito::Server::new_async().await;
386 let _mock = server
387 .mock("GET", mockito::Matcher::Any)
388 .with_status(401)
389 .with_body("{}")
390 .create_async()
391 .await;
392
393 let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
394 let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
395
396 assert!(matches!(
397 result,
398 Err(crate::error::FinanceError::AuthenticationFailed { .. })
399 ));
400 }
401
402 #[tokio::test]
403 async fn test_fmp_body_error_message_returns_invalid_parameter() {
404 let mut server = mockito::Server::new_async().await;
405 let _mock = server
406 .mock("GET", mockito::Matcher::Any)
407 .with_status(200)
408 .with_body(r#"{"Error Message":"Invalid API KEY."}"#)
409 .create_async()
410 .await;
411
412 let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
413 let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
414
415 assert!(matches!(
416 result,
417 Err(crate::error::FinanceError::InvalidParameter { .. })
418 ));
419 }
420
421 #[tokio::test]
422 async fn test_fmp_500_returns_server_error() {
423 let mut server = mockito::Server::new_async().await;
424 let _mock = server
425 .mock("GET", mockito::Matcher::Any)
426 .with_status(500)
427 .with_body("{}")
428 .create_async()
429 .await;
430
431 let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
432 let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
433
434 assert!(matches!(
435 result,
436 Err(crate::error::FinanceError::ServerError { .. })
437 ));
438 }
439}