Skip to main content

finance_query/adapters/fmp/
mod.rs

1//! Financial Modeling Prep API client for financial data.
2//!
3//! Requires the **`fmp`** feature flag and an API key from
4//! <https://financialmodelingprep.com/>.
5//!
6//! Call [`init`] once at startup before using any query functions.
7//!
8//! # Quick Start
9//!
10//! ```no_run
11//! use finance_query::{Providers, Provider, Capability, StatementType, Frequency};
12//! use finance_query::format::Raw;
13//!
14//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
15//! // Route fundamentals and quote data to FMP with Yahoo as fallback
16//! let providers = Providers::builder()
17//!     .route(Capability::FUNDAMENTALS, [Provider::Fmp, Provider::Yahoo])
18//!     .route(Capability::QUOTE, [Provider::Fmp, Provider::Yahoo])
19//!     .build().await?;
20//!
21//! let ticker = providers.ticker("AAPL").build().await?;
22//! let quote = ticker.quote::<Raw>().await?;
23//! let income = ticker.financials(StatementType::Income, Frequency::Quarterly).await?;
24//! # Ok(())
25//! # }
26//! ```
27
28mod client;
29pub(crate) mod models;
30
31// Capability-mapped subdirectory modules
32pub(crate) mod commodities; // COMMODITIES
33pub(crate) mod corporate; // CORPORATE
34pub(crate) mod crypto; // CRYPTO
35pub(crate) mod discovery; // DISCOVERY
36pub(crate) mod forex; // FOREX
37pub(crate) mod fundamentals; // FUNDAMENTALS
38pub(crate) mod indices; // INDICES
39pub(crate) mod market; // CALENDAR + MARKET
40pub(crate) mod quote; // QUOTE
41
42use crate::adapters::singleton::{provider_build_client, provider_singleton_state};
43use crate::error::{FinanceError, Result};
44use std::time::Duration;
45
46pub use models::*;
47
48/// FMP sends `""` for missing text fields, so blank strings deserialize as `None`.
49pub(crate) fn blank_as_none<'de, D>(
50    deserializer: D,
51) -> std::result::Result<Option<String>, D::Error>
52where
53    D: serde::Deserializer<'de>,
54{
55    let value = <Option<String> as serde::Deserialize>::deserialize(deserializer)?;
56    Ok(value.filter(|s| !s.trim().is_empty()))
57}
58
59/// Take the first row of a single-row FMP response.
60///
61/// FMP wraps per-symbol rollups in a single-element array; an empty one means
62/// the symbol is absent from FMP's universe rather than that the call failed.
63pub(crate) fn first_or_missing<T>(rows: Vec<T>, symbol: &str, field: &str) -> Result<T> {
64    rows.into_iter()
65        .next()
66        .ok_or_else(|| FinanceError::SymbolNotFound {
67            symbol: Some(symbol.to_string()),
68            context: format!("FMP returned no {field} for {symbol}"),
69        })
70}
71
72/// FMP default rate limit: 5 req/sec.
73const FMP_RATE_PER_SEC: f64 = 5.0;
74
75provider_singleton_state!(
76    name = FmpSingleton,
77    static_name = FMP_SINGLETON,
78    rate_const = FMP_RATE_PER_SEC,
79    provider_key = "fmp",
80    already_init_reason = "FMP client already initialized",
81);
82
83provider_build_client!(
84    name = FmpSingleton,
85    static_name = FMP_SINGLETON,
86    rate_const = FMP_RATE_PER_SEC,
87    provider_key = "fmp",
88    env_var = "FMP_API_KEY",
89    env_missing_reason = "FMP_API_KEY not set. Call fmp::init(key) or set FMP_API_KEY env var.",
90    builder = client::FmpClientBuilder,
91    client_ty = client::FmpClient,
92);
93
94/// Initialize the global FMP client with an API key.
95///
96/// Must be called once before using any query functions. Subsequent calls return an error.
97///
98/// # Errors
99///
100/// Returns [`FinanceError::InvalidParameter`] if already initialized.
101#[allow(dead_code)] // public init path for direct adapter use; ProviderSet initialises via env var
102pub fn init(api_key: impl Into<String>) -> Result<()> {
103    init_with_timeout(api_key, Duration::from_secs(30))
104}
105
106/// Initialize the FMP client with a custom timeout.
107#[allow(dead_code)] // public init path for direct adapter use; ProviderSet initialises via env var
108pub fn init_with_timeout(api_key: impl Into<String>, timeout: Duration) -> Result<()> {
109    set_singleton(api_key, timeout)
110}
111
112#[cfg(test)]
113mod live_tests;
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::error::FinanceError;
119
120    #[test]
121    fn test_init_errors_on_double_init() {
122        let _ = init("test-key-1");
123        let result = init("test-key-2");
124        assert!(matches!(result, Err(FinanceError::InvalidParameter { .. })));
125    }
126}