use crate::bulk_processing::{BulkProcessingConfig, ChunkProcessor};
use crate::cache::{CacheConfig, IntelligentCache, SmartCacheStrategy};
use crate::error::{Error, Result};
use crate::production::{Metrics, PoolConfig, ProductionExecutor, RateLimitConfig, RetryConfig};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::sync::Arc;
use std::time::Duration;
use tracing::{info, warn};
#[derive(Debug, Clone)]
pub struct ProductionConfig {
pub api_key: String,
pub base_url: String,
pub timeout: Duration,
pub rate_limit: RateLimitConfig,
pub pool: PoolConfig,
pub retry: RetryConfig,
pub cache: CacheConfig,
pub bulk_processing: BulkProcessingConfig,
pub enable_caching: bool,
pub enable_metrics: bool,
pub enable_tracing: bool,
}
impl Default for ProductionConfig {
fn default() -> Self {
Self {
api_key: std::env::var("FMP_API_KEY").unwrap_or_default(),
base_url: "https://financialmodelingprep.com/stable".to_string(),
timeout: Duration::from_secs(30),
rate_limit: RateLimitConfig::default(),
pool: PoolConfig::default(),
retry: RetryConfig::default(),
cache: CacheConfig::default(),
bulk_processing: BulkProcessingConfig::default(),
enable_caching: true,
enable_metrics: true,
enable_tracing: true,
}
}
}
pub struct ProductionFmpClient {
config: ProductionConfig,
executor: Arc<ProductionExecutor>,
cache: Option<Arc<IntelligentCache<String>>>, }
impl ProductionFmpClient {
pub async fn new(config: ProductionConfig) -> Result<Self> {
if config.api_key.is_empty() {
return Err(Error::MissingApiKey);
}
if config.enable_tracing {
Self::init_tracing();
}
let executor = Arc::new(ProductionExecutor::new(
config.pool.clone(),
config.rate_limit.clone(),
config.retry.clone(),
config.timeout,
)?);
let cache = if config.enable_caching {
Some(Arc::new(IntelligentCache::new(config.cache.clone())))
} else {
None
};
info!(
"Production FMP client initialized with caching: {}, metrics: {}",
config.enable_caching, config.enable_metrics
);
Ok(Self {
config,
executor,
cache,
})
}
pub fn builder() -> ProductionClientBuilder {
ProductionClientBuilder::new()
}
pub async fn get_cached<T>(&self, endpoint: &str, query_params: Option<&str>) -> Result<T>
where
T: DeserializeOwned,
{
let cache_key = SmartCacheStrategy::generate_cache_key(endpoint, query_params);
if let Some(cache) = &self.cache {
if let Some(cached_json) = cache.get(&cache_key).await {
match serde_json::from_str::<T>(&cached_json) {
Ok(data) => return Ok(data),
Err(e) => {
warn!("Failed to deserialize cached data: {}", e);
}
}
}
}
let url = self.build_url(endpoint);
let mut request_builder = self.executor.client().get(&url);
if let Some(params) = query_params {
request_builder = request_builder
.header("Content-Type", "application/x-www-form-urlencoded")
.body(params.to_string());
} else {
request_builder = request_builder.query(&[("apikey", &self.config.api_key)]);
}
let json_response: String = self.executor.execute_request(request_builder).await?;
if let Some(cache) = &self.cache {
if SmartCacheStrategy::should_cache_endpoint(endpoint) {
let ttl = SmartCacheStrategy::get_ttl_for_endpoint(endpoint);
cache.set(cache_key, json_response.clone(), Some(ttl)).await;
}
}
serde_json::from_str(&json_response).map_err(Error::from)
}
pub async fn get_with_query<T, Q>(&self, endpoint: &str, query: &Q) -> Result<T>
where
T: DeserializeOwned,
Q: Serialize,
{
let query_string = serde_json::to_string(query)
.map_err(|e| Error::Custom(format!("Failed to serialize query: {}", e)))?;
self.get_cached(endpoint, Some(&query_string)).await
}
pub async fn process_bulk_data<T, F, R>(&self, data: Vec<T>, processor: F) -> Result<Vec<R>>
where
T: Clone + Send + Sync,
F: Fn(Vec<T>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<R>> + Send>>
+ Send
+ Sync,
R: Send,
{
let mut chunk_processor = ChunkProcessor::new(self.config.bulk_processing.clone());
chunk_processor.add_items(data)?;
chunk_processor
.process_chunks(|chunk| processor(chunk))
.await
}
pub async fn get_metrics(&self) -> Option<Metrics> {
if self.config.enable_metrics {
Some(self.executor.get_metrics().await)
} else {
None
}
}
pub async fn get_cache_metrics(&self) -> Option<crate::cache::CacheMetrics> {
if let Some(cache) = &self.cache {
Some(cache.get_metrics().await)
} else {
None
}
}
pub async fn cleanup_cache(&self) {
if let Some(cache) = &self.cache {
cache.cleanup_expired().await;
}
}
fn build_url(&self, path: &str) -> String {
format!("{}/{}", self.config.base_url, path.trim_start_matches('/'))
}
fn init_tracing() {
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
tracing_subscriber::fmt().init();
});
}
pub async fn health_check(&self) -> Result<bool> {
match self
.get_cached::<serde_json::Value>("/search", Some("query=AAPL&limit=1"))
.await
{
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub async fn get_quote(&self, symbol: &str) -> Result<Vec<crate::models::quote::StockQuote>> {
let endpoint = format!("quote/{}", symbol);
self.get_cached(&endpoint, None).await
}
pub async fn get_quotes_bulk(
&self,
symbols: &[&str],
) -> Result<Vec<crate::models::quote::StockQuote>> {
let mut all_quotes = Vec::new();
let symbols_vec: Vec<String> = symbols.iter().map(|s| s.to_string()).collect();
let quotes = self
.process_bulk_data(symbols_vec, |_chunk| {
Box::pin(async move {
let chunk_quotes = Vec::new();
Ok(chunk_quotes)
})
})
.await?;
for mut quote_batch in quotes {
all_quotes.append(&mut quote_batch);
}
Ok(all_quotes)
}
pub async fn check_health(&self) -> Result<bool> {
self.health_check().await
}
pub async fn get_performance_metrics(&self) -> Option<Metrics> {
self.get_metrics().await
}
pub fn quote(&self) -> ProductionQuote {
ProductionQuote::new(self)
}
pub fn company_info(&self) -> ProductionCompanyInfo {
ProductionCompanyInfo::new(self)
}
}
pub struct ProductionClientBuilder {
config: ProductionConfig,
}
impl ProductionClientBuilder {
pub fn new() -> Self {
Self {
config: ProductionConfig::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 rate_limit(mut self, rate_limit: RateLimitConfig) -> Self {
self.config.rate_limit = rate_limit;
self
}
pub fn pool_config(mut self, pool: PoolConfig) -> Self {
self.config.pool = pool;
self
}
pub fn retry_config(mut self, retry: RetryConfig) -> Self {
self.config.retry = retry;
self
}
pub fn cache_config(mut self, cache: CacheConfig) -> Self {
self.config.cache = cache;
self
}
pub fn enable_caching(mut self, enable: bool) -> Self {
self.config.enable_caching = enable;
self
}
pub fn enable_metrics(mut self, enable: bool) -> Self {
self.config.enable_metrics = enable;
self
}
pub fn enable_tracing(mut self, enable: bool) -> Self {
self.config.enable_tracing = enable;
self
}
pub async fn build(self) -> Result<ProductionFmpClient> {
ProductionFmpClient::new(self.config).await
}
}
pub struct ProductionQuote<'a> {
client: &'a ProductionFmpClient,
}
impl<'a> ProductionQuote<'a> {
fn new(client: &'a ProductionFmpClient) -> Self {
Self { client }
}
pub async fn get_quote(&self, symbol: &str) -> Result<serde_json::Value> {
self.client
.get_cached(&format!("/quote/{}", symbol), None)
.await
}
}
pub struct ProductionCompanyInfo<'a> {
client: &'a ProductionFmpClient,
}
impl<'a> ProductionCompanyInfo<'a> {
fn new(client: &'a ProductionFmpClient) -> Self {
Self { client }
}
pub async fn get_profile(&self, symbol: &str) -> Result<serde_json::Value> {
self.client
.get_cached(&format!("/profile/{}", symbol), None)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_production_client_builder() {
let config = ProductionConfig {
api_key: "test_key".to_string(),
enable_caching: false,
enable_metrics: false,
enable_tracing: false,
..Default::default()
};
let client = ProductionFmpClient::new(config).await;
assert!(client.is_ok());
}
#[tokio::test]
async fn test_health_check() {
let config = ProductionConfig {
api_key: "test_key".to_string(),
enable_tracing: false,
..Default::default()
};
let client = ProductionFmpClient::new(config).await.unwrap();
let _result = client.health_check().await;
}
}