mod client;
pub mod models;
mod treasury;
use crate::error::{FinanceError, Result};
use crate::rate_limiter::RateLimiter;
use client::FredClientBuilder;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
pub use models::{MacroObservation, MacroSeries, TreasuryYield};
const FRED_RATE_PER_SEC: f64 = 2.0;
struct FredSingleton {
api_key: String,
timeout: Duration,
limiter: Arc<RateLimiter>,
}
static FRED_SINGLETON: OnceLock<FredSingleton> = OnceLock::new();
pub fn init(api_key: impl Into<String>) -> Result<()> {
init_with_timeout(api_key, Duration::from_secs(30))
}
pub fn init_with_timeout(api_key: impl Into<String>, timeout: Duration) -> Result<()> {
FRED_SINGLETON
.set(FredSingleton {
api_key: api_key.into(),
timeout,
limiter: Arc::new(RateLimiter::new(FRED_RATE_PER_SEC)),
})
.map_err(|_| FinanceError::InvalidParameter {
param: "fred".to_string(),
reason: "FRED client already initialized".to_string(),
})
}
pub async fn series(series_id: &str) -> Result<MacroSeries> {
let s = FRED_SINGLETON
.get()
.ok_or_else(|| FinanceError::InvalidParameter {
param: "fred".to_string(),
reason: "FRED not initialized. Call fred::init(api_key) first.".to_string(),
})?;
let c = FredClientBuilder::new(&s.api_key)
.timeout(s.timeout)
.build_with_limiter(Arc::clone(&s.limiter))?;
c.series(series_id).await
}
pub async fn treasury_yields(year: u32) -> Result<Vec<TreasuryYield>> {
treasury::fetch_yields(year).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init_errors_on_double_init() {
let _ = init("test-key-1");
let result = init("test-key-2");
assert!(matches!(result, Err(FinanceError::InvalidParameter { .. })));
}
#[test]
fn test_series_without_init_fails_gracefully() {
if FRED_SINGLETON.get().is_none() {
let err = FinanceError::InvalidParameter {
param: "fred".to_string(),
reason: "not initialized".to_string(),
};
assert!(matches!(err, FinanceError::InvalidParameter { .. }));
}
}
}