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/// Take the first row of a single-row FMP response.
49///
50/// FMP wraps per-symbol rollups in a single-element array; an empty one means
51/// the symbol is absent from FMP's universe rather than that the call failed.
52pub(crate) fn first_or_missing<T>(rows: Vec<T>, symbol: &str, field: &str) -> Result<T> {
53 rows.into_iter()
54 .next()
55 .ok_or_else(|| FinanceError::SymbolNotFound {
56 symbol: Some(symbol.to_string()),
57 context: format!("FMP returned no {field} for {symbol}"),
58 })
59}
60
61/// FMP default rate limit: 5 req/sec.
62const FMP_RATE_PER_SEC: f64 = 5.0;
63
64provider_singleton_state!(
65 name = FmpSingleton,
66 static_name = FMP_SINGLETON,
67 rate_const = FMP_RATE_PER_SEC,
68 provider_key = "fmp",
69 already_init_reason = "FMP client already initialized",
70);
71
72provider_build_client!(
73 name = FmpSingleton,
74 static_name = FMP_SINGLETON,
75 rate_const = FMP_RATE_PER_SEC,
76 provider_key = "fmp",
77 env_var = "FMP_API_KEY",
78 env_missing_reason = "FMP_API_KEY not set. Call fmp::init(key) or set FMP_API_KEY env var.",
79 builder = client::FmpClientBuilder,
80 client_ty = client::FmpClient,
81);
82
83/// Initialize the global FMP client with an API key.
84///
85/// Must be called once before using any query functions. Subsequent calls return an error.
86///
87/// # Errors
88///
89/// Returns [`FinanceError::InvalidParameter`] if already initialized.
90#[allow(dead_code)] // public init path for direct adapter use; ProviderSet initialises via env var
91pub fn init(api_key: impl Into<String>) -> Result<()> {
92 init_with_timeout(api_key, Duration::from_secs(30))
93}
94
95/// Initialize the FMP client with a custom timeout.
96#[allow(dead_code)] // public init path for direct adapter use; ProviderSet initialises via env var
97pub fn init_with_timeout(api_key: impl Into<String>, timeout: Duration) -> Result<()> {
98 set_singleton(api_key, timeout)
99}
100
101#[cfg(test)]
102mod live_tests;
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::error::FinanceError;
108
109 #[test]
110 fn test_init_errors_on_double_init() {
111 let _ = init("test-key-1");
112 let result = init("test-key-2");
113 assert!(matches!(result, Err(FinanceError::InvalidParameter { .. })));
114 }
115}