434 lines
13 KiB
Rust
434 lines
13 KiB
Rust
// Tripping iterator
|
||
//
|
||
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
|
||
//
|
||
// This file is part of TAME.
|
||
//
|
||
// This program is free software: you can redistribute it and/or modify
|
||
// it under the terms of the GNU General Public License as published by
|
||
// the Free Software Foundation, either version 3 of the License, or
|
||
// (at your option) any later version.
|
||
//
|
||
// This program is distributed in the hope that it will be useful,
|
||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
// GNU General Public License for more details.
|
||
//
|
||
// You should have received a copy of the GNU General Public License
|
||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
//! An iterator that wraps [`ResultIterator`] and trips on the first
|
||
//! [`Err`].
|
||
//!
|
||
//! This acts as a circuit breaker---when
|
||
//! an error occurs,
|
||
//! the iterator trips to prevent further damage,
|
||
//! halting downstream processing while providing the opportunity to
|
||
//! inspect and act upon the error.
|
||
//!
|
||
//! This iterator simplifies the management of [`ResultIterator`]s and
|
||
//! allows downstram APIs of those iterators to remain simple without
|
||
//! having to concern themselves with error conditions from a source
|
||
//! iterator.
|
||
//!
|
||
//! _Support for immediate error recovery has not yet been added._
|
||
//! It will be once it is needed.
|
||
//!
|
||
//! See the [parent module](super) and [`TripIter`] for more information.
|
||
|
||
use super::ResultIterator;
|
||
use std::marker::PhantomData;
|
||
|
||
/// An iterator that trips when encountering an [`Err`] on an underling
|
||
/// iterator.
|
||
///
|
||
/// This iterator can be constructed using one of
|
||
///
|
||
/// 1. [`with_iter_while_ok`] to retain ownership over the source
|
||
/// iterator; or
|
||
/// 2. [`into_iter_while_ok`] for a more ergonomic API at the cost of
|
||
/// ceding ownership over the source iterator.
|
||
///
|
||
/// See those two functions and the [parent module](super) for more
|
||
/// information and examples.
|
||
pub struct TripIter<'a, I: ResultIterator<T, EI>, T, EI> {
|
||
/// Iterator that will be unwrapped while its item is [`Ok`].
|
||
inner: &'a mut I,
|
||
|
||
/// The current state of the iterator,
|
||
/// including the most recently encountered error.
|
||
///
|
||
/// If there is an error,
|
||
/// then we have been tripped and will return [`None`] until the error
|
||
/// condition has been resolved.
|
||
state: Result<(), EI>,
|
||
|
||
/// Our [`Iterator`] implementation will yield `T`,
|
||
/// but we don't store it.
|
||
_phantom: PhantomData<T>,
|
||
}
|
||
|
||
impl<'a, I: ResultIterator<T, EI>, T, EI> TripIter<'a, I, T, EI> {
|
||
/// Given a mutable reference to a
|
||
/// [`ResultIterator<T, E>`](ResultIterator),
|
||
/// yield a [`TripIter`] that yields the inner `T` value while the
|
||
/// iterator yields an [`Ok`] item.
|
||
///
|
||
/// See the public [`with_iter_while_ok`] function for more
|
||
/// information.
|
||
#[inline]
|
||
fn with_iter_while_ok<F, U, E>(iter: &'a mut I, f: F) -> Result<U, E>
|
||
where
|
||
F: FnOnce(&mut Self) -> Result<U, E>,
|
||
EI: Into<E>,
|
||
{
|
||
let mut biter = Self {
|
||
inner: iter,
|
||
state: Ok(()),
|
||
_phantom: Default::default(),
|
||
};
|
||
|
||
// The TripIter will be available only for the duration of this
|
||
// call...
|
||
let fret = f(&mut biter);
|
||
|
||
// ...which ensures that we have the opportunity,
|
||
// after the caller is done with it,
|
||
// to check to see if we have tripped and force the caller to
|
||
// consider the error.
|
||
biter.state.map_err(EI::into).and(fret)
|
||
}
|
||
|
||
/// Whether the iterator has been tripped by an [`Err`] on the
|
||
/// underlying iterator.
|
||
///
|
||
/// Until a recovery mechanism is provided,
|
||
/// the [`Err`] is available through one of [`with_iter_while_ok`] or
|
||
/// [`into_iter_while_ok`].
|
||
#[inline]
|
||
pub fn is_tripped(&self) -> bool {
|
||
self.state.is_err()
|
||
}
|
||
}
|
||
|
||
impl<'a, I, T, EI> Iterator for TripIter<'a, I, T, EI>
|
||
where
|
||
I: Iterator<Item = Result<T, EI>>,
|
||
{
|
||
type Item = T;
|
||
|
||
/// Unwrap and return the next [`Ok`] result from the underlying
|
||
/// iterator,
|
||
/// otherwise [`None`].
|
||
///
|
||
/// Once an [`Err`] is encountered,
|
||
/// this iterator trips and will continue to return [`None`] until the
|
||
/// error state is resolved.
|
||
/// See [`with_iter_while_ok`] for more information.
|
||
#[inline]
|
||
fn next(&mut self) -> Option<Self::Item> {
|
||
if self.is_tripped() {
|
||
return None;
|
||
}
|
||
|
||
match self.inner.next() {
|
||
Some(Ok(value)) => Some(value),
|
||
Some(Err(err)) => {
|
||
self.state = Err(err);
|
||
None
|
||
}
|
||
None => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Given a mutable reference to a
|
||
/// [`ResultIterator<T, EI>`](ResultIterator),
|
||
/// yield a [`TripIter`] that yields the inner `T` value while the
|
||
/// iterator yields an [`Ok`] item.
|
||
///
|
||
/// Once an [`Err`] is encountered,
|
||
/// [`TripIter`] trips,
|
||
/// yielding [`None`] until the error condition is resolved.
|
||
/// This allows for a recovery mechanism that resumes computation,
|
||
/// provided that the system expects [`TripIter`] to be resumable.
|
||
///
|
||
/// The [`TripIter`] is provided as an argument to a callback `f`
|
||
/// and is valid only for its duration.
|
||
/// This allows us to return either the most recently encountered [`Err`],
|
||
/// otherwise the return value of `f`,
|
||
/// ensuring that the error causing the trip will not be lost.
|
||
///
|
||
/// The callback function `f` must return a [`Result`] whose error type is
|
||
/// [`Into<E>`](Into);
|
||
/// this allows [`Result`]s to be continuously flattened into a
|
||
/// consistent type,
|
||
/// which is especially useful when nesting [`TripIter`].
|
||
/// This puts the error type in control of the caller via the callback.
|
||
/// Since nested [`TripIter`] using this function must share the same error
|
||
/// type `E`,
|
||
/// this operation is monadic.
|
||
///
|
||
/// This function accepts a mutable reference to the underlying iterator,
|
||
/// allowing the caller to retain ownership for further processing after
|
||
/// iteration completes.
|
||
/// If this is not needed
|
||
/// (e.g. all processing will be performed in `f`),
|
||
/// [`into_iter_while_ok`] may provide a more ergonomic API at the cost of
|
||
/// ownership.
|
||
///
|
||
/// ```
|
||
/// use tamer::iter::with_iter_while_ok;
|
||
///
|
||
/// let mut values = [Ok(0), Err("trip"), Ok(1)].into_iter();
|
||
///
|
||
/// let result = with_iter_while_ok(&mut values, |iter| {
|
||
/// // First is `Ok`, so it yields.
|
||
/// assert_eq!(Some(0), iter.next());
|
||
/// assert!(!iter.is_tripped());
|
||
///
|
||
/// // But the next is an `Err`, so it trips and returns `None`
|
||
/// // from that point onward.
|
||
/// assert_eq!(None, iter.next());
|
||
/// assert_eq!(None, iter.next());
|
||
/// assert!(iter.is_tripped());
|
||
///
|
||
/// Ok(())
|
||
/// });
|
||
///
|
||
/// // The error that caused the trip is returned.
|
||
/// assert_eq!(Err("trip"), result);
|
||
///
|
||
/// // We still have access to the iterator where it left off.
|
||
/// assert_eq!(Some(Ok(1)), values.next());
|
||
/// ```
|
||
///
|
||
/// See the [parent module](super) for more information and examples.
|
||
#[inline]
|
||
pub fn with_iter_while_ok<'a, I, T, U, E, EI>(
|
||
from: &'a mut I,
|
||
f: impl FnOnce(&mut TripIter<'a, I, T, EI>) -> Result<U, E>,
|
||
) -> Result<U, E>
|
||
where
|
||
I: ResultIterator<T, EI>,
|
||
EI: Into<E>,
|
||
{
|
||
TripIter::with_iter_while_ok(from, f)
|
||
}
|
||
|
||
/// Given an object capable of being converted into a
|
||
/// [`ResultIterator<T, EI>`](ResultIterator),
|
||
/// yield a [`TripIter`] that yields the inner `T` value while the
|
||
/// iterator yields an [`Ok`] item.
|
||
///
|
||
/// This is a more ergonomic form of [`with_iter_while_ok`] if ownership
|
||
/// over the `from` iterator can be ceded;
|
||
/// see that function for more information.
|
||
///
|
||
/// ```
|
||
/// use tamer::iter::into_iter_while_ok;
|
||
///
|
||
/// let values = [Ok(0), Err("trip"), Ok(1)];
|
||
///
|
||
/// let result = into_iter_while_ok(values, |iter| {
|
||
/// // First is `Ok`, so it yields.
|
||
/// assert_eq!(Some(0), iter.next());
|
||
/// assert!(!iter.is_tripped());
|
||
///
|
||
/// // But the next is an `Err`, so it trips and returns `None`
|
||
/// // from that point onward.
|
||
/// assert_eq!(None, iter.next());
|
||
/// assert_eq!(None, iter.next());
|
||
/// assert!(iter.is_tripped());
|
||
///
|
||
/// Ok(())
|
||
/// });
|
||
///
|
||
/// // The error that caused the trip is returned.
|
||
/// assert_eq!(Err("trip"), result);
|
||
///
|
||
/// // But we cannot access the remainder of the iterator, having
|
||
/// // lost ownership. See `with_iter_while_ok` if this is needed.
|
||
/// ```
|
||
///
|
||
/// See the [parent module](super) for more information and examples.
|
||
#[inline]
|
||
pub fn into_iter_while_ok<I, T, U, E, EI>(
|
||
from: I,
|
||
f: impl FnOnce(&mut TripIter<I::IntoIter, T, EI>) -> Result<U, E>,
|
||
) -> Result<U, E>
|
||
where
|
||
I: IntoIterator<Item = Result<T, EI>>,
|
||
EI: Into<E>,
|
||
{
|
||
with_iter_while_ok(&mut from.into_iter(), f)
|
||
}
|
||
|
||
/// An [`Iterator`] supporting the [`while_ok`](TrippableIterator::while_ok)
|
||
/// and [`into_while_ok`](TrippableIterator::into_while_ok) trip
|
||
/// operations.
|
||
///
|
||
/// For more information,
|
||
/// see the [module-level documentation](self).
|
||
pub trait TrippableIterator<T, EI>
|
||
where
|
||
Self: Iterator<Item = Result<T, EI>> + Sized,
|
||
{
|
||
/// Given a mutable reference to a
|
||
/// [`ResultIterator<T, E>`](ResultIterator),
|
||
/// yield a [`TripIter`] that yields the inner `T` value while the
|
||
/// iterator yields an [`Ok`] item.
|
||
///
|
||
/// For more information,
|
||
/// see [`with_iter_while_ok`] and the
|
||
/// [module-level documentation](super).
|
||
#[inline]
|
||
fn while_ok<F, U, E>(&mut self, f: F) -> Result<U, E>
|
||
where
|
||
F: FnOnce(&mut TripIter<Self, T, EI>) -> Result<U, E>,
|
||
EI: Into<E>,
|
||
{
|
||
TripIter::with_iter_while_ok(self, f)
|
||
}
|
||
|
||
/// Given an object capable of being converted into a
|
||
/// [`ResultIterator<T, E>`](ResultIterator),
|
||
/// yield a [`TripIter`] that yields the inner `T` value while the
|
||
/// iterator yields an [`Ok`] item.
|
||
///
|
||
/// For more information,
|
||
/// see [`into_iter_while_ok`] and the [module-level documentation](super).
|
||
#[inline]
|
||
fn into_while_ok<F, U, E>(mut self, f: F) -> Result<U, E>
|
||
where
|
||
F: FnOnce(&mut TripIter<Self, T, EI>) -> Result<U, E>,
|
||
EI: Into<E>,
|
||
{
|
||
self.while_ok(f)
|
||
}
|
||
}
|
||
|
||
impl<T, E, I> TrippableIterator<T, E> for I where
|
||
I: Iterator<Item = Result<T, E>> + Sized
|
||
{
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod test {
|
||
use super::*;
|
||
|
||
type TestResult<E = ()> = Result<(), E>;
|
||
|
||
#[test]
|
||
fn inner_none_yields_none() -> TestResult {
|
||
let empty = Vec::<TestResult>::new();
|
||
|
||
into_iter_while_ok(empty, |sut| {
|
||
assert_eq!(None, sut.next());
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn ok_yields_unwrapped_values() -> TestResult {
|
||
let value1 = "inner1";
|
||
let value2 = "inner2";
|
||
|
||
let iter = [Result::<_, ()>::Ok(value1), Ok(value2)];
|
||
|
||
into_iter_while_ok(iter, |sut| {
|
||
assert_eq!(Some(value1), sut.next());
|
||
assert_eq!(Some(value2), sut.next());
|
||
assert_eq!(None, sut.next());
|
||
|
||
assert!(!sut.is_tripped());
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn err_yields_none_and_trips() {
|
||
let value = "okay value";
|
||
let err = "tripped";
|
||
|
||
let result =
|
||
into_iter_while_ok([Ok(value), Err(err), Ok(value)], |sut| {
|
||
// The first value is `Ok`,
|
||
// but the second is `Err` which trips the breaker and does not
|
||
// yield any further elements.
|
||
assert_eq!(Some(value), sut.next());
|
||
assert!(!sut.is_tripped());
|
||
|
||
// Trips here.
|
||
assert_eq!(None, sut.next());
|
||
assert!(sut.is_tripped());
|
||
|
||
// Nor should it ever, while tripped.
|
||
assert_eq!(None, sut.next());
|
||
assert!(sut.is_tripped());
|
||
|
||
Ok("")
|
||
});
|
||
|
||
assert_eq!(result, Err(err));
|
||
}
|
||
|
||
#[test]
|
||
fn trip_stops_consuming_iter() {
|
||
let mut values = [Err("trip"), Ok(1)].into_iter();
|
||
|
||
assert_eq!(
|
||
Err("trip"),
|
||
with_iter_while_ok(&mut values, |sut| {
|
||
// Try to consume everything.
|
||
assert_eq!(None, sut.next());
|
||
assert_eq!(None, sut.next());
|
||
|
||
Ok(())
|
||
}),
|
||
);
|
||
|
||
// Should have only consumed up to the `Err`.
|
||
assert_eq!(Some(Ok(1)), values.next());
|
||
}
|
||
|
||
#[test]
|
||
fn open_returns_f_ret_after_none() {
|
||
let ret = "retval";
|
||
assert_eq!(
|
||
Result::<_, ()>::Ok(ret),
|
||
into_iter_while_ok([Result::<_, ()>::Ok(())], |sut| {
|
||
sut.next();
|
||
Ok(ret)
|
||
})
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn tripped_ignores_f_ret_after_none() {
|
||
let err = "tripped ignore ret";
|
||
assert_eq!(
|
||
Err(err),
|
||
into_iter_while_ok([Result::<(), _>::Err(err)], |sut| {
|
||
sut.next();
|
||
Ok("not used")
|
||
})
|
||
);
|
||
}
|
||
|
||
// If we don't call `next`,
|
||
// then we haven't hit the error and so it should not trip.
|
||
#[test]
|
||
fn does_not_trip_before_err_is_encountered() {
|
||
let ret = "no next";
|
||
assert_eq!(
|
||
Result::<_, &str>::Ok(ret),
|
||
into_iter_while_ok(
|
||
[Result::<(), _>::Err("will not be seen")],
|
||
|_| { Ok("no next") }
|
||
)
|
||
);
|
||
}
|
||
}
|