1use crate::endpoints::*;
4use crate::error::{Error, Result};
5use reqwest::{Client, Response};
6use std::sync::Arc;
7use std::time::Duration;
8
9pub const FMP_API_BASE_URL: &str = "https://financialmodelingprep.com/stable";
11
12#[derive(Debug, Clone)]
14pub struct FmpConfig {
15 pub api_key: String,
17 pub base_url: String,
19 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
33pub struct FmpClientBuilder {
35 config: FmpConfig,
36}
37
38impl FmpClientBuilder {
39 pub fn new() -> Self {
41 Self {
42 config: FmpConfig::default(),
43 }
44 }
45
46 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 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 pub fn timeout(mut self, timeout: Duration) -> Self {
60 self.config.timeout = timeout;
61 self
62 }
63
64 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
90struct FmpClientInner {
92 http_client: Client,
93 api_key: String,
94 base_url: String,
95}
96
97#[derive(Clone)]
99pub struct FmpClient {
100 inner: Arc<FmpClientInner>,
101}
102
103impl FmpClient {
104 pub fn new() -> Result<Self> {
106 FmpClientBuilder::new().build()
107 }
108
109 pub fn with_api_key(api_key: impl Into<String>) -> Result<Self> {
111 FmpClientBuilder::new().api_key(api_key).build()
112 }
113
114 pub fn builder() -> FmpClientBuilder {
116 FmpClientBuilder::new()
117 }
118
119 pub(crate) fn api_key(&self) -> &str {
121 &self.inner.api_key
122 }
123
124 #[allow(dead_code)]
126 pub(crate) fn base_url(&self) -> &str {
127 &self.inner.base_url
128 }
129
130 pub(crate) fn build_url(&self, path: &str) -> String {
132 format!("{}/{}", self.inner.base_url, path.trim_start_matches('/'))
133 }
134
135 #[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 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 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 pub fn company_search(&self) -> CompanySearch {
184 CompanySearch::new(self.clone())
185 }
186
187 pub fn quote(&self) -> Quote {
189 Quote::new(self.clone())
190 }
191
192 pub fn stock_directory(&self) -> StockDirectory {
194 StockDirectory::new(self.clone())
195 }
196
197 pub fn company_info(&self) -> CompanyInfo {
199 CompanyInfo::new(self.clone())
200 }
201
202 pub fn financials(&self) -> Financials {
204 Financials::new(self.clone())
205 }
206
207 pub fn charts(&self) -> Charts {
209 Charts::new(self.clone())
210 }
211
212 pub fn economics(&self) -> Economics {
214 Economics::new(self.clone())
215 }
216
217 pub fn corporate_actions(&self) -> CorporateActions {
219 CorporateActions::new(self.clone())
220 }
221
222 pub fn news(&self) -> News {
224 News::new(self.clone())
225 }
226
227 pub fn analyst(&self) -> Analyst {
229 Analyst::new(self.clone())
230 }
231
232 pub fn market_performance(&self) -> MarketPerformance {
234 MarketPerformance::new(self.clone())
235 }
236
237 pub fn etf(&self) -> Etf {
239 Etf::new(self.clone())
240 }
241
242 pub fn sec_filings(&self) -> SecFilings {
244 SecFilings::new(self.clone())
245 }
246
247 pub fn insider_trades(&self) -> InsiderTrades {
249 InsiderTrades::new(self.clone())
250 }
251
252 pub fn indexes(&self) -> Indexes {
254 Indexes::new(self.clone())
255 }
256
257 pub fn commodities(&self) -> Commodities {
259 Commodities::new(self.clone())
260 }
261
262 pub fn dcf(&self) -> Dcf {
264 Dcf::new(self.clone())
265 }
266
267 pub fn forex(&self) -> Forex {
269 Forex::new(self.clone())
270 }
271
272 pub fn crypto(&self) -> Crypto {
274 Crypto::new(self.clone())
275 }
276
277 pub fn technical_indicators(&self) -> TechnicalIndicators {
279 TechnicalIndicators::new(self.clone())
280 }
281
282 pub fn institutional(&self) -> Institutional {
284 Institutional::new(self.clone())
285 }
286
287 pub fn congress(&self) -> Congress {
289 Congress::new(self.clone())
290 }
291
292 pub fn esg(&self) -> Esg {
294 Esg::new(self.clone())
295 }
296
297 pub fn market_hours(&self) -> MarketHours {
299 MarketHours::new(self.clone())
300 }
301
302 pub fn mutual_funds(&self) -> MutualFunds {
304 MutualFunds::new(self.clone())
305 }
306
307 pub fn transcripts(&self) -> Transcripts {
309 Transcripts::new(self.clone())
310 }
311
312 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}