nstree 1.0.0

construct branched 'namespace strings' for nested subcomponents, often for logging
Documentation
//! Module that manages the actual rendering of [`NamespacePath`] and
//! [`RawNamespacePath`] structures.

use core::fmt;

use super::{NamespacePath, RawNamespacePath};

/// Wrapper implementing [`fmt::Display`] that - if [`MaybeWithPrefix::render_prefix`] is `true` -
/// renders the component with the `"::"`-prefix added. If not, it forwards directly to the
/// [`fmt::Display::fmt`] implementation of [`Self::component`]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub struct MaybeWithPrefix<Component> {
    /// Control whether the [`fmt::Display`] impl from [`Self::component`] gets `"::"` put before
    /// it.
    pub render_prefix: bool,
    /// Component that needs to implement [`fmt::Display`] for it to be conditionally prefixed or
    /// not prefixed.
    pub component: Component,
}

impl<Component> MaybeWithPrefix<Component> {
    /// See [`MaybeWithPrefix`] for what this function creates.
    #[inline]
    pub const fn new(render_prefix: bool, component: Component) -> Self {
        Self {
            render_prefix,
            component,
        }
    }

    /// Wrap a single component with [`MaybeWithPrefix`] in such a way that it would act in the [`RenderStyle`]
    /// appropriate for a single-element namespace path of the given style.
    #[inline]
    const fn wrap_single_element(render_style: RenderStyle, component: Component) -> Self {
        match render_style {
            RenderStyle::WithFirstSeparator => Self::new(true, component),
            RenderStyle::NoFirstSeparator => Self::new(false, component),
        }
    }
}

impl<Component: fmt::Display> fmt::Display for MaybeWithPrefix<Component> {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.render_prefix {
            f.write_str("::")?;
        }
        self.component.fmt(f)
    }
}

/// Implementation of [`Iterator`] that acts to create [`MaybeWithPrefix`] structures from the
/// inner iterator following a specified [`RenderStyle`].
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Default)]
pub struct DisplayPrefixIter<Iter> {
    /// Format of all "remaining" elements. As [`RenderStyle::WithFirstSeparator`] is composable
    /// (i.e. it can be attached to the end of any other valid non-empty sequence - or even an
    /// ambiguous empty sequence without increasing ambiguity) - it is used "all of the time".
    ///
    /// Except in the specific case when it's set to [`RenderStyle::NoFirstSeparator`], in which
    /// case exactly one element will be produced without a render prefix, and then it will be
    /// switched to produce all new elements with [`RenderStyle::WithFirstSeparator`].
    remaining_element_format: RenderStyle,
    /// Inner iterator of elements to be wrapped in [`MaybeWithPrefix`]
    inner: Iter,
}

impl<Iter> DisplayPrefixIter<Iter> {
    /// See [`DisplayPrefixIter`] for info on what this creates.
    #[inline]
    pub const fn new(components: Iter, render_style: RenderStyle) -> Self {
        Self {
            remaining_element_format: render_style,
            inner: components,
        }
    }
}

impl<Iter: Iterator> Iterator for DisplayPrefixIter<Iter> {
    type Item = MaybeWithPrefix<Iter::Item>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        let next_v = self
            .inner
            .next()
            .map(|i| MaybeWithPrefix::wrap_single_element(self.remaining_element_format, i));
        self.remaining_element_format = RenderStyle::WithFirstSeparator;
        next_v
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.inner.size_hint()
    }
}

/// Whether to render some sequence of [`crate::NamespaceComponent`] with or without the "first"
/// `"::"` separator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum RenderStyle {
    /// Render a sequence of components with the first separator. For instance, the component
    /// sequence `["europe", "france", "paris"]` would be rendered as `"::europe::france::paris"`
    ///
    /// This mode may look very slightly less nice (depending on personal preference) but there are
    /// no cases in which it is ambiguous - hence it is the default.
    #[default]
    WithFirstSeparator,
    /// Render a sequence of components without the first separator. For instance, the component
    /// sequence `["europe", "france", "paris"]` would be rendered as `"europe::france::paris"`
    ///
    /// This mode often looks nicer, but it is ambiguous between an empty component sequence and
    /// one with a single, empty first component (though if there are any further components it is
    /// not ambiguous).
    NoFirstSeparator,
}

impl RenderStyle {
    /// Get the number of separators when rendering in this style, for a [`crate::NamespacePath`]
    /// or [`crate::RawNamespacePath`] of the given number of components.
    #[must_use]
    #[inline]
    pub const fn separator_count(&self, components: usize) -> usize {
        match self {
            RenderStyle::WithFirstSeparator => components,
            RenderStyle::NoFirstSeparator => components.saturating_sub(1),
        }
    }
}

#[cfg(test)]
mod test_render_prefix_components {
    use core::fmt;

    use crate::path::render::DisplayPrefixIter;

    #[test]
    fn prefix_emit_components_test() {
        use super::RenderStyle::{self, NoFirstSeparator, WithFirstSeparator};

        /// Quick struct for rendering an iterator of things-implementing-display.
        struct RenderIterator<I>(pub I);

        impl<I: Iterator<Item: fmt::Display> + Clone> fmt::Display for RenderIterator<I> {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                self.0.clone().try_fold((), |_, v| v.fmt(f))
            }
        }

        fn render_in_style(
            components: impl IntoIterator<Item: fmt::Display, IntoIter: Clone>,
            style: RenderStyle,
        ) -> String {
            RenderIterator(DisplayPrefixIter::new(components.into_iter(), style)).to_string()
        }

        assert_eq!(render_in_style([""], WithFirstSeparator), "::");
        assert_eq!(render_in_style([""], NoFirstSeparator), "");
        assert_eq!(render_in_style(["a"], WithFirstSeparator), "::a");
        assert_eq!(render_in_style(["a"], NoFirstSeparator), "a");
        assert_eq!(render_in_style(["a", "b"], WithFirstSeparator), "::a::b");
        assert_eq!(render_in_style(["a", "b"], NoFirstSeparator), "a::b");
    }
}

/// [`NamespacePath`] wrapper that implements [`fmt::Display`] using the internal [`RenderStyle`].
///
/// See: [`NamespacePath::render`]
///
/// Also provides other utility functionality for rendering, such as creating strings in such a way
/// as to pre-allocate nicely using the [`crate::path::NamespaceComponentsHint`] of the namespace
/// path.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub struct NamespacePathRender<NSPath>(pub NSPath, pub RenderStyle);

impl<NSPath> NamespacePathRender<NSPath> {
    #[inline]
    #[must_use]
    pub const fn new(namespace_path: NSPath, render_style: RenderStyle) -> Self {
        Self(namespace_path, render_style)
    }

    /// Configure for rendering the given namespace path using [`RenderStyle::WithFirstSeparator`]
    #[inline]
    #[must_use]
    pub const fn with_first_separator(namespace_path: NSPath) -> Self {
        Self::new(namespace_path, RenderStyle::WithFirstSeparator)
    }

    /// Configure for rendering the given namespace path using [`RenderStyle::NoFirstSeparator`]
    #[inline]
    #[must_use]
    pub const fn without_first_separator(namespace_path: NSPath) -> Self {
        Self::new(namespace_path, RenderStyle::NoFirstSeparator)
    }

    /// Obtain a likely-accurate estimate for the amount of extra bytes that would be produced by
    /// rendering in the style specified.
    ///
    /// For most cases this will prevent any extra allocation as long as this much
    /// memory is available.
    ///
    /// If this is likely to be a frequent usage occurance, consider using
    /// [`NamespacePath::by_ref_cache`] or [`NamespacePath::cache_components_hint`] on the
    /// namespace path first.
    #[inline]
    #[must_use]
    pub fn estimated_string_bytes(&self) -> usize
    where
        NSPath: NamespacePath,
    {
        self.0.components_hint().estimated_string_bytes(self.1)
    }

    /// Render this structure into a [`String`]. Unlike normal [`ToString::to_string`] this can do
    /// some nice pre-allocation
    #[allow(
        clippy::inherent_to_string_shadow_display,
        reason = "this just calls out to the fmt::Display impl directly but adds better pre-allocation"
    )]
    #[must_use]
    #[cfg(any(feature = "alloc", test))]
    pub fn to_string(&self) -> alloc::string::String
    where
        NSPath: NamespacePath,
    {
        use fmt::Write;
        let estimated_bytes = self.estimated_string_bytes();
        let mut v = alloc::string::String::with_capacity(estimated_bytes);
        write!(v, "{self}").expect("in-memory writing, should not have error");
        v
    }
}

impl<NSPath: NamespacePath> fmt::Display for NamespacePathRender<NSPath> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        DisplayPrefixIter::new(self.0.components().into_iter(), self.1)
            .try_fold((), |_, v| v.fmt(f))
    }
}

impl<Inner: NamespacePath> NamespacePath for NamespacePathRender<Inner> {
    #[inline]
    fn components(&self) -> impl IntoIterator<Item = crate::NamespaceComponent<'_>> {
        self.0.components()
    }

    #[inline]
    fn components_hint(&self) -> super::NamespaceComponentsHint {
        self.0.components_hint()
    }
}

impl<Inner: RawNamespacePath> RawNamespacePath for NamespacePathRender<Inner> {
    #[inline]
    fn raw_components(&self) -> impl IntoIterator<Item = impl fmt::Display> {
        self.0.raw_components()
    }
}

/// [`RawNamespacePath`] wrapper that implements [`fmt::Display`] using the internal [`RenderStyle`].
///
/// See: [`RawNamespacePath::raw_render`]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub struct RawNamespacePathRender<RawNSPath>(pub RawNSPath, pub RenderStyle);

impl<RawNSPath> RawNamespacePathRender<RawNSPath> {
    #[inline]
    #[must_use]
    pub const fn new(raw_namespace_path: RawNSPath, render_style: RenderStyle) -> Self {
        Self(raw_namespace_path, render_style)
    }

    /// Configure for rendering the given raw namespace path using [`RenderStyle::WithFirstSeparator`]
    #[inline]
    #[must_use]
    pub const fn with_first_separator(raw_namespace_path: RawNSPath) -> Self {
        Self::new(raw_namespace_path, RenderStyle::WithFirstSeparator)
    }

    /// Configure for rendering the given raw namespace path using [`RenderStyle::NoFirstSeparator`]
    #[inline]
    #[must_use]
    pub const fn without_first_separator(raw_namespace_path: RawNSPath) -> Self {
        Self::new(raw_namespace_path, RenderStyle::NoFirstSeparator)
    }
}

impl<RawNSPath: RawNamespacePath> fmt::Display for RawNamespacePathRender<RawNSPath> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        DisplayPrefixIter::new(self.0.raw_components().into_iter(), self.1)
            .try_fold((), |_, v| v.fmt(f))
    }
}

impl<Inner: NamespacePath> NamespacePath for RawNamespacePathRender<Inner> {
    #[inline]
    fn components(&self) -> impl IntoIterator<Item = crate::NamespaceComponent<'_>> {
        self.0.components()
    }

    #[inline]
    fn components_hint(&self) -> super::NamespaceComponentsHint {
        self.0.components_hint()
    }
}

impl<Inner: RawNamespacePath> RawNamespacePath for RawNamespacePathRender<Inner> {
    #[inline]
    fn raw_components(&self) -> impl IntoIterator<Item = impl fmt::Display> {
        self.0.raw_components()
    }
}

// nstree - nested namespace string-generating abstraction library
// Copyright (C) 2025  Matti <infomorphic-matti at protonmail dot com>
//
// 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 <https://www.gnu.org/licenses/>.
// ------
// SPDX-License-Identifier: GPL-3.0-or-later