simulator-api 0.19.0

Wire-protocol types for the Solana simulator backtest WebSocket API
Documentation
//! An inclusive slot range.
//!
//! Slot bounds travel in pairs through most of the pipeline. As two adjacent
//! `u64` parameters nothing stops a caller transposing them, and the ordering
//! invariant gets re-checked ad hoc (or not at all) at each layer. This carries
//! both together and checks once, at construction.

use std::fmt;

use serde::{Deserialize, Serialize};

/// An inclusive `[start, end]` slot range. Construct via [`SlotRange::new`],
/// which rejects an inverted range, or [`SlotRange::new_unchecked`] where the
/// ordering is already guaranteed by the caller.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SlotRange {
    start: u64,
    end: u64,
}

/// Why a pair of bounds isn't a valid range.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("invalid range: start_slot {start} > end_slot {end}")]
pub struct InvertedSlotRange {
    pub start: u64,
    pub end: u64,
}

impl SlotRange {
    /// An inclusive range, rejecting `start > end`.
    pub fn new(start: u64, end: u64) -> Result<Self, InvertedSlotRange> {
        if start > end {
            return Err(InvertedSlotRange { start, end });
        }
        Ok(Self { start, end })
    }

    /// A range whose ordering the caller already guarantees — e.g. one derived
    /// from a `start` plus a non-zero count. Prefer [`SlotRange::new`].
    pub const fn new_unchecked(start: u64, end: u64) -> Self {
        Self { start, end }
    }

    /// The range covering `count` slots from `start`. `count` of 0 yields a
    /// single-slot range, matching how the callers' slot counts behave.
    pub const fn from_count(start: u64, count: u64) -> Self {
        Self {
            start,
            end: start.saturating_add(count.saturating_sub(1)),
        }
    }

    pub const fn start(&self) -> u64 {
        self.start
    }

    pub const fn end(&self) -> u64 {
        self.end
    }

    /// Number of slots covered, inclusive of both bounds.
    pub const fn len(&self) -> u64 {
        self.end - self.start + 1
    }

    /// Never true — an inclusive range always covers at least one slot. Present
    /// so `len()` doesn't trip `clippy::len_without_is_empty`.
    pub const fn is_empty(&self) -> bool {
        false
    }

    pub const fn contains(&self, slot: u64) -> bool {
        slot >= self.start && slot <= self.end
    }
}

impl fmt::Display for SlotRange {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}..={}", self.start, self.end)
    }
}

impl From<SlotRange> for std::ops::RangeInclusive<u64> {
    fn from(range: SlotRange) -> Self {
        range.start..=range.end
    }
}

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

    #[test]
    fn an_inverted_range_is_rejected() {
        assert_eq!(
            SlotRange::new(10, 9),
            Err(InvertedSlotRange { start: 10, end: 9 })
        );
        assert!(SlotRange::new(10, 10).is_ok());
    }

    #[test]
    fn length_is_inclusive_of_both_bounds() {
        assert_eq!(SlotRange::new(10, 10).expect("range").len(), 1);
        assert_eq!(SlotRange::new(10, 19).expect("range").len(), 10);
    }

    #[test]
    fn from_count_matches_the_start_plus_count_convention() {
        assert_eq!(
            SlotRange::from_count(100, 1),
            SlotRange::new_unchecked(100, 100)
        );
        assert_eq!(
            SlotRange::from_count(100, 10),
            SlotRange::new_unchecked(100, 109)
        );
        // A zero count still covers the anchor slot rather than inverting.
        assert_eq!(
            SlotRange::from_count(100, 0),
            SlotRange::new_unchecked(100, 100)
        );
    }

    #[test]
    fn contains_is_inclusive() {
        let range = SlotRange::new(10, 12).expect("range");
        assert!(range.contains(10) && range.contains(12));
        assert!(!range.contains(9) && !range.contains(13));
    }
}