use crate::endpoints::*;
use crate::error::{Error, Result};
use reqwest::{Client, Response};
use std::sync::Arc;
use std::time::Duration;
pub const FMP_API_BASE_URL: &str = "https://financialmodelingprep.com/stable";
#[derive(Debug, Clone)]
pub struct FmpConfig {
pub api_key: String,
pub base_url: String,
pub timeout: Duration,
}
impl Default for FmpConfig {
fn default() -> Self {
Self {
api_key: String::new(),
base_url: FMP_API_BASE_URL.to_string(),
timeout: Duration::from_secs(30),
}
}
}
pub struct FmpClientBuilder {
config: FmpConfig,
}
impl FmpClientBuilder {
pub fn new() -> Self {
Self {
config: FmpConfig::default(),
}
}
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.config.api_key = api_key.into();
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.config.base_url = base_url.into();
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout;
self
}
pub fn build(self) -> Result<FmpClient> {
let api_key = if self.config.api_key.is_empty() {
std::env::var("FMP_API_KEY").map_err(|_| Error::MissingApiKey)?
} else {
self.config.api_key
};
let http_client = Client::builder().timeout(self.config.timeout).build()?;
Ok(FmpClient {
inner: Arc::new(FmpClientInner {
http_client,
api_key,
base_url: self.config.base_url,
}),
})
}
}
impl Default for FmpClientBuilder {
fn default() -> Self {
Self::new()
}
}
struct FmpClientInner {
http_client: Client,
api_key: String,
base_url: String,
}
#[derive(Clone)]
pub struct FmpClient {
inner: Arc<FmpClientInner>,
}
impl FmpClient {
pub fn new() -> Result<Self> {
FmpClientBuilder::new().build()
}
pub fn with_api_key(api_key: impl Into<String>) -> Result<Self> {
FmpClientBuilder::new().api_key(api_key).build()
}
pub fn builder() -> FmpClientBuilder {
FmpClientBuilder::new()
}
pub(crate) fn api_key(&self) -> &str {
&self.inner.api_key
}
#[allow(dead_code)]
pub(crate) fn base_url(&self) -> &str {
&self.inner.base_url
}
pub(crate) fn build_url(&self, path: &str) -> String {
format!("{}/{}", self.inner.base_url, path.trim_start_matches('/'))
}
#[allow(dead_code)]
pub(crate) async fn get<T>(&self, url: &str) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let response = self.inner.http_client.get(url).send().await?;
self.handle_response(response).await
}
pub(crate) async fn get_with_query<T, Q>(&self, url: &str, query: &Q) -> Result<T>
where
T: serde::de::DeserializeOwned,
Q: serde::Serialize,
{
let response = self.inner.http_client.get(url).query(query).send().await?;
self.handle_response(response).await
}
async fn handle_response<T>(&self, response: Response) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let status = response.status();
if status.is_success() {
let text = response.text().await?;
serde_json::from_str(&text).map_err(|e| {
eprintln!("Failed to parse response: {}", text);
Error::from(e)
})
} else {
let status_code = status.as_u16();
let error_text = response.text().await.unwrap_or_default();
match status_code {
429 => Err(Error::RateLimitExceeded),
404 => Err(Error::NotFound(error_text)),
_ => Err(Error::api(status_code, error_text)),
}
}
}
pub fn company_search(&self) -> CompanySearch {
CompanySearch::new(self.clone())
}
pub fn quote(&self) -> Quote {
Quote::new(self.clone())
}
pub fn stock_directory(&self) -> StockDirectory {
StockDirectory::new(self.clone())
}
pub fn company_info(&self) -> CompanyInfo {
CompanyInfo::new(self.clone())
}
pub fn financials(&self) -> Financials {
Financials::new(self.clone())
}
pub fn charts(&self) -> Charts {
Charts::new(self.clone())
}
pub fn economics(&self) -> Economics {
Economics::new(self.clone())
}
pub fn corporate_actions(&self) -> CorporateActions {
CorporateActions::new(self.clone())
}
pub fn news(&self) -> News {
News::new(self.clone())
}
pub fn analyst(&self) -> Analyst {
Analyst::new(self.clone())
}
pub fn market_performance(&self) -> MarketPerformance {
MarketPerformance::new(self.clone())
}
pub fn etf(&self) -> Etf {
Etf::new(self.clone())
}
pub fn sec_filings(&self) -> SecFilings {
SecFilings::new(self.clone())
}
pub fn insider_trades(&self) -> InsiderTrades {
InsiderTrades::new(self.clone())
}
pub fn indexes(&self) -> Indexes {
Indexes::new(self.clone())
}
pub fn commodities(&self) -> Commodities {
Commodities::new(self.clone())
}
pub fn dcf(&self) -> Dcf {
Dcf::new(self.clone())
}
pub fn forex(&self) -> Forex {
Forex::new(self.clone())
}
pub fn crypto(&self) -> Crypto {
Crypto::new(self.clone())
}
pub fn technical_indicators(&self) -> TechnicalIndicators {
TechnicalIndicators::new(self.clone())
}
pub fn institutional(&self) -> Institutional {
Institutional::new(self.clone())
}
pub fn congress(&self) -> Congress {
Congress::new(self.clone())
}
pub fn esg(&self) -> Esg {
Esg::new(self.clone())
}
pub fn market_hours(&self) -> MarketHours {
MarketHours::new(self.clone())
}
pub fn mutual_funds(&self) -> MutualFunds {
MutualFunds::new(self.clone())
}
pub fn transcripts(&self) -> Transcripts {
Transcripts::new(self.clone())
}
pub fn bulk(&self) -> Bulk {
Bulk::new(self.clone())
}
}
impl Default for FmpClient {
fn default() -> Self {
Self::new().expect("Failed to create FmpClient from environment variable")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_builder() {
let client = FmpClient::builder()
.api_key("test_key")
.base_url("https://test.example.com")
.timeout(Duration::from_secs(10))
.build()
.unwrap();
assert_eq!(client.api_key(), "test_key");
assert_eq!(client.base_url(), "https://test.example.com");
}
#[test]
fn test_build_url() {
let client = FmpClient::builder().api_key("test_key").build().unwrap();
assert_eq!(
client.build_url("/quote"),
format!("{}/quote", FMP_API_BASE_URL)
);
assert_eq!(
client.build_url("quote"),
format!("{}/quote", FMP_API_BASE_URL)
);
}
}