fmp_rs/endpoints/
company_info.rs

1//! Company information endpoints
2
3use crate::client::FmpClient;
4use crate::error::Result;
5use crate::models::company::{
6    CompanyCoreInfo, CompanyOutlook, CompanyProfile, CompanyRating, Executive, MarketCap,
7    ShareFloat, StockPeers,
8};
9use serde::Serialize;
10
11/// Company information API endpoints
12pub struct CompanyInfo {
13    client: FmpClient,
14}
15
16impl CompanyInfo {
17    pub(crate) fn new(client: FmpClient) -> Self {
18        Self { client }
19    }
20
21    /// Get company profile
22    pub async fn get_profile(&self, symbol: &str) -> Result<Vec<CompanyProfile>> {
23        #[derive(Serialize)]
24        struct Query<'a> {
25            symbol: &'a str,
26            apikey: &'a str,
27        }
28
29        let url = self.client.build_url("/profile");
30        self.client
31            .get_with_query(
32                &url,
33                &Query {
34                    symbol,
35                    apikey: self.client.api_key(),
36                },
37            )
38            .await
39    }
40
41    /// Get company executives
42    pub async fn get_executives(&self, symbol: &str) -> Result<Vec<Executive>> {
43        #[derive(Serialize)]
44        struct Query<'a> {
45            symbol: &'a str,
46            apikey: &'a str,
47        }
48
49        let url = self.client.build_url("/key-executives");
50        self.client
51            .get_with_query(
52                &url,
53                &Query {
54                    symbol,
55                    apikey: self.client.api_key(),
56                },
57            )
58            .await
59    }
60
61    /// Get company market cap
62    pub async fn get_market_cap(&self, symbol: &str) -> Result<Vec<MarketCap>> {
63        #[derive(Serialize)]
64        struct Query<'a> {
65            symbol: &'a str,
66            apikey: &'a str,
67        }
68
69        let url = self.client.build_url("/market-capitalization");
70        self.client
71            .get_with_query(
72                &url,
73                &Query {
74                    symbol,
75                    apikey: self.client.api_key(),
76                },
77            )
78            .await
79    }
80
81    /// Get historical market cap
82    pub async fn get_historical_market_cap(
83        &self,
84        symbol: &str,
85        from: Option<&str>,
86        to: Option<&str>,
87    ) -> Result<Vec<MarketCap>> {
88        #[derive(Serialize)]
89        struct Query<'a> {
90            symbol: &'a str,
91            #[serde(skip_serializing_if = "Option::is_none")]
92            from: Option<&'a str>,
93            #[serde(skip_serializing_if = "Option::is_none")]
94            to: Option<&'a str>,
95            apikey: &'a str,
96        }
97
98        let url = self.client.build_url("/historical-market-capitalization");
99        self.client
100            .get_with_query(
101                &url,
102                &Query {
103                    symbol,
104                    from,
105                    to,
106                    apikey: self.client.api_key(),
107                },
108            )
109            .await
110    }
111
112    /// Get share float
113    pub async fn get_share_float(&self, symbol: &str) -> Result<Vec<ShareFloat>> {
114        #[derive(Serialize)]
115        struct Query<'a> {
116            symbol: &'a str,
117            apikey: &'a str,
118        }
119
120        let url = self.client.build_url("/shares-float");
121        self.client
122            .get_with_query(
123                &url,
124                &Query {
125                    symbol,
126                    apikey: self.client.api_key(),
127                },
128            )
129            .await
130    }
131
132    /// Get stock peers
133    pub async fn get_peers(&self, symbol: &str) -> Result<Vec<StockPeers>> {
134        #[derive(Serialize)]
135        struct Query<'a> {
136            symbol: &'a str,
137            apikey: &'a str,
138        }
139
140        let url = self.client.build_url("/stock-peers");
141        self.client
142            .get_with_query(
143                &url,
144                &Query {
145                    symbol,
146                    apikey: self.client.api_key(),
147                },
148            )
149            .await
150    }
151
152    /// Get comprehensive company outlook
153    ///
154    /// Returns a comprehensive view of company data including profile, metrics,
155    /// ratios, insider trades, executives, and more.
156    ///
157    /// # Arguments
158    /// * `symbol` - Stock symbol (e.g., "AAPL")
159    ///
160    /// # Example
161    /// ```no_run
162    /// # use fmp_rs::FmpClient;
163    /// # #[tokio::main]
164    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
165    /// let client = FmpClient::new()?;
166    /// let outlook = client.company_info().get_outlook("AAPL").await?;
167    /// println!("Company: {}", outlook.profile.company_name);
168    /// if let Some(rating) = &outlook.rating {
169    ///     if let Some(r) = rating.first() {
170    ///         println!("Rating: {} (Score: {})", r.rating, r.rating_score);
171    ///     }
172    /// }
173    /// # Ok(())
174    /// # }
175    /// ```
176    pub async fn get_outlook(&self, symbol: &str) -> Result<CompanyOutlook> {
177        #[derive(Serialize)]
178        struct Query<'a> {
179            symbol: &'a str,
180            apikey: &'a str,
181        }
182
183        let url = self
184            .client
185            .build_url(&format!("/company-outlook?symbol={}", symbol));
186        self.client
187            .get_with_query(
188                &url,
189                &Query {
190                    symbol,
191                    apikey: self.client.api_key(),
192                },
193            )
194            .await
195    }
196
197    /// Get company rating
198    ///
199    /// Returns the company's rating and score based on various financial metrics.
200    ///
201    /// # Arguments
202    /// * `symbol` - Stock symbol (e.g., "AAPL")
203    ///
204    /// # Example
205    /// ```no_run
206    /// # use fmp_rs::FmpClient;
207    /// # #[tokio::main]
208    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
209    /// let client = FmpClient::new()?;
210    /// let ratings = client.company_info().get_rating("AAPL").await?;
211    /// for rating in ratings {
212    ///     println!("Rating: {} (Score: {})", rating.rating, rating.rating_score);
213    ///     println!("Recommendation: {}", rating.rating_recommendation);
214    /// }
215    /// # Ok(())
216    /// # }
217    /// ```
218    pub async fn get_rating(&self, symbol: &str) -> Result<Vec<CompanyRating>> {
219        #[derive(Serialize)]
220        struct Query<'a> {
221            symbol: &'a str,
222            apikey: &'a str,
223        }
224
225        let url = self.client.build_url(&format!("/rating/{}", symbol));
226        self.client
227            .get_with_query(
228                &url,
229                &Query {
230                    symbol,
231                    apikey: self.client.api_key(),
232                },
233            )
234            .await
235    }
236
237    /// Get historical company rating
238    ///
239    /// Returns historical ratings for a company over time.
240    ///
241    /// # Arguments
242    /// * `symbol` - Stock symbol (e.g., "AAPL")
243    /// * `limit` - Number of results (optional)
244    ///
245    /// # Example
246    /// ```no_run
247    /// # use fmp_rs::FmpClient;
248    /// # #[tokio::main]
249    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
250    /// let client = FmpClient::new()?;
251    /// let ratings = client.company_info().get_historical_rating("AAPL", Some(10)).await?;
252    /// for rating in ratings {
253    ///     println!("{}: {} (Score: {})", rating.date, rating.rating, rating.rating_score);
254    /// }
255    /// # Ok(())
256    /// # }
257    /// ```
258    pub async fn get_historical_rating(
259        &self,
260        symbol: &str,
261        limit: Option<u32>,
262    ) -> Result<Vec<CompanyRating>> {
263        #[derive(Serialize)]
264        struct Query<'a> {
265            symbol: &'a str,
266            #[serde(skip_serializing_if = "Option::is_none")]
267            limit: Option<u32>,
268            apikey: &'a str,
269        }
270
271        let url = self
272            .client
273            .build_url(&format!("/historical-rating/{}", symbol));
274        self.client
275            .get_with_query(
276                &url,
277                &Query {
278                    symbol,
279                    limit,
280                    apikey: self.client.api_key(),
281                },
282            )
283            .await
284    }
285
286    /// Get company core information
287    ///
288    /// Returns basic company information including CIK, symbol, and exchange.
289    ///
290    /// # Arguments
291    /// * `symbol` - Stock symbol (e.g., "AAPL")
292    ///
293    /// # Example
294    /// ```no_run
295    /// # use fmp_rs::FmpClient;
296    /// # #[tokio::main]
297    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
298    /// let client = FmpClient::new()?;
299    /// let info = client.company_info().get_core_info("AAPL").await?;
300    /// println!("CIK: {}, Exchange: {}", info.cik, info.exchange);
301    /// # Ok(())
302    /// # }
303    /// ```
304    pub async fn get_core_info(&self, symbol: &str) -> Result<Vec<CompanyCoreInfo>> {
305        #[derive(Serialize)]
306        struct Query<'a> {
307            symbol: &'a str,
308            apikey: &'a str,
309        }
310
311        let url = self.client.build_url("/company-core-information");
312        self.client
313            .get_with_query(
314                &url,
315                &Query {
316                    symbol,
317                    apikey: self.client.api_key(),
318                },
319            )
320            .await
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_new() {
330        let client = FmpClient::builder().api_key("test_key").build().unwrap();
331        let _ = CompanyInfo::new(client);
332    }
333
334    // Golden path tests (require API key)
335    #[tokio::test]
336    #[ignore = "requires FMP API key"]
337    async fn test_get_profile() {
338        let client = FmpClient::new().unwrap();
339        let result = client.company_info().get_profile("AAPL").await;
340        assert!(result.is_ok());
341        let profiles = result.unwrap();
342        assert!(!profiles.is_empty());
343        assert_eq!(profiles[0].symbol, "AAPL");
344    }
345
346    #[tokio::test]
347    #[ignore = "requires FMP API key"]
348    async fn test_get_outlook() {
349        let client = FmpClient::new().unwrap();
350        let result = client.company_info().get_outlook("AAPL").await;
351        assert!(result.is_ok());
352        let outlook = result.unwrap();
353        assert_eq!(outlook.profile.symbol, "AAPL");
354        assert!(outlook.profile.company_name.contains("Apple"));
355    }
356
357    #[tokio::test]
358    #[ignore = "requires FMP API key"]
359    async fn test_get_rating() {
360        let client = FmpClient::new().unwrap();
361        let result = client.company_info().get_rating("AAPL").await;
362        assert!(result.is_ok());
363        let ratings = result.unwrap();
364        assert!(!ratings.is_empty());
365        assert_eq!(ratings[0].symbol, "AAPL");
366    }
367
368    #[tokio::test]
369    #[ignore = "requires FMP API key"]
370    async fn test_get_historical_rating() {
371        let client = FmpClient::new().unwrap();
372        let result = client
373            .company_info()
374            .get_historical_rating("AAPL", Some(5))
375            .await;
376        assert!(result.is_ok());
377        let ratings = result.unwrap();
378        assert!(!ratings.is_empty());
379        assert!(ratings.len() <= 5);
380    }
381
382    #[tokio::test]
383    #[ignore = "requires FMP API key"]
384    async fn test_get_core_info() {
385        let client = FmpClient::new().unwrap();
386        let result = client.company_info().get_core_info("AAPL").await;
387        assert!(result.is_ok());
388        let info = result.unwrap();
389        assert!(!info.is_empty());
390        assert_eq!(info[0].symbol, "AAPL");
391    }
392
393    #[tokio::test]
394    #[ignore = "requires FMP API key"]
395    async fn test_get_executives() {
396        let client = FmpClient::new().unwrap();
397        let result = client.company_info().get_executives("AAPL").await;
398        assert!(result.is_ok());
399        let execs = result.unwrap();
400        assert!(!execs.is_empty());
401    }
402
403    #[tokio::test]
404    #[ignore = "requires FMP API key"]
405    async fn test_get_market_cap() {
406        let client = FmpClient::new().unwrap();
407        let result = client.company_info().get_market_cap("AAPL").await;
408        assert!(result.is_ok());
409    }
410
411    #[tokio::test]
412    #[ignore = "requires FMP API key"]
413    async fn test_get_share_float() {
414        let client = FmpClient::new().unwrap();
415        let result = client.company_info().get_share_float("AAPL").await;
416        assert!(result.is_ok());
417    }
418
419    #[tokio::test]
420    #[ignore = "requires FMP API key"]
421    async fn test_get_peers() {
422        let client = FmpClient::new().unwrap();
423        let result = client.company_info().get_peers("AAPL").await;
424        assert!(result.is_ok());
425        let peers = result.unwrap();
426        assert!(!peers.is_empty());
427        assert_eq!(peers[0].symbol, "AAPL");
428    }
429
430    // Edge case tests
431    #[tokio::test]
432    #[ignore = "requires FMP API key"]
433    async fn test_get_profile_invalid_symbol() {
434        let client = FmpClient::new().unwrap();
435        let result = client
436            .company_info()
437            .get_profile("INVALID_SYMBOL_XYZ123")
438            .await;
439        // Should either return empty vec or error
440        if let Ok(profiles) = result {
441            assert!(profiles.is_empty());
442        }
443    }
444
445    #[tokio::test]
446    #[ignore = "requires FMP API key"]
447    async fn test_get_rating_invalid_symbol() {
448        let client = FmpClient::new().unwrap();
449        let result = client
450            .company_info()
451            .get_rating("INVALID_SYMBOL_XYZ123")
452            .await;
453        // Should either return empty vec or error
454        if let Ok(ratings) = result {
455            assert!(ratings.is_empty());
456        }
457    }
458
459    #[tokio::test]
460    #[ignore = "requires FMP API key"]
461    async fn test_get_historical_market_cap_with_date_range() {
462        let client = FmpClient::new().unwrap();
463        let result = client
464            .company_info()
465            .get_historical_market_cap("AAPL", Some("2024-01-01"), Some("2024-12-31"))
466            .await;
467        assert!(result.is_ok());
468    }
469
470    #[tokio::test]
471    #[ignore = "requires FMP API key"]
472    async fn test_get_historical_rating_with_limit() {
473        let client = FmpClient::new().unwrap();
474        let result = client
475            .company_info()
476            .get_historical_rating("AAPL", Some(3))
477            .await;
478        assert!(result.is_ok());
479        if let Ok(ratings) = result {
480            assert!(ratings.len() <= 3);
481        }
482    }
483
484    // Test error handling
485    #[tokio::test]
486    async fn test_invalid_api_key() {
487        let client = FmpClient::builder()
488            .api_key("invalid_key_12345")
489            .build()
490            .unwrap();
491        let result = client.company_info().get_profile("AAPL").await;
492        assert!(result.is_err());
493    }
494
495    #[tokio::test]
496    async fn test_empty_symbol() {
497        let client = FmpClient::builder().api_key("test_key").build().unwrap();
498        let result = client.company_info().get_profile("").await;
499        // Should handle gracefully
500        assert!(result.is_err() || result.unwrap().is_empty());
501    }
502}