inline_flexstr 0.1.9

A simple to use, copy/clone-efficient inline string type for Rust
Documentation
use alloc::{borrow::Cow, fmt, string::String};
use core::{
    error::Error,
    str::{FromStr, Utf8Error},
};
#[cfg(feature = "std")]
use std::{ffi::OsStr, path::Path};

use crate::inline::{InlineFlexStr, TooLongForInlining, inline_partial_eq_impl};

use flexstr_support::StringToFromBytes;

/// Inline `str` type
pub type InlineStr = InlineFlexStr<str>;

// *** TooLongOrUtf8Error ***

/// Error type returned when a string is too long for inline storage or has an invalid UTF-8 sequence.
#[derive(Debug)]
pub enum TooLongOrUtf8Error {
    /// The string is too long for inline storage
    TooLong(TooLongForInlining),
    /// The string has an invalid UTF-8 sequence
    Utf8Error(Utf8Error),
}

impl fmt::Display for TooLongOrUtf8Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TooLongOrUtf8Error::TooLong(e) => e.fmt(f),
            TooLongOrUtf8Error::Utf8Error(e) => e.fmt(f),
        }
    }
}

impl Error for TooLongOrUtf8Error {}

// *** TryFrom for InlineFlexStr ***

// NOTE: Cannot be implemented generically because of impl<T, U> TryFrom<U> for T where U: Into<T>
impl<'s> TryFrom<&'s str> for InlineFlexStr<str> {
    type Error = TooLongForInlining;

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

impl<'s> TryFrom<&'s [u8]> for InlineFlexStr<str> {
    type Error = TooLongOrUtf8Error;

    #[inline]
    fn try_from(s: &'s [u8]) -> Result<Self, Self::Error> {
        match str::from_utf8(s) {
            Ok(s) => InlineFlexStr::try_from_type(s).map_err(TooLongOrUtf8Error::TooLong),
            Err(e) => Err(TooLongOrUtf8Error::Utf8Error(e)),
        }
    }
}

#[cfg(feature = "std")]
impl<'s> TryFrom<&'s OsStr> for InlineFlexStr<str> {
    type Error = TooLongOrUtf8Error;

    #[inline]
    fn try_from(s: &'s OsStr) -> Result<Self, Self::Error> {
        match s.try_into() {
            Ok(s) => InlineFlexStr::try_from_type(s).map_err(TooLongOrUtf8Error::TooLong),
            Err(e) => Err(TooLongOrUtf8Error::Utf8Error(e)),
        }
    }
}

#[cfg(feature = "std")]
impl<'s> TryFrom<&'s Path> for InlineFlexStr<str> {
    type Error = TooLongOrUtf8Error;

    #[inline]
    fn try_from(s: &'s Path) -> Result<Self, Self::Error> {
        match s.as_os_str().try_into() {
            Ok(s) => InlineFlexStr::try_from_type(s).map_err(TooLongOrUtf8Error::TooLong),
            Err(e) => Err(TooLongOrUtf8Error::Utf8Error(e)),
        }
    }
}

// *** PartialEq ***

inline_partial_eq_impl!(str, str);
inline_partial_eq_impl!(&str, str);
inline_partial_eq_impl!(String, str);
inline_partial_eq_impl!(Cow<'_, str>, str);

// *** AsRef ***

impl<S: ?Sized + StringToFromBytes> AsRef<str> for InlineFlexStr<S>
where
    S: AsRef<str>,
{
    fn as_ref(&self) -> &str {
        self.as_ref_type().as_ref()
    }
}

// *** FromStr ***

impl FromStr for InlineFlexStr<str> {
    type Err = TooLongForInlining;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        InlineFlexStr::try_from_type(s)
    }
}

// *** Prost ***

#[cfg(feature = "prost")]
#[allow(deprecated)] // DecodeError::new is deprecated in prost 0.14 with no public replacement yet
impl prost::Message for InlineFlexStr<str> {
    fn encode_raw(&self, buf: &mut impl prost::bytes::BufMut)
    where
        Self: Sized,
    {
        buf.put_slice(self.as_ref_type().as_bytes());
    }

    fn merge_field(
        &mut self,
        tag: u32,
        wire_type: prost::encoding::WireType,
        buf: &mut impl prost::bytes::Buf,
        ctx: prost::encoding::DecodeContext,
    ) -> Result<(), prost::DecodeError>
    where
        Self: Sized,
    {
        prost::encoding::skip_field(wire_type, tag, buf, ctx)
    }

    fn encoded_len(&self) -> usize {
        self.as_ref_type().len()
    }

    fn clear(&mut self) {
        *self = Default::default();
    }

    fn merge(&mut self, mut buf: impl prost::bytes::Buf) -> Result<(), prost::DecodeError>
    where
        Self: Sized,
    {
        let bytes = buf.copy_to_bytes(buf.remaining());
        let s = core::str::from_utf8(&bytes)
            .map_err(|_| prost::DecodeError::new("invalid UTF-8 in string field"))?;
        *self = InlineFlexStr::try_from_type(s)
            .map_err(|_| prost::DecodeError::new("string too long for inline storage"))?;
        Ok(())
    }

    fn merge_length_delimited(
        &mut self,
        mut buf: impl prost::bytes::Buf,
    ) -> Result<(), prost::DecodeError>
    where
        Self: Sized,
    {
        let len = prost::encoding::decode_varint(&mut buf)? as usize;
        if buf.remaining() < len {
            return Err(prost::DecodeError::new("buffer underflow"));
        }
        self.merge(buf.take(len))
    }
}

// *** SQLx ***

#[cfg(feature = "sqlx")]
impl<'r, DB: sqlx::Database> sqlx::Decode<'r, DB> for InlineFlexStr<str>
where
    &'r str: sqlx::Decode<'r, DB>,
{
    fn decode(
        value: <DB as sqlx::Database>::ValueRef<'r>,
    ) -> Result<Self, sqlx::error::BoxDynError> {
        let value = <&str as sqlx::Decode<DB>>::decode(value)?;
        Ok(value.try_into()?)
    }
}

#[cfg(feature = "sqlx")]
impl<'r, DB: sqlx::Database> sqlx::Encode<'r, DB> for InlineFlexStr<str>
where
    String: sqlx::Encode<'r, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as sqlx::Database>::ArgumentBuffer<'r>,
    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
        // There might be a more efficient way to do this (or not?), but the lifetimes seem to be constraining
        // us to using an owned type here. Works at the cost of an allocation/copy.
        <String as sqlx::Encode<'r, DB>>::encode(self.to_string(), buf)
    }

    fn size_hint(&self) -> usize {
        self.len()
    }
}

#[cfg(feature = "sqlx")]
impl<DB: sqlx::Database> sqlx::Type<DB> for InlineFlexStr<str>
where
    str: sqlx::Type<DB>,
{
    fn type_info() -> <DB as sqlx::Database>::TypeInfo {
        <str as sqlx::Type<DB>>::type_info()
    }

    fn compatible(ty: &<DB as sqlx::Database>::TypeInfo) -> bool {
        <str as sqlx::Type<DB>>::compatible(ty)
    }
}

#[cfg(all(feature = "sqlx", feature = "sqlx_pg_arrays"))]
impl sqlx::postgres::PgHasArrayType for InlineFlexStr<str>
where
    for<'a> &'a str: sqlx::postgres::PgHasArrayType,
{
    fn array_type_info() -> sqlx::postgres::PgTypeInfo {
        <&str as sqlx::postgres::PgHasArrayType>::array_type_info()
    }

    fn array_compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
        <&str as sqlx::postgres::PgHasArrayType>::array_compatible(ty)
    }
}

// *** Utoipa ***

#[cfg(feature = "utoipa")]
impl utoipa::PartialSchema for InlineFlexStr<str> {
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        utoipa::openapi::schema::ObjectBuilder::new()
            .schema_type(utoipa::openapi::schema::SchemaType::new(
                utoipa::openapi::schema::Type::String,
            ))
            .into()
    }
}

#[cfg(feature = "utoipa")]
impl utoipa::ToSchema for InlineFlexStr<str> {
    fn name() -> Cow<'static, str> {
        Cow::Borrowed("String")
    }
}