std_io_iterators 1.1.0

An iterator for `STDIN` and a wrapper for `STDOUT`. Allows easy piping, and graceful closing of application if pipe breaks
Documentation
//! Add [`pipe_out()`][PipeOut::pipe_out] to any [`Iterator`] of [`std::fmt::Display`]

use std::{
    io::Write,
    marker::Sized,
    sync::atomic::{AtomicBool, Ordering::Relaxed},
};

use crate::pipe_out_recovered_iterator::PipeOutRecoveredIterator;

/// Allows [`WriteLineResult::PreviousError`] without custom [`std::io::Error`]
#[must_use = "this may be an error variant (`BrokenPipe`, `OtherError`, \
              `PreviousError`), which should be handled"]
pub enum WriteLineResult {
    /// [`std::io::ErrorKind::BrokenPipe`] state
    BrokenPipe(std::io::Error),

    /// Other error state
    OtherError(std::io::Error),

    /// Other error occurred in last [`PipeOut::pipe_out`]
    PreviousError,

    /// `STDOUT` is already locked
    StdOutLock,

    /// No error occurred
    Ok,
}

impl WriteLineResult {
    /// Compare variants of [`WriteLineResult`], ignoring contained [`std::io::Error`]
    ///
    /// Per <https://github.com/rust-lang/rust/pull/34192> it is illogical to
    /// compare [`std::io::Error`].  For testing reasons, this function compares
    /// the variants of two [`WriteLineResult`] so some degree of [`PartialEq`]
    /// can be emulated
    #[must_use]
    pub fn same_variant_as(&self, variant: &Self) -> bool {
        self.variant_to_usize() == variant.variant_to_usize()
    }

    fn variant_to_usize(&self) -> usize {
        match self {
            Self::BrokenPipe(_) => 1,
            Self::OtherError(_) => 2,
            Self::PreviousError => 3,
            Self::StdOutLock => 4,
            Self::Ok => 5,
        }
    }
}

/// Convert from [`std::io::Error`], catch [`std::io::ErrorKind::BrokenPipe`]
impl From<std::io::Error> for WriteLineResult {
    fn from(e: std::io::Error) -> Self {
        if e.kind() == std::io::ErrorKind::BrokenPipe {
            Self::BrokenPipe(e)
        } else {
            Self::OtherError(e)
        }
    }
}

/// Convert [`WriteLineResult`] to [`std::io::Error`]
///
/// # Panic
///
/// Will panic of non [`std::io::Error`] state:
/// - [`WriteLineResult::Ok`]
/// - [`WriteLineResult::PreviousError`]
impl From<WriteLineResult> for std::io::Error {
    fn from(e: WriteLineResult) -> Self {
        match e {
            WriteLineResult::OtherError(e) | WriteLineResult::BrokenPipe(e) => {
                e
            }
            _ => panic!(
                "1656622268 - Can't unwrap io::Error from non-std::io::Error \
                 variant"
            ),
        }
    }
}

impl From<WriteLineResult> for Result<(), std::io::Error> {
    fn from(value: WriteLineResult) -> Self {
        match value {
            WriteLineResult::Ok => Ok(()),
            WriteLineResult::OtherError(e) | WriteLineResult::BrokenPipe(e) => {
                Err(e)
            }
            WriteLineResult::PreviousError => Err(std::io::Error::other(
                "1669225745 - Other error occurred in last [Self::pipe_out]",
            )),
            WriteLineResult::StdOutLock => Err(std::io::Error::other(
                "1669225823 - STDOUT is already locked",
            )),
        }
    }
}

/// Add [`pipe_out()`][PipeOut::pipe_out] to any [`Iterator`] of [`std::fmt::Display`]
pub trait PipeOut<D: std::fmt::Display>: Iterator<Item = D> {
    /// Pipe to `STDOUT` the elements of the parent iterator, return
    /// [`PipeOutRecoveredIterator`]
    ///
    /// # Warning
    ///
    /// At this time, there is no reliable way to ensure when or if the pipe was
    /// broken.  Due to some unknown reason (probably related to buffering) the
    /// output is allowed to continue past the broken pipe.
    ///
    /// For the below test:
    ///
    /// ```sh
    /// cargo run --example="example_handles_broken_pipe" | cargo run --example="break_pipe"
    /// ```
    ///
    /// The results should ALWAYS be:
    ///
    /// ```text
    /// Launched
    /// Test1
    /// Test2
    /// Test3 - Recovered
    /// Recovery Done
    /// ```
    ///
    /// However, most times the test returns other values like:
    ///
    /// ```text
    /// Launched
    /// Finished
    /// Test1
    /// Test2
    /// ```
    ///
    /// or
    ///
    /// ```text
    /// Launched
    /// Test1
    /// Finished
    /// Test2
    /// ```
    ///
    /// There appears to be no way to fix this inconsistent behavior.  If you
    /// know of a fix, please submit a pull request or e-mail
    /// <emeraldinspirations@gmail.com>
    ///
    /// # Errors
    ///
    /// If something fails to pipe, a [`PipeOutRecoveredIterator`] is returned
    ///
    /// # Example
    ///
    /// ```no_run
    #[doc = include_str!("examples/pipe_out.rs")]
    /// ```
    fn pipe_out(mut self) -> Result<(), PipeOutRecoveredIterator<D, Self>>
    where
        Self: Sized,
    {
        if check_error_state() || set_lock_state().is_err() {
            return Err(PipeOutRecoveredIterator {
                iter: self,
                recovered_datum: None,
                result: WriteLineResult::StdOutLock,
            });
        }

        let stdout = std::io::stdout();
        let mut lock = stdout.lock();

        for datum in self.by_ref() {
            if check_error_state() {
                release_lock_state();
                return Err(PipeOutRecoveredIterator {
                    iter: self,
                    recovered_datum: Some(datum),
                    result: WriteLineResult::PreviousError,
                });
            }

            if let Err(error) = lock.write_all(format!("{datum}\n").as_bytes())
            {
                set_error_state();
                release_lock_state();
                return Err(PipeOutRecoveredIterator {
                    iter: self,
                    recovered_datum: Some(datum),
                    result: error.into(),
                });
            }
        }

        release_lock_state();
        Ok(())
    }
}

impl<D: std::fmt::Display, I: Iterator<Item = D>> PipeOut<D> for I {}

static ERROR: AtomicBool = AtomicBool::new(false);

fn check_error_state() -> bool {
    ERROR.load(Relaxed)
}

fn set_error_state() {
    ERROR.store(true, Relaxed);
}

static LOCKED: AtomicBool = AtomicBool::new(false);

fn set_lock_state() -> Result<bool, bool> {
    LOCKED.compare_exchange(false, true, Relaxed, Relaxed)
}

fn release_lock_state() {
    LOCKED.store(false, Relaxed);
}

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

    fn check_lock_state() -> bool {
        LOCKED.load(Relaxed)
    }

    #[test]
    fn test1656794821() {
        assert!(!check_error_state());
        // set_lock_state(); // Make sure correct variable
        assert!(!check_error_state());
        set_error_state();
        assert!(check_error_state());
        assert!(check_error_state());
    }
    // Commented code for single-test runs only as state will maintain between
    // tests

    #[test]
    fn test1656795696() {
        assert!(!check_lock_state());
        // set_error_state(); // Make sure correct variable
        assert!(!check_lock_state());
        set_lock_state().unwrap();
        assert!(check_lock_state());
        assert!(check_lock_state());
        release_lock_state();
        assert!(!check_lock_state());
        assert!(!check_lock_state());
    }
    // Commented code for single-test runs only as state will maintain between
    // tests
}