loadwise-core 0.1.0

Core traits, strategies, and in-memory stores for loadwise
Documentation
use super::{SelectionContext, Strategy};

/// Tries the primary strategy first, falls back to secondary if primary returns `None`.
///
/// This is a **generic combinator** — zero-cost at runtime because both strategies are
/// monomorphised. Use [`FallbackChain`] when you need a dynamic list of strategies.
///
/// # When does fallback trigger?
///
/// A strategy returns `None` when `candidates` is empty **or** all candidates are
/// excluded via [`SelectionContext::exclude`]. `WithFallback` is most useful when
/// composed with filtered candidate lists or retry-with-exclude patterns.
///
/// # Examples
///
/// ```
/// # extern crate loadwise_core as loadwise;
/// use loadwise::{Strategy, SelectionContext};
/// use loadwise::strategy::{Random, RoundRobin, WithFallback};
///
/// // Prefer random, but fall back to round-robin if it returns None.
/// let strategy = WithFallback::new(Random::new(), RoundRobin::new());
///
/// let nodes = [1, 2, 3];
/// let ctx = SelectionContext::default();
/// assert!(strategy.select(&nodes, &ctx).is_some());
/// ```
pub struct WithFallback<P, F> {
    primary: P,
    fallback: F,
}

impl<P: std::fmt::Debug, F: std::fmt::Debug> std::fmt::Debug for WithFallback<P, F> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("WithFallback")
            .field("primary", &self.primary)
            .field("fallback", &self.fallback)
            .finish()
    }
}

impl<P, F> WithFallback<P, F> {
    pub fn new(primary: P, fallback: F) -> Self {
        Self { primary, fallback }
    }
}

impl<N, P, F> Strategy<N> for WithFallback<P, F>
where
    P: Strategy<N>,
    F: Strategy<N>,
{
    fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
        self.primary
            .select(candidates, ctx)
            .or_else(|| self.fallback.select(candidates, ctx))
    }
}

/// Tries each strategy in order until one returns a result.
///
/// Unlike [`WithFallback`] (which is generic and zero-cost), `FallbackChain` uses
/// trait objects — suitable when the strategy list is built at runtime.
pub struct FallbackChain<N> {
    strategies: Vec<Box<dyn Strategy<N> + Send + Sync>>,
}

impl<N> std::fmt::Debug for FallbackChain<N> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("FallbackChain")
            .field("strategies", &self.strategies.len())
            .finish()
    }
}

impl<N> FallbackChain<N> {
    pub fn new(strategies: Vec<Box<dyn Strategy<N> + Send + Sync>>) -> Self {
        Self { strategies }
    }
}

impl<N> Strategy<N> for FallbackChain<N> {
    fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
        self.strategies
            .iter()
            .find_map(|s| s.select(candidates, ctx))
    }
}

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

    #[test]
    fn with_fallback_propagates_exclude() {
        let strategy = WithFallback::new(RoundRobin::new(), RoundRobin::new());
        let nodes = [1, 2];
        let ctx = SelectionContext::builder().exclude(vec![0, 1]).build();
        // Both strategies see the same exclude — both return None
        assert_eq!(strategy.select(&nodes, &ctx), None);
    }

    #[test]
    fn fallback_chain_propagates_exclude() {
        let chain: FallbackChain<i32> = FallbackChain::new(vec![
            Box::new(RoundRobin::new()),
            Box::new(RoundRobin::new()),
        ]);
        let nodes = [1, 2];
        let ctx = SelectionContext::builder().exclude(vec![0, 1]).build();
        assert_eq!(chain.select(&nodes, &ctx), None);
    }

    #[test]
    fn empty_candidates_returns_none() {
        let strategy = WithFallback::new(RoundRobin::new(), RoundRobin::new());
        let nodes: [i32; 0] = [];
        let ctx = SelectionContext::default();
        assert_eq!(strategy.select(&nodes, &ctx), None);
    }

    #[test]
    fn fallback_used_when_primary_excludes_all() {
        // Primary: RoundRobin on [1, 2] with both excluded → None
        // Fallback: RoundRobin on same → also None (same ctx)
        // This verifies fallback IS called even though primary had candidates.
        let strategy = WithFallback::new(RoundRobin::new(), RoundRobin::new());
        let nodes = [1, 2];
        let ctx = SelectionContext::builder().exclude(vec![0, 1]).build();
        assert_eq!(strategy.select(&nodes, &ctx), None);
    }
}