derail-report 0.1.0

Tools for reporting `derail::Error`s.
Documentation
//! Multiline report implementation.

use core::{fmt, marker::PhantomData, ops::ControlFlow};

use derail::{Error, VisitContext, Visitor, VisitorExt as _};

use crate::{BufferFactory, buffer::Buffer, sealed::Sealed};

mod unicode {
    //! Unicode characters used in the output.

    #![allow(clippy::missing_docs_in_private_items)]

    pub(super) const BLACK_RIGHT_POINTING_POINTER: char = '\u{25BA}';
    pub(super) const BLACK_SQUARE: char = '\u{25A0}';
    pub(super) const LIGHT_ARC_UP_AND_RIGHT: char = '\u{2570}';
    pub(super) const LIGHT_HORIZONTAL: char = '\u{2500}';
    pub(super) const LIGHT_VERTICAL: char = '\u{2502}';
    pub(super) const LIGHT_VERTICAL_AND_RIGHT: char = '\u{251C}';
}

/// An element in the buffer.
#[derive(Clone, Copy, Default)]
struct Element {
    /// Whether this is the last sibling at this depth.
    is_last_sibling: bool,
}

/// [`Display`]s [`Error`]s on multiple lines. Suitable for user-facing output.
///
/// If the length of any chains of nodes from a root to a leaf is greater than
/// [`StackFactory`]'s `LEN`, the output will not look as good, but information
/// will not be lost.
///
/// [`Display`]: fmt::Display
/// [`StackFactory`]: crate::StackFactory
#[cfg_attr(
    feature = "alloc",
    doc = "\n\n[`HeapFactory`](crate::HeapFactory) does not have this \
           limitation."
)]
pub fn display<'a, I, E, B>(errors: I) -> impl fmt::Display
where
    I: IntoIterator<Item = &'a E> + Clone,
    E: Error + ?Sized + 'a,
    B: BufferFactory,
{
    DisplayImpl(errors, PhantomData::<B>)
}

/// [`Display`] implementation.
///
/// [`Display`]: fmt::Display
struct DisplayImpl<I, B>(I, PhantomData<B>);

impl<'a, I, E, B> fmt::Display for DisplayImpl<I, B>
where
    I: IntoIterator<Item = &'a E> + Clone,
    E: Error + ?Sized + 'a,
    B: BufferFactory,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut visitor = VisitorImpl {
            writer: f,
            result: Ok(()),
            first: true,
            buffer: B::new(Sealed),
            details: PhantomData::<E::Details>,
        };

        let _: ControlFlow<(), ()> = visitor.visit_many(self.0.clone());

        visitor.result
    }
}

/// [`Visitor`] implementation.
struct VisitorImpl<W, B, D> {
    /// The writer to write to.
    writer: W,

    /// The write result.
    result: fmt::Result,

    /// Whether this is the first [`Visitor::visit`] call.
    first: bool,

    /// The buffer.
    buffer: B,

    /// The [`Error::Details`] type.
    details: PhantomData<D>,
}

impl<W, B, D> Visitor for VisitorImpl<W, B, D>
where
    W: fmt::Write,
    B: Buffer<Element>,
{
    type Details = D;

    fn visit(
        &mut self,
        visitee: &dyn Error<Details = Self::Details>,
        ctx: VisitContext<'_, Self::Details>,
    ) -> ControlFlow<()> {
        if self.first {
            self.first = false;
        } else {
            attempt!(writeln!(self.writer), self.result);
        }

        if ctx.next_sibling().is_none() {
            if let Some(x) = self.buffer.last_mut(Sealed) {
                x.is_last_sibling = true;
            }
        }

        for depth in 0..=self.buffer.depth(Sealed) {
            let Some(x) = self.buffer.get_mut(depth, Sealed).copied() else {
                attempt!(
                    write!(
                        self.writer,
                        "{s} LEN exceeded by {c} {h}{p} ",
                        s = unicode::BLACK_SQUARE,
                        c = self
                            .buffer
                            .depth(Sealed)
                            .saturating_sub(self.buffer.max_depth(Sealed))
                            .saturating_add(1),
                        h = unicode::LIGHT_HORIZONTAL,
                        p = unicode::BLACK_RIGHT_POINTING_POINTER,
                    ),
                    self.result
                );
                break;
            };

            if depth == self.buffer.depth(Sealed) {
                attempt!(
                    write!(
                        self.writer,
                        "{v}{h}{p} ",
                        v = if x.is_last_sibling {
                            unicode::LIGHT_ARC_UP_AND_RIGHT
                        } else {
                            unicode::LIGHT_VERTICAL_AND_RIGHT
                        },
                        h = unicode::LIGHT_HORIZONTAL,
                        p = unicode::BLACK_RIGHT_POINTING_POINTER,
                    ),
                    self.result,
                );
            } else {
                attempt!(
                    write!(
                        self.writer,
                        "{v}   ",
                        v = if x.is_last_sibling {
                            ' '
                        } else {
                            unicode::LIGHT_VERTICAL
                        }
                    ),
                    self.result,
                );
            }
        }

        attempt!(write!(self.writer, "{visitee}"), self.result);

        ControlFlow::Continue(())
    }

    fn push(&mut self) -> ControlFlow<()> {
        self.buffer.push(Sealed);

        ControlFlow::Continue(())
    }

    fn pop(&mut self) -> ControlFlow<()> {
        if let Some(x) = self.buffer.last_mut(Sealed) {
            x.is_last_sibling = false;
        }

        self.buffer.pop(Sealed);

        ControlFlow::Continue(())
    }
}