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

@ -345,6 +345,40 @@ pub fn handle(
}
}
#[allow(dead_code)] // wired up once per-subcommand handlers are fully split
pub(crate) fn fetch_with_cache<T, F>(
cache: &Cache,
bucket: &str,
key: &str,
ttl_secs: u64,
offline: bool,
no_cache: bool,
fetch_fn: F,
) -> Result<T, IdxError>
where
T: Serialize + DeserializeOwned,
F: FnOnce() -> Result<T, IdxError>,
{
if !no_cache
&& !offline
&& let Some(cached) = cache.get::<T>(bucket, key)?
{
return Ok(cached);
}
if offline {
return cache
.get_stale::<T>(bucket, key)?
.ok_or_else(|| IdxError::Offline("no cached data available".to_string()));
}
let data = fetch_fn()?;
if !no_cache {
let _ = cache.put(bucket, key, &data, ttl_secs);
}
Ok(data)
}
fn fetch_fundamental_analysis_report<T, F>(
cache: &Cache,
provider: &dyn MarketDataProvider,