nstree 1.0.0

construct branched 'namespace strings' for nested subcomponents, often for logging
Documentation
//! Management of namespace components.

use core::{borrow::Borrow, fmt, iter};

/// Get the [`NamespaceComponent`]s in a string, with any `::`-prefix removed
///
/// This does NOT just split the string on `::`. It ensures, for example, that `::a::b` does not
/// produce an empty component at the start (but does allow unrooted namespace paths like `a::b::c`).
/// It will create empty components after a `::`, though.
///
/// An empty string will produce zero components, but any non-empty string should produce at least
/// one component.
///
/// # Example
/// ```rust
/// use nstree::get_components;
///
/// // Note how this strips off the `::`-prefix from the components
/// assert_eq!(get_components("africa::tunisia").collect::<Vec<_>>(), vec!["africa", "tunisia"]);
/// assert_eq!(get_components("::europe::france").collect::<Vec<_>>(), vec!["europe", "france"]);
/// // A more stark illustration of how `::` acts as an initiator (in this case for an empty
/// // namespace component)
/// assert_eq!(get_components("::").collect::<Vec<_>>(), vec![""]);
/// assert_eq!(get_components("").collect::<Vec<_>>(), Vec::<&str>::new());
/// ```
#[inline]
pub fn get_components(v: &str) -> impl Iterator<Item = NamespaceComponent<'_>> {
    let mut component_iterator = v.split("::").peekable();
    // If the first component is empty, then discard it.
    //
    // This will only leave the iterator without components in the case that the initial string was
    // empty. Either:
    // * The string is not empty and contains no `::` - in which case the first element will not be
    //   empty.
    // * The string is not empty and contains `::` - in which case there must be at least one
    //   element after the first in the iterator, and as such, the iterator is not empty.
    if component_iterator.peek() == Some(&"") {
        let _ = component_iterator.next();
    }

    component_iterator.map(|valid_component| {
        // SAFETY: The source of this iterator was split on `"::"`, so it cannot contain any
        // `"::"`.
        unsafe { NamespaceComponent::new_unchecked(valid_component) }
    })
}

/// Error for when exactly 1 [`NamespaceComponent`] - or equivalent string - was expected, but you
/// actually found either no components, or several.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub enum NotSingleComponentError {
    NoComponents,
    MultipleComponents,
}

impl core::error::Error for NotSingleComponentError {}
impl core::fmt::Display for NotSingleComponentError {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MultipleComponents => f.write_str("invalid single-namespace-component string, containing more than one `::`-delimited components"),
            Self::NoComponents => f.write_str("invalid single-namespace-component string: contains no components (probably empty)")
        }
    }
}

/// Single namespace component, consisting of a string that does not contain two or more contiguous
/// `:` characters - i.e. it contains no Namespace Separators.
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct NamespaceComponent<'s>(&'s str);

impl<'s> NamespaceComponent<'s> {
    /// Create a new namespace component.
    ///
    /// # SAFETY
    /// This function must only be called with strings that do not contain the sequence
    /// `"::"`
    #[inline]
    pub const unsafe fn new_unchecked(raw_component: &'s str) -> Self {
        Self(raw_component)
    }

    /// Create a new namespace component from the given raw string - if it is a valid component.
    ///
    /// This function will work with components that have a single `"::"` as a prefix, as well as
    /// strings that do not. If such a single prefix is present, then it will be stripped from the
    /// returned type.
    ///
    /// If more than one component is present, this will return [`Err`].
    ///
    /// ```rust
    /// use nstree::component::{NamespaceComponent, NotSingleComponentError};
    ///
    /// assert_eq!(NamespaceComponent::new("london").unwrap(), "london");
    /// assert_eq!(NamespaceComponent::new(":london").unwrap(), ":london");
    /// assert_eq!(NamespaceComponent::new("::london").unwrap(), "london");
    /// assert_eq!(NamespaceComponent::new(":::london").unwrap(), ":london");
    /// assert_eq!(NamespaceComponent::new("").unwrap(), "");
    /// assert_eq!(
    ///     NamespaceComponent::new("::::london"),
    ///     Err(NotSingleComponentError::MultipleComponents)
    /// );
    /// ```
    #[inline]
    pub fn new(raw_component: &'s str) -> Result<Self, NotSingleComponentError> {
        let raw_component = raw_component.strip_prefix("::").unwrap_or(raw_component);
        if raw_component.contains("::") {
            Err(NotSingleComponentError::MultipleComponents)
        } else {
            // SAFETY: We ensured that the component contains no `"::"`-dividers. If there was one
            // as a prefix, it was removed
            Ok(unsafe { Self::new_unchecked(raw_component) })
        }
    }

    /// Get the component directly as a string.
    #[must_use]
    #[inline]
    pub const fn as_str(&self) -> &'s str {
        self.0
    }

    /// Get the byte-length of the component (as a string)
    #[must_use]
    #[inline]
    pub const fn len(&self) -> usize {
        self.as_str().len()
    }

    /// Get if this component is an empty component
    #[must_use]
    #[inline]
    pub const fn is_empty(&self) -> bool {
        self.len() == 0
    }

    #[cfg(any(feature = "std", test))]
    /// Get the component as an [`std::ffi::OsStr`]
    #[must_use]
    #[inline]
    pub fn as_os_str(&self) -> &std::ffi::OsStr {
        std::ffi::OsStr::new(self.as_str())
    }
}

impl Borrow<str> for NamespaceComponent<'_> {
    #[inline]
    fn borrow(&self) -> &str {
        self.as_str()
    }
}

impl AsRef<str> for NamespaceComponent<'_> {
    #[inline]
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl fmt::Display for NamespaceComponent<'_> {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl<'s> TryFrom<&'s str> for NamespaceComponent<'s> {
    type Error = NotSingleComponentError;

    #[inline]
    fn try_from(value: &'s str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl<'s> From<NamespaceComponent<'s>> for &'s str {
    #[inline]
    fn from(value: NamespaceComponent<'s>) -> Self {
        value.as_str()
    }
}

impl crate::NamespacePath for NamespaceComponent<'_> {
    #[inline(always)]
    fn components(&self) -> impl IntoIterator<Item = NamespaceComponent<'_>> {
        iter::once(*self)
    }

    #[inline(always)]
    fn components_hint(&self) -> crate::path::NamespaceComponentsHint {
        crate::path::NamespaceComponentsHint::single(*self)
    }
}

/// Basically taken from https://doc.rust-lang.org/src/alloc/string.rs.html#2478
/// but for [NamespaceComponent], and it piggybacks upon the str implementations (avoiding
/// borrowing/deref on NamespaceComponent or the RHS).
///
/// Also do not implement "in-reverse" (on stdlib types), as per the
/// [guidelines for `PartialEq`][`PartialEq`].
///
/// this also implements [`PartialOrd`], following the sets of guidelines from there.
macro_rules! impl_eq_ord {
    ($lhs:ty, $rhs:ty) => {
        impl PartialEq<$rhs> for $lhs {
            #[inline]
            fn eq(&self, rhs: &$rhs) -> bool {
                PartialEq::eq(self.as_str(), &rhs[..])
            }

            // We allow this on "forwarding principle" - we're forwarding to another trait impl,
            // so just in case it has some weird performance optimisation or something...
            #[allow(clippy::partialeq_ne_impl)]
            #[inline]
            fn ne(&self, rhs: &$rhs) -> bool {
                PartialEq::ne(self.as_str(), &rhs[..])
            }
        }

        impl PartialOrd<$rhs> for $lhs {
            #[inline]
            fn partial_cmp(&self, rhs: &$rhs) -> Option<core::cmp::Ordering> {
                PartialOrd::partial_cmp(self.as_str(), &rhs[..])
            }

            #[inline]
            fn lt(&self, rhs: &$rhs) -> bool {
                PartialOrd::lt(self.as_str(), &rhs[..])
            }

            #[inline]
            fn le(&self, rhs: &$rhs) -> bool {
                PartialOrd::le(self.as_str(), &rhs[..])
            }

            #[inline]
            fn gt(&self, rhs: &$rhs) -> bool {
                PartialOrd::gt(self.as_str(), &rhs[..])
            }

            #[inline]
            fn ge(&self, rhs: &$rhs) -> bool {
                PartialOrd::ge(self.as_str(), &rhs[..])
            }
        }
    };
}

impl_eq_ord! {NamespaceComponent<'_>, str}
impl_eq_ord! {NamespaceComponent<'_>, &str}
impl_eq_ord! {NamespaceComponent<'_>, &mut str}

#[cfg(any(feature = "alloc", test))]
impl_eq_ord! {NamespaceComponent<'_>, alloc::string::String}
#[cfg(any(feature = "alloc", test))]
impl_eq_ord! {NamespaceComponent<'_>, alloc::borrow::Cow<'_, str>}

#[cfg(any(feature = "std", test))]
/// osstr and osstring implementations, same as above, but only applicable with `std` feature
macro_rules! impl_eq_ord_osstr {
    {$lhs:ty, $rhs:ty $(, $rhs_extract:expr)?} => {
        impl PartialEq<$rhs> for $lhs {
            #[inline]
            fn eq(&self, rhs: &$rhs) -> bool {
                PartialEq::eq(self.as_os_str(), impl_eq_ord_osstr!(@rhs_extract $({closure: $rhs_extract})? {rhs: rhs}))
            }

            // We allow this on "forwarding principle" - we're forwarding to another trait impl,
            // so just in case it has some weird performance optimisation or something...
            #[allow(clippy::partialeq_ne_impl)]
            #[inline]
            fn ne(&self, rhs: &$rhs) -> bool {
                PartialEq::ne(self.as_os_str(), impl_eq_ord_osstr!(@rhs_extract $({closure: $rhs_extract})? {rhs: rhs}))
            }
        }

        impl PartialOrd<$rhs> for $lhs {
            #[inline]
            fn partial_cmp(&self, rhs: &$rhs) -> Option<core::cmp::Ordering> {
                PartialOrd::partial_cmp(self.as_os_str(), impl_eq_ord_osstr!(@rhs_extract $({closure: $rhs_extract})? {rhs: rhs}))
            }

            #[inline]
            fn lt(&self, rhs: &$rhs) -> bool {
                PartialOrd::lt(self.as_os_str(), impl_eq_ord_osstr!(@rhs_extract $({closure: $rhs_extract})? {rhs: rhs}))
            }

            #[inline]
            fn le(&self, rhs: &$rhs) -> bool {
                PartialOrd::le(self.as_os_str(), impl_eq_ord_osstr!(@rhs_extract $({closure: $rhs_extract})? {rhs: rhs}))
            }

            #[inline]
            fn gt(&self, rhs: &$rhs) -> bool {
                PartialOrd::gt(self.as_os_str(), impl_eq_ord_osstr!(@rhs_extract $({closure: $rhs_extract})? {rhs: rhs}))
            }

            #[inline]
            fn ge(&self, rhs: &$rhs) -> bool {
                PartialOrd::ge(self.as_os_str(), impl_eq_ord_osstr!(@rhs_extract $({closure: $rhs_extract})? {rhs: rhs}))
            }
        }
    };
    {@rhs_extract {closure: $closure:expr} {rhs: $rhs_expr:expr}} => {
        ($closure)($rhs_expr)
    };
    {@rhs_extract {rhs: $rhs_expr:expr}} => {
        AsRef::<std::ffi::OsStr>::as_ref($rhs_expr)
    };
}

#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, std::ffi::OsStr}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, std::path::Path}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, &std::ffi::OsStr}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, &std::path::Path}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, &mut std::ffi::OsStr}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, &mut std::path::Path}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, std::ffi::OsString}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, std::path::PathBuf}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, alloc::borrow::Cow<'_, std::ffi::OsStr>}
#[cfg(any(feature = "std", test))]
impl_eq_ord_osstr! {NamespaceComponent<'_>, alloc::borrow::Cow<'_, std::path::Path>, cow_path_as_osstr}

#[cfg(any(feature = "std", test))]
/// Utility function to help type deduction where a `|| {}`-like closure won't work due to lack of
/// explicit lifetime.
#[inline]
fn cow_path_as_osstr<'u>(v: &'u alloc::borrow::Cow<'u, std::path::Path>) -> &'u std::ffi::OsStr {
    v.as_os_str()
}

#[cfg(test)]
mod test_get_components {
    use crate::component::NamespaceComponent;

    use super::get_components;

    #[test]
    pub fn get_components_test() {
        fn get_components_collect(v: &str) -> Vec<NamespaceComponent<'_>> {
            get_components(v).collect()
        }

        assert_eq!(get_components_collect("::a::b"), vec!["a", "b"]);
        assert_eq!(get_components_collect("a::b::c::"), vec!["a", "b", "c", ""]);
        assert_eq!(get_components_collect("::"), vec![""]);
        assert_eq!(get_components_collect("africa::tunisia"), vec![
            "africa", "tunisia"
        ]);
        assert_eq!(get_components_collect("::africa::tunisia"), vec![
            "africa", "tunisia"
        ]);
    }
}

// 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