use serde::{Deserialize, Serialize};
use crate::adapters::common::encode_path_segment;
use crate::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CompanyProfile {
pub symbol: Option<String>,
pub price: Option<f64>,
pub beta: Option<f64>,
#[serde(rename = "volAvg")]
pub vol_avg: Option<f64>,
#[serde(rename = "mktCap")]
pub mkt_cap: Option<f64>,
#[serde(rename = "lastDiv")]
pub last_div: Option<f64>,
pub range: Option<String>,
pub changes: Option<f64>,
#[serde(rename = "companyName")]
pub company_name: Option<String>,
pub currency: Option<String>,
pub cik: Option<String>,
pub isin: Option<String>,
pub cusip: Option<String>,
pub exchange: Option<String>,
#[serde(rename = "exchangeShortName")]
pub exchange_short_name: Option<String>,
pub industry: Option<String>,
pub website: Option<String>,
pub description: Option<String>,
pub ceo: Option<String>,
pub sector: Option<String>,
pub country: Option<String>,
#[serde(rename = "fullTimeEmployees")]
pub full_time_employees: Option<String>,
pub phone: Option<String>,
pub address: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub zip: Option<String>,
#[serde(rename = "dcfDiff")]
pub dcf_diff: Option<f64>,
pub dcf: Option<f64>,
pub image: Option<String>,
#[serde(rename = "ipoDate")]
pub ipo_date: Option<String>,
#[serde(rename = "defaultImage")]
pub default_image: Option<bool>,
#[serde(rename = "isEtf")]
pub is_etf: Option<bool>,
#[serde(rename = "isActivelyTrading")]
pub is_actively_trading: Option<bool>,
#[serde(rename = "isAdr")]
pub is_adr: Option<bool>,
#[serde(rename = "isFund")]
pub is_fund: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct KeyExecutive {
pub title: Option<String>,
pub name: Option<String>,
pub pay: Option<f64>,
#[serde(rename = "currencyPay")]
pub currency_pay: Option<String>,
pub gender: Option<String>,
#[serde(rename = "yearBorn")]
pub year_born: Option<i32>,
#[serde(rename = "titleSince")]
pub title_since: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MarketCap {
pub symbol: Option<String>,
pub date: Option<String>,
#[serde(rename = "marketCap")]
pub market_cap: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CompanyOutlook {
pub profile: Option<CompanyProfile>,
pub metrics: Option<serde_json::Value>,
pub ratios: Option<Vec<serde_json::Value>>,
#[serde(rename = "insideTrades")]
pub inside_trades: Option<Vec<serde_json::Value>>,
#[serde(rename = "keyExecutives")]
pub key_executives: Option<Vec<KeyExecutive>>,
#[serde(rename = "stockNews")]
pub stock_news: Option<Vec<serde_json::Value>>,
pub rating: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockPeers {
pub symbol: Option<String>,
#[serde(rename = "peersList")]
pub peers_list: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DelistedCompany {
pub symbol: Option<String>,
#[serde(rename = "companyName")]
pub company_name: Option<String>,
pub exchange: Option<String>,
#[serde(rename = "ipoDate")]
pub ipo_date: Option<String>,
#[serde(rename = "delistedDate")]
pub delisted_date: Option<String>,
}
pub async fn company_profile(symbol: &str) -> Result<Vec<CompanyProfile>> {
let client = super::build_client()?;
client
.get(
&format!("/api/v3/profile/{}", encode_path_segment(symbol)),
&[],
)
.await
}
pub async fn key_executives(symbol: &str) -> Result<Vec<KeyExecutive>> {
let client = super::build_client()?;
client
.get(
&format!("/api/v3/key-executives/{}", encode_path_segment(symbol)),
&[],
)
.await
}
pub async fn market_cap(symbol: &str) -> Result<Vec<MarketCap>> {
let client = super::build_client()?;
client
.get(
&format!(
"/api/v3/market-capitalization/{}",
encode_path_segment(symbol)
),
&[],
)
.await
}
pub async fn historical_market_cap(symbol: &str, limit: Option<u32>) -> Result<Vec<MarketCap>> {
let client = super::build_client()?;
let limit_str = limit.unwrap_or(100).to_string();
client
.get(
&format!(
"/api/v3/historical-market-capitalization/{}",
encode_path_segment(symbol)
),
&[("limit", &limit_str)],
)
.await
}
pub async fn company_outlook(symbol: &str) -> Result<CompanyOutlook> {
let client = super::build_client()?;
client
.get("/api/v4/company-outlook", &[("symbol", symbol)])
.await
}
pub async fn stock_peers(symbol: &str) -> Result<Vec<StockPeers>> {
let client = super::build_client()?;
client
.get("/api/v4/stock_peers", &[("symbol", symbol)])
.await
}
pub async fn delisted_companies(limit: Option<u32>) -> Result<Vec<DelistedCompany>> {
let client = super::build_client()?;
let limit_str = limit.unwrap_or(100).to_string();
client
.get("/api/v3/delisted-companies", &[("limit", &limit_str)])
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_company_profile_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/api/v3/profile/AAPL")
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apikey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_body(
serde_json::json!([{
"symbol": "AAPL",
"price": 178.72,
"beta": 1.286,
"volAvg": 58405568,
"mktCap": 2794000000000_f64,
"companyName": "Apple Inc.",
"currency": "USD",
"exchange": "NASDAQ Global Select",
"exchangeShortName": "NASDAQ",
"industry": "Consumer Electronics",
"sector": "Technology",
"country": "US",
"ceo": "Mr. Timothy D. Cook",
"isEtf": false,
"isActivelyTrading": true
}])
.to_string(),
)
.create_async()
.await;
let client = super::super::build_test_client(&server.url()).unwrap();
let result: Vec<CompanyProfile> = client.get("/api/v3/profile/AAPL", &[]).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].symbol.as_deref(), Some("AAPL"));
assert_eq!(result[0].company_name.as_deref(), Some("Apple Inc."));
assert_eq!(result[0].sector.as_deref(), Some("Technology"));
assert_eq!(result[0].is_etf, Some(false));
}
#[tokio::test]
async fn test_key_executives_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/api/v3/key-executives/AAPL")
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apikey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_body(
serde_json::json!([
{
"title": "Chief Executive Officer",
"name": "Mr. Timothy D. Cook",
"pay": 16425933,
"currencyPay": "USD",
"gender": "male",
"yearBorn": 1960
},
{
"title": "Chief Financial Officer",
"name": "Mr. Luca Maestri",
"pay": 5019783,
"currencyPay": "USD",
"gender": "male",
"yearBorn": 1963
}
])
.to_string(),
)
.create_async()
.await;
let client = super::super::build_test_client(&server.url()).unwrap();
let result: Vec<KeyExecutive> = client
.get("/api/v3/key-executives/AAPL", &[])
.await
.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].name.as_deref(), Some("Mr. Timothy D. Cook"));
assert_eq!(result[0].pay, Some(16425933.0));
}
#[tokio::test]
async fn test_fmp_rate_limit_returns_rate_limited_error() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", mockito::Matcher::Any)
.with_status(429)
.with_body("{}")
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
assert!(matches!(
result,
Err(crate::error::FinanceError::RateLimited { .. })
));
}
#[tokio::test]
async fn test_fmp_401_returns_authentication_failed() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", mockito::Matcher::Any)
.with_status(401)
.with_body("{}")
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
assert!(matches!(
result,
Err(crate::error::FinanceError::AuthenticationFailed { .. })
));
}
#[tokio::test]
async fn test_fmp_body_error_message_returns_invalid_parameter() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", mockito::Matcher::Any)
.with_status(200)
.with_body(r#"{"Error Message":"Invalid API KEY."}"#)
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
assert!(matches!(
result,
Err(crate::error::FinanceError::InvalidParameter { .. })
));
}
#[tokio::test]
async fn test_fmp_500_returns_server_error() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", mockito::Matcher::Any)
.with_status(500)
.with_body("{}")
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
assert!(matches!(
result,
Err(crate::error::FinanceError::ServerError { .. })
));
}
}