refactor: schema-driven architecture, capability traits, hardened error paths

- Split parse.rs into raw_types.rs (serde structs) + map.rs (pure transforms) for MSN and Yahoo
- Replace Yahoo fundamentals dynamic HashMap with typed structs (SummaryDetail, DefaultKeyStatistics, etc.)
- Introduce capability-based provider traits: QuoteProvider, FundamentalsProvider, HistoryProvider
- Add future MSN capability traits: ProfileProvider, EarningsProvider, FinancialsProvider,
  SentimentProvider, InsightsProvider, NewsProvider (all dead_code until wired to CLI)
- Add shared domain types in src/api/types.rs (CompanyProfile, EarningsReport, FinancialStatements,
  SentimentData, InsightData, NewsItem)
- Harden error propagation: Yahoo cookie auth, MSN partial fundamentals, history symbol context,
  cache clear failures
- Strict config parsing: invalid IDX_OUTPUT returns ConfigError instead of silent fallback
- Cache schema version enforcement: version mismatch treated as cache miss
- Extract fetch_with_cache() helper in cli/stocks.rs
- Add MSN retry/backoff parity with Yahoo client
- Standardize Option<T> policy through parse/map layers
- All 56 tests passing, clippy clean
This commit is contained in:
Ciphercat 2026-03-06 19:17:50 +00:00
commit 3998e38ffc
17 changed files with 1185 additions and 969 deletions

View file

@ -1,13 +1,15 @@
mod client;
mod map;
mod parse;
mod raw_types;
mod symbols;
use crate::api::MarketDataProvider;
use crate::api::types::{Fundamentals, Interval, Ohlc, Period, Quote};
use crate::api::types::{Bar, Fundamentals, Interval, Period, Quote};
use crate::api::{FundamentalsProvider, HistoryProvider, QuoteProvider};
use crate::error::IdxError;
use client::MsnClient;
use parse::{parse_fundamentals, parse_quote};
use map::{parse_fundamentals, parse_quote};
pub(crate) use parse::{parse_fundamentals_from_str, parse_history_from_str, parse_quote_from_str};
@ -15,46 +17,38 @@ const HISTORY_UNSUPPORTED_REASON: &str = "MSN provider does not currently suppor
pub struct MsnProvider {
client: MsnClient,
verbose: bool,
}
impl MsnProvider {
pub fn new(verbose: bool) -> Self {
pub fn new(_verbose: bool) -> Self {
Self {
client: MsnClient::new(),
verbose,
}
}
}
impl MarketDataProvider for MsnProvider {
impl QuoteProvider for MsnProvider {
fn quote(&self, symbol: &str) -> Result<Quote, IdxError> {
let quotes = self.client.fetch_quotes(symbol)?;
parse_quote(symbol, &quotes)
}
}
impl FundamentalsProvider for MsnProvider {
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError> {
let ratios = self.client.fetch_key_ratios(symbol)?;
let quote = self
.client
.fetch_quotes(symbol)
.map_err(|e| {
if self.verbose {
eprintln!("warning: quote fetch for fundamentals failed: {e}");
}
e
})
.ok()
.and_then(|quotes| quotes.into_iter().next());
parse_fundamentals(&ratios, quote.as_ref())
let quote = self.client.fetch_quotes(symbol)?;
parse_fundamentals(&ratios, quote.first())
}
}
impl HistoryProvider for MsnProvider {
fn history(
&self,
_symbol: &str,
_period: &Period,
_interval: &Interval,
) -> Result<Vec<Ohlc>, IdxError> {
) -> Result<Vec<Bar>, IdxError> {
Err(IdxError::Unsupported(
HISTORY_UNSUPPORTED_REASON.to_string(),
))
@ -64,7 +58,7 @@ impl MarketDataProvider for MsnProvider {
#[cfg(test)]
mod tests {
use super::MsnProvider;
use crate::api::MarketDataProvider;
use crate::api::HistoryProvider;
use crate::api::types::{Interval, Period};
use crate::error::IdxError;