fmp_rs/
client.rs

1//! HTTP client for interacting with the FMP API.
2
3use crate::endpoints::*;
4use crate::error::{Error, Result};
5use reqwest::{Client, Response};
6use std::sync::Arc;
7use std::time::Duration;
8
9/// Base URL for the FMP API
10pub const FMP_API_BASE_URL: &str = "https://financialmodelingprep.com/stable";
11
12/// HTTP client configuration
13#[derive(Debug, Clone)]
14pub struct FmpConfig {
15    /// API key for authentication
16    pub api_key: String,
17    /// Base URL for the API
18    pub base_url: String,
19    /// Request timeout in seconds
20    pub timeout: Duration,
21}
22
23impl Default for FmpConfig {
24    fn default() -> Self {
25        Self {
26            api_key: String::new(),
27            base_url: FMP_API_BASE_URL.to_string(),
28            timeout: Duration::from_secs(30),
29        }
30    }
31}
32
33/// Builder for creating an FmpClient with custom configuration
34pub struct FmpClientBuilder {
35    config: FmpConfig,
36}
37
38impl FmpClientBuilder {
39    /// Create a new builder
40    pub fn new() -> Self {
41        Self {
42            config: FmpConfig::default(),
43        }
44    }
45
46    /// Set the API key
47    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
48        self.config.api_key = api_key.into();
49        self
50    }
51
52    /// Set the base URL (useful for testing)
53    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
54        self.config.base_url = base_url.into();
55        self
56    }
57
58    /// Set the request timeout
59    pub fn timeout(mut self, timeout: Duration) -> Self {
60        self.config.timeout = timeout;
61        self
62    }
63
64    /// Build the client
65    pub fn build(self) -> Result<FmpClient> {
66        let api_key = if self.config.api_key.is_empty() {
67            std::env::var("FMP_API_KEY").map_err(|_| Error::MissingApiKey)?
68        } else {
69            self.config.api_key
70        };
71
72        let http_client = Client::builder().timeout(self.config.timeout).build()?;
73
74        Ok(FmpClient {
75            inner: Arc::new(FmpClientInner {
76                http_client,
77                api_key,
78                base_url: self.config.base_url,
79            }),
80        })
81    }
82}
83
84impl Default for FmpClientBuilder {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90/// Inner client data shared via Arc
91struct FmpClientInner {
92    http_client: Client,
93    api_key: String,
94    base_url: String,
95}
96
97/// Main client for interacting with the FMP API
98#[derive(Clone)]
99pub struct FmpClient {
100    inner: Arc<FmpClientInner>,
101}
102
103impl FmpClient {
104    /// Create a new FMP client with the API key from environment variable
105    pub fn new() -> Result<Self> {
106        FmpClientBuilder::new().build()
107    }
108
109    /// Create a new FMP client with the provided API key
110    pub fn with_api_key(api_key: impl Into<String>) -> Result<Self> {
111        FmpClientBuilder::new().api_key(api_key).build()
112    }
113
114    /// Get a builder for creating a customized client
115    pub fn builder() -> FmpClientBuilder {
116        FmpClientBuilder::new()
117    }
118
119    /// Get the API key
120    pub(crate) fn api_key(&self) -> &str {
121        &self.inner.api_key
122    }
123
124    /// Get the base URL
125    #[allow(dead_code)]
126    pub(crate) fn base_url(&self) -> &str {
127        &self.inner.base_url
128    }
129
130    /// Build a full URL from a path
131    pub(crate) fn build_url(&self, path: &str) -> String {
132        format!("{}/{}", self.inner.base_url, path.trim_start_matches('/'))
133    }
134
135    /// Execute a GET request
136    #[allow(dead_code)]
137    pub(crate) async fn get<T>(&self, url: &str) -> Result<T>
138    where
139        T: serde::de::DeserializeOwned,
140    {
141        let response = self.inner.http_client.get(url).send().await?;
142        self.handle_response(response).await
143    }
144
145    /// Execute a GET request with query parameters
146    pub(crate) async fn get_with_query<T, Q>(&self, url: &str, query: &Q) -> Result<T>
147    where
148        T: serde::de::DeserializeOwned,
149        Q: serde::Serialize,
150    {
151        let response = self.inner.http_client.get(url).query(query).send().await?;
152        self.handle_response(response).await
153    }
154
155    /// Handle the HTTP response
156    async fn handle_response<T>(&self, response: Response) -> Result<T>
157    where
158        T: serde::de::DeserializeOwned,
159    {
160        let status = response.status();
161
162        if status.is_success() {
163            let text = response.text().await?;
164            serde_json::from_str(&text).map_err(|e| {
165                eprintln!("Failed to parse response: {}", text);
166                Error::from(e)
167            })
168        } else {
169            let status_code = status.as_u16();
170            let error_text = response.text().await.unwrap_or_default();
171
172            match status_code {
173                429 => Err(Error::RateLimitExceeded),
174                404 => Err(Error::NotFound(error_text)),
175                _ => Err(Error::api(status_code, error_text)),
176            }
177        }
178    }
179
180    // Endpoint accessor methods
181
182    /// Access company search endpoints
183    pub fn company_search(&self) -> CompanySearch {
184        CompanySearch::new(self.clone())
185    }
186
187    /// Access quote endpoints
188    pub fn quote(&self) -> Quote {
189        Quote::new(self.clone())
190    }
191
192    /// Access stock directory endpoints
193    pub fn stock_directory(&self) -> StockDirectory {
194        StockDirectory::new(self.clone())
195    }
196
197    /// Access company information endpoints
198    pub fn company_info(&self) -> CompanyInfo {
199        CompanyInfo::new(self.clone())
200    }
201
202    /// Access financial statements endpoints
203    pub fn financials(&self) -> Financials {
204        Financials::new(self.clone())
205    }
206
207    /// Access historical price chart endpoints
208    pub fn charts(&self) -> Charts {
209        Charts::new(self.clone())
210    }
211
212    /// Access economics endpoints
213    pub fn economics(&self) -> Economics {
214        Economics::new(self.clone())
215    }
216
217    /// Access earnings, dividends, and splits endpoints
218    pub fn corporate_actions(&self) -> CorporateActions {
219        CorporateActions::new(self.clone())
220    }
221
222    /// Access news endpoints
223    pub fn news(&self) -> News {
224        News::new(self.clone())
225    }
226
227    /// Access analyst endpoints
228    pub fn analyst(&self) -> Analyst {
229        Analyst::new(self.clone())
230    }
231
232    /// Access market performance endpoints
233    pub fn market_performance(&self) -> MarketPerformance {
234        MarketPerformance::new(self.clone())
235    }
236
237    /// Access ETF and mutual fund endpoints
238    pub fn etf(&self) -> Etf {
239        Etf::new(self.clone())
240    }
241
242    /// Access SEC filings endpoints
243    pub fn sec_filings(&self) -> SecFilings {
244        SecFilings::new(self.clone())
245    }
246
247    /// Access insider trading endpoints
248    pub fn insider_trades(&self) -> InsiderTrades {
249        InsiderTrades::new(self.clone())
250    }
251
252    /// Access index endpoints
253    pub fn indexes(&self) -> Indexes {
254        Indexes::new(self.clone())
255    }
256
257    /// Access commodity endpoints
258    pub fn commodities(&self) -> Commodities {
259        Commodities::new(self.clone())
260    }
261
262    /// Access DCF valuation endpoints
263    pub fn dcf(&self) -> Dcf {
264        Dcf::new(self.clone())
265    }
266
267    /// Access forex endpoints
268    pub fn forex(&self) -> Forex {
269        Forex::new(self.clone())
270    }
271
272    /// Access cryptocurrency endpoints
273    pub fn crypto(&self) -> Crypto {
274        Crypto::new(self.clone())
275    }
276
277    /// Access technical indicators endpoints
278    pub fn technical_indicators(&self) -> TechnicalIndicators {
279        TechnicalIndicators::new(self.clone())
280    }
281
282    /// Access institutional ownership (Form 13F) endpoints
283    pub fn institutional(&self) -> Institutional {
284        Institutional::new(self.clone())
285    }
286
287    /// Access senate/congress trading endpoints
288    pub fn congress(&self) -> Congress {
289        Congress::new(self.clone())
290    }
291
292    /// Access ESG (Environmental, Social, Governance) endpoints
293    pub fn esg(&self) -> Esg {
294        Esg::new(self.clone())
295    }
296
297    /// Access market hours endpoints
298    pub fn market_hours(&self) -> MarketHours {
299        MarketHours::new(self.clone())
300    }
301
302    /// Access mutual funds endpoints
303    pub fn mutual_funds(&self) -> MutualFunds {
304        MutualFunds::new(self.clone())
305    }
306
307    /// Access earnings transcript endpoints
308    pub fn transcripts(&self) -> Transcripts {
309        Transcripts::new(self.clone())
310    }
311
312    /// Access bulk data endpoints
313    pub fn bulk(&self) -> Bulk {
314        Bulk::new(self.clone())
315    }
316}
317
318impl Default for FmpClient {
319    fn default() -> Self {
320        Self::new().expect("Failed to create FmpClient from environment variable")
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_client_builder() {
330        let client = FmpClient::builder()
331            .api_key("test_key")
332            .base_url("https://test.example.com")
333            .timeout(Duration::from_secs(10))
334            .build()
335            .unwrap();
336
337        assert_eq!(client.api_key(), "test_key");
338        assert_eq!(client.base_url(), "https://test.example.com");
339    }
340
341    #[test]
342    fn test_build_url() {
343        let client = FmpClient::builder().api_key("test_key").build().unwrap();
344
345        assert_eq!(
346            client.build_url("/quote"),
347            format!("{}/quote", FMP_API_BASE_URL)
348        );
349
350        assert_eq!(
351            client.build_url("quote"),
352            format!("{}/quote", FMP_API_BASE_URL)
353        );
354    }
355}