digdigdig3 0.3.4

Unified async Rust API for 47 exchange connectors (REST + WebSocket). The core layer — pure ExchangeHub + connectors. Higher-level builder, persistence, replay, OB tracker live in `digdigdig3-station`.
Documentation
//! # Validation Snapshot Loader
//!
//! Embeds `data/validation_snapshot.json` at compile time and exposes
//! a `validation_for(ExchangeId)` lookup.
//!
//! The snapshot is generated by the `e2e_smoke` harness after each live run.
//! It records which REST methods and WS streams returned real data on a specific date.

use std::collections::HashMap;
use std::sync::OnceLock;

use crate::core::types::{ExchangeId, ValidationStamp};

const SNAPSHOT_JSON: &str = include_str!("../../../data/validation_snapshot.json");

static SNAPSHOT: OnceLock<HashMap<ExchangeId, ValidationStamp>> = OnceLock::new();

fn load() -> HashMap<ExchangeId, ValidationStamp> {
    let raw: HashMap<String, ValidationStamp> = serde_json::from_str(SNAPSHOT_JSON)
        .expect("data/validation_snapshot.json is malformed — fix the JSON or regenerate");
    raw.into_iter()
        .filter_map(|(k, v)| ExchangeId::from_str(&k).map(|id| (id, v)))
        .collect()
}

/// Look up the `ValidationStamp` for an exchange.
///
/// Returns `None` if the exchange has no entry in the current snapshot
/// (i.e. it was not in scope for the harness run, or was omitted due to auth-gating).
pub fn validation_for(id: ExchangeId) -> Option<&'static ValidationStamp> {
    SNAPSHOT.get_or_init(load).get(&id)
}

/// Return all entries in the snapshot (sorted by exchange id display name).
pub fn all_entries() -> Vec<(ExchangeId, &'static ValidationStamp)> {
    let map = SNAPSHOT.get_or_init(load);
    let mut entries: Vec<_> = map.iter().map(|(&id, v)| (id, v)).collect();
    entries.sort_by_key(|(id, _)| id.as_str());
    entries
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn snapshot_loads_without_panic() {
        // Just call load() — it will panic if JSON is malformed
        let _ = load();
    }

    #[test]
    fn at_least_five_known_exchanges_present() {
        let map = load();
        for id in [
            ExchangeId::Binance,
            ExchangeId::Bybit,
            ExchangeId::OKX,
            ExchangeId::KuCoin,
            ExchangeId::Kraken,
        ] {
            assert!(
                map.contains_key(&id),
                "Expected {:?} in validation snapshot",
                id
            );
        }
    }

    #[test]
    fn binance_is_validated() {
        let map = load();
        let stamp = map.get(&ExchangeId::Binance).expect("Binance missing from snapshot");
        // harness_version drifts each time the e2e_smoke harness regenerates the
        // snapshot. Don't pin it — just assert it's populated.
        assert!(
            !stamp.harness_version.is_empty(),
            "harness_version is empty for Binance"
        );
        assert!(stamp.rest.contains_key("get_ticker"));
        let ticker = &stamp.rest["get_ticker"];
        assert!(ticker.is_validated(), "Binance get_ticker should be Validated");
    }

    #[test]
    fn bitstamp_ws_is_failed() {
        let map = load();
        let stamp = map.get(&ExchangeId::Bitstamp).expect("Bitstamp missing from snapshot");
        let ws_ticker = &stamp.ws["Ticker"];
        assert!(
            !ws_ticker.is_validated(),
            "Bitstamp WS Ticker was silent — should NOT be Validated"
        );
    }

    #[test]
    fn upbit_ws_is_populated_but_empty() {
        use crate::core::types::FieldValidation;
        let map = load();
        let stamp = map.get(&ExchangeId::Upbit).expect("Upbit missing from snapshot");
        assert!(
            matches!(stamp.ws["Ticker"], FieldValidation::PopulatedButEmpty { .. }),
            "Upbit WS Ticker had parser bug — should be PopulatedButEmpty"
        );
    }

    #[test]
    fn validation_for_static_lookup_consistent() {
        // Static lookup must return the same result as a fresh load() for Binance
        let from_static = validation_for(ExchangeId::Binance);
        let from_fresh = load();
        assert!(from_static.is_some());
        assert_eq!(from_static.unwrap(), from_fresh.get(&ExchangeId::Binance).unwrap());
    }

    #[test]
    fn entry_count_at_least_20() {
        let map = load();
        assert!(
            map.len() >= 20,
            "Expected at least 20 entries in snapshot, got {}",
            map.len()
        );
    }
}