que-luyo 0.1.0

Transactional bite-sized FIFO queue consumption with automatic rollback
Documentation
//! Transactional, bite-sized FIFO queue consumption.
//!
//! A [`Bite`] temporarily removes a prefix from a [`BiteQueue`]. Committing
//! keeps it removed; every other exit path restores it in original order.
//!
//! ```
//! use que_luyo::BiteQueue;
//!
//! let mut queue = BiteQueue::from_iter([1, 2, 3]);
//! {
//!     let bite = queue.bite(2)?;
//!     assert_eq!(bite.iter().copied().collect::<Vec<_>>(), [1, 2]);
//! }
//! assert_eq!(queue.iter().copied().collect::<Vec<_>>(), [1, 2, 3]);
//!
//! let eaten = queue.bite(2)?.commit();
//! assert_eq!(eaten, [1, 2]);
//! # Ok::<(), que_luyo::BiteError>(())
//! ```

use std::collections::VecDeque;
use std::error::Error;
use std::fmt::{self, Debug, Display, Formatter};
use std::iter::FromIterator;

/// A FIFO queue that lends transactional batches.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BiteQueue<T> {
    items: VecDeque<T>,
}

impl<T> BiteQueue<T> {
    /// Creates an empty queue.
    pub const fn new() -> Self {
        Self {
            items: VecDeque::new(),
        }
    }

    /// Creates an empty queue with space for at least `capacity` items.
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            items: VecDeque::with_capacity(capacity),
        }
    }

    /// Adds an item at the back.
    pub fn push(&mut self, item: T) {
        self.items.push_back(item);
    }

    /// Returns the next item without removing it.
    pub fn front(&self) -> Option<&T> {
        self.items.front()
    }

    /// Returns the number of queued items.
    pub fn len(&self) -> usize {
        self.items.len()
    }

    /// Returns whether the queue contains no items.
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    /// Iterates in FIFO order without removing items.
    pub fn iter(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator {
        self.items.iter()
    }

    /// Takes up to `maximum` items as a transactional bite.
    ///
    /// Dropping the returned bite without committing it restores the items.
    pub fn bite(&mut self, maximum: usize) -> Result<Bite<'_, T>, BiteError> {
        if maximum == 0 {
            return Err(BiteError::ZeroSize);
        }
        let amount = maximum.min(self.items.len());
        Ok(self.take_prefix(amount))
    }

    /// Takes exactly `amount` items or leaves the queue unchanged.
    pub fn bite_exact(&mut self, amount: usize) -> Result<Bite<'_, T>, BiteError> {
        if amount == 0 {
            return Err(BiteError::ZeroSize);
        }
        if self.items.len() < amount {
            return Err(BiteError::InsufficientItems {
                requested: amount,
                available: self.items.len(),
            });
        }
        Ok(self.take_prefix(amount))
    }

    /// Consumes the wrapper and returns the underlying FIFO storage.
    pub fn into_inner(self) -> VecDeque<T> {
        self.items
    }

    fn take_prefix(&mut self, amount: usize) -> Bite<'_, T> {
        let items = self.items.drain(..amount).collect();
        Bite {
            source: self,
            items,
            finished: false,
        }
    }
}

impl<T> Extend<T> for BiteQueue<T> {
    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
        self.items.extend(iter);
    }
}

impl<T> FromIterator<T> for BiteQueue<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        Self {
            items: iter.into_iter().collect(),
        }
    }
}

impl<T> IntoIterator for BiteQueue<T> {
    type Item = T;
    type IntoIter = std::collections::vec_deque::IntoIter<T>;

    fn into_iter(self) -> Self::IntoIter {
        self.items.into_iter()
    }
}

impl<'a, T> IntoIterator for &'a BiteQueue<T> {
    type Item = &'a T;
    type IntoIter = std::collections::vec_deque::Iter<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.items.iter()
    }
}

/// A borrowed, transactional prefix of a [`BiteQueue`].
///
/// The source queue stays exclusively borrowed until this value is committed,
/// explicitly rolled back, or dropped.
#[must_use = "an uncommitted bite automatically rolls back"]
pub struct Bite<'a, T> {
    source: &'a mut BiteQueue<T>,
    items: VecDeque<T>,
    finished: bool,
}

impl<T: Debug> Debug for Bite<'_, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("Bite")
            .field("items", &self.items)
            .field("finished", &self.finished)
            .finish_non_exhaustive()
    }
}

impl<T> Bite<'_, T> {
    /// Number of items in this bite.
    pub fn len(&self) -> usize {
        self.items.len()
    }

    /// Returns whether this bite contains no items.
    ///
    /// A bite from an empty queue can be empty even though its requested
    /// maximum was non-zero.
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    /// First item in this bite.
    pub fn front(&self) -> Option<&T> {
        self.items.front()
    }

    /// Iterates over the bite in original FIFO order.
    pub fn iter(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator {
        self.items.iter()
    }

    /// Commits the bite and returns its items in FIFO order.
    pub fn commit(mut self) -> Vec<T> {
        self.finished = true;
        self.items.drain(..).collect()
    }

    /// Explicitly restores the bite to the queue.
    ///
    /// Dropping without calling either `commit` or `rollback` has the same
    /// restoration behavior.
    pub fn rollback(mut self) {
        self.restore();
    }

    fn restore(&mut self) {
        while let Some(item) = self.items.pop_back() {
            self.source.items.push_front(item);
        }
        self.finished = true;
    }
}

impl<T> Drop for Bite<'_, T> {
    fn drop(&mut self) {
        if !self.finished {
            self.restore();
        }
    }
}

impl<'a, T> IntoIterator for &'a Bite<'_, T> {
    type Item = &'a T;
    type IntoIter = std::collections::vec_deque::Iter<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.items.iter()
    }
}

/// Invalid bite request.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BiteError {
    /// A zero-sized bite was requested.
    ZeroSize,
    /// An exact bite requested more items than were queued.
    InsufficientItems {
        /// Exact number requested.
        requested: usize,
        /// Number available when requested.
        available: usize,
    },
}

impl Display for BiteError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::ZeroSize => f.write_str("bite size must be greater than zero"),
            Self::InsufficientItems {
                requested,
                available,
            } => write!(
                f,
                "requested {requested} items, but only {available} are available"
            ),
        }
    }
}

impl Error for BiteError {}

#[cfg(test)]
mod tests {
    use super::*;
    use std::panic::{AssertUnwindSafe, catch_unwind};

    fn values(queue: &BiteQueue<i32>) -> Vec<i32> {
        queue.iter().copied().collect()
    }

    #[test]
    fn commit_permanently_consumes_a_prefix() {
        let mut queue = BiteQueue::from_iter([1, 2, 3, 4]);
        let committed = queue.bite(2).unwrap().commit();
        assert_eq!(committed, [1, 2]);
        assert_eq!(values(&queue), [3, 4]);
    }

    #[test]
    fn drop_rolls_back_in_original_order() {
        let mut queue = BiteQueue::from_iter([1, 2, 3]);
        {
            let bite = queue.bite(2).unwrap();
            assert_eq!(bite.iter().copied().collect::<Vec<_>>(), [1, 2]);
        }
        assert_eq!(values(&queue), [1, 2, 3]);
    }

    #[test]
    fn explicit_rollback_restores_the_prefix() {
        let mut queue = BiteQueue::from_iter([1, 2, 3]);
        queue.bite(2).unwrap().rollback();
        assert_eq!(values(&queue), [1, 2, 3]);
    }

    #[test]
    fn panic_unwinding_rolls_back() {
        let mut queue = BiteQueue::from_iter([1, 2, 3]);
        let result = catch_unwind(AssertUnwindSafe(|| {
            let _bite = queue.bite(2).unwrap();
            panic!("processing failed");
        }));
        assert!(result.is_err());
        assert_eq!(values(&queue), [1, 2, 3]);
    }

    #[test]
    fn bite_uses_at_most_the_requested_size() {
        let mut queue = BiteQueue::from_iter([1, 2]);
        let bite = queue.bite(10).unwrap();
        assert_eq!(bite.len(), 2);
    }

    #[test]
    fn exact_failure_does_not_change_the_queue() {
        let mut queue = BiteQueue::from_iter([1, 2]);
        assert_eq!(
            queue.bite_exact(3).unwrap_err(),
            BiteError::InsufficientItems {
                requested: 3,
                available: 2,
            }
        );
        assert_eq!(values(&queue), [1, 2]);
    }

    #[test]
    fn zero_size_is_rejected() {
        let mut queue = BiteQueue::<i32>::new();
        assert_eq!(queue.bite(0).unwrap_err(), BiteError::ZeroSize);
        assert_eq!(queue.bite_exact(0).unwrap_err(), BiteError::ZeroSize);
    }
}