osom_lib_strings 0.1.5

ABI-stable string types and helpers for osom_lib.
Documentation
use core::{
    borrow::Borrow, fmt::Display, hash::Hash, sync::atomic::{Ordering, fence}
};

use osom_lib_alloc::traits::Allocator;
use osom_lib_primitives::length::Length;
use osom_lib_reprc::macros::reprc;
use osom_lib_try_clone::TryClone;

use crate::immutable::{
    ImmutableStringError, MAX_REFERENCES, MaxReferencesExceededError, WeakImmutableString,
    internal_string::InternalString,
};

/// This struct is a smart pointer around a string. Internally keeps reference counters
/// for both strong and weak references. Cloning the struct is therefore cheap.
///
/// NOTE: the immutable string internally keeps one byte more than the specified data. This last
/// byte is always set to `\0` and thus the string is suitable to use as a C-style string,
/// by calling [`ImmutableString::as_c_str()`][ImmutableString::as_c_str] method.
#[reprc]
#[repr(transparent)]
#[derive(Debug)]
#[must_use]
pub struct ImmutableString<TAllocator: Allocator> {
    internal: InternalString<TAllocator>,
}

impl<TAllocator: Allocator> ImmutableString<TAllocator> {
    #[inline(always)]
    pub(crate) fn from_internal(internal: InternalString<TAllocator>) -> Self {
        Self { internal }
    }

    /// Creates a new, empty [`ImmutableString`].
    ///
    /// This function allocates under the hood, since all immutable strings are backed by smart pointer.
    ///
    /// # Errors
    ///
    /// For details see [`ImmutableStringError`].
    #[inline(always)]
    pub fn empty() -> Result<Self, ImmutableStringError> {
        Self::empty_with_allocator(TAllocator::default())
    }

    /// Creates a new [`ImmutableString`] out of passed string slice.
    ///
    /// This function allocates under the hood, since all immutable strings are backed by smart pointer.
    /// It also copies the input into the output's buffer.
    ///
    /// # Errors
    ///
    /// For details see [`ImmutableStringError`].
    #[inline(always)]
    pub fn from_str_slice(text: &str) -> Result<Self, ImmutableStringError> {
        Self::from_str_slice_and_allocator(text, TAllocator::default())
    }

    /// Creates a new [`ImmutableString`] out of passed string slice and allocator.
    ///
    /// This function allocates under the hood, since all immutable strings are backed by smart pointer.
    /// It also copies the input into the output's buffer.
    ///
    /// # Errors
    ///
    /// For details see [`ImmutableStringError`].
    #[inline]
    pub fn from_str_slice_and_allocator(text: &str, allocator: TAllocator) -> Result<Self, ImmutableStringError> {
        let text_len = Length::try_from_usize(text.len())?;
        let mut builder = super::ImmutableStringBuilder::with_capacity_and_allocator(text_len, allocator)?;
        builder.try_push(text)?;
        let result = builder.build()?;
        Ok(result)
    }

    /// Creates a new, empty [`ImmutableString`] with a given allocator.
    ///
    /// This function allocates under the hood, since all immutable strings are backed by smart pointer.
    ///
    /// # Errors
    ///
    /// For details see [`ImmutableStringError`].
    #[inline(always)]
    pub fn empty_with_allocator(allocator: TAllocator) -> Result<Self, ImmutableStringError> {
        Self::from_str_slice_and_allocator("", allocator)
    }

    #[inline(always)]
    #[must_use]
    pub fn strong_count(&self) -> u32 {
        self.internal.strong().load(Ordering::Relaxed)
    }

    #[inline(always)]
    #[must_use]
    pub fn weak_count(&self) -> u32 {
        self.internal.weak().load(Ordering::Relaxed)
    }

    /// Returns the underlying string.
    #[inline]
    #[must_use]
    pub const fn as_str(&self) -> &str {
        unsafe {
            let slice = core::slice::from_raw_parts(self.internal.data_start(), self.internal.length().as_usize() - 1);
            core::str::from_utf8_unchecked(slice)
        }
    }

    /// Returns the underlying string as C-string.
    ///
    /// Meaning the string has an additional 0 at the end of the buffer. In particular,
    /// the C-string returned by this method has length +1 compared to [`ImmutableString::as_str`]
    /// call.
    #[inline]
    #[must_use]
    pub const fn as_c_str(&self) -> &str {
        unsafe {
            let slice = core::slice::from_raw_parts(self.internal.data_start(), self.internal.length().as_usize());
            core::str::from_utf8_unchecked(slice)
        }
    }

    /// The length of the string.
    #[inline]
    pub const fn length(&self) -> Length {
        unsafe { Length::new_unchecked(self.internal.length().as_u32() - 1) }
    }

    /// Creates a new [`WeakImmutableString`] out of current.
    ///
    /// # Errors
    ///
    /// Returns [`MaxReferencesExceededError`] if the weak reference count is too high.
    /// Cannot exceed [`MAX_REFERENCES`].
    pub fn downgrade(&self) -> Result<WeakImmutableString<TAllocator>, MaxReferencesExceededError> {
        let internal_clone = self.internal.clone();
        let prev_value = internal_clone.weak().fetch_add(1, Ordering::Relaxed);
        if prev_value >= MAX_REFERENCES {
            internal_clone.weak().fetch_sub(1, Ordering::Relaxed);
            return Err(MaxReferencesExceededError);
        }

        Ok(WeakImmutableString::from_internal(internal_clone))
    }

    /// Abandons current [`ImmutableString`].
    ///
    /// This function returns None if the underlying strong reference counter
    /// is still positive. Otherwise it returns the final [`WeakImmutableString`]
    /// reference.
    #[inline]
    #[must_use]
    pub fn abandon(mut self) -> Option<WeakImmutableString<TAllocator>> {
        let result = self.internal_abandon();
        core::mem::forget(self);
        result
    }

    fn internal_abandon(&mut self) -> Option<WeakImmutableString<TAllocator>> {
        let internal = unsafe { core::ptr::read(&raw const self.internal) };
        let prev = internal.strong().fetch_sub(1, Ordering::Release);
        if prev > 1 {
            return None;
        }

        // Synchronize with all prior Release decrements before deallocating.
        fence(Ordering::Acquire);
        Some(WeakImmutableString::from_internal(internal))
    }
}

impl<TAllocator: Allocator> Drop for ImmutableString<TAllocator> {
    fn drop(&mut self) {
        let _ = self.internal_abandon();
    }
}

impl<TAllocator: Allocator> TryClone for ImmutableString<TAllocator> {
    type Error = MaxReferencesExceededError;

    fn try_clone(&self) -> Result<Self, Self::Error> {
        let internal_clone = self.internal.clone();
        let prev_value = internal_clone.strong().fetch_add(1, Ordering::Relaxed);
        if prev_value >= MAX_REFERENCES {
            internal_clone.strong().fetch_sub(1, Ordering::Relaxed);
            return Err(MaxReferencesExceededError);
        }
        Ok(Self {
            internal: internal_clone,
        })
    }
}

impl<TAllocator: Allocator> Clone for ImmutableString<TAllocator> {
    fn clone(&self) -> Self {
        self.try_clone()
            .expect("ImmutableString strong reference count is too high.")
    }
}

impl<TAllocator: Allocator> AsRef<str> for ImmutableString<TAllocator> {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl<TAllocator: Allocator> Borrow<str> for ImmutableString<TAllocator> {
    fn borrow(&self) -> &str {
        self.as_str()
    }
}

impl<TAllocator: Allocator> Hash for ImmutableString<TAllocator> {
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        self.as_str().hash(state);
    }
}

impl<TAllocator: Allocator, TRight: AsRef<str>> PartialEq<TRight> for ImmutableString<TAllocator> {
    fn eq(&self, other: &TRight) -> bool {
        self.as_str() == other.as_ref()
    }
}

impl<TAllocator: Allocator> Eq for ImmutableString<TAllocator> {}

impl<TAllocator: Allocator> Display for ImmutableString<TAllocator> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}