use crate::{PrimitiveError, PrimitiveResult};
use core::{
cmp::Ordering,
fmt,
hash::{Hash, Hasher},
ops::Deref,
str::FromStr,
};
#[derive(Clone, Copy)]
pub struct InlineStr<const MIN: usize, const MAX: usize> {
buf: [u8; MAX],
len: usize,
}
impl<const MIN: usize, const MAX: usize> InlineStr<MIN, MAX> {
pub fn new(value: &str) -> PrimitiveResult<Self> {
let bytes = value.as_bytes();
let actual = bytes.len();
if MIN > MAX {
return Err(PrimitiveError::OutOfRange {
min: MIN as u128,
max: MAX as u128,
actual: actual as u128,
});
}
if actual < MIN {
return Err(PrimitiveError::TooShort { min: MIN, actual });
}
if actual > MAX {
return Err(PrimitiveError::TooLong { max: MAX, actual });
}
if MIN > 0 && value.trim().is_empty() {
return Err(PrimitiveError::Empty);
}
let mut buf = [0u8; MAX];
buf[..actual].copy_from_slice(bytes);
Ok(Self { buf, len: actual })
}
pub fn as_str(&self) -> &str {
core::str::from_utf8(self.as_bytes())
.expect("InlineStr always holds valid UTF-8 by construction")
}
pub fn as_bytes(&self) -> &[u8] {
&self.buf[..self.len]
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn min_len(&self) -> usize {
MIN
}
pub fn max_len(&self) -> usize {
MAX
}
}
impl<const MIN: usize, const MAX: usize> fmt::Display for InlineStr<MIN, MAX> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<const MIN: usize, const MAX: usize> fmt::Debug for InlineStr<MIN, MAX> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self.as_str(), f)
}
}
impl<const MIN: usize, const MAX: usize> AsRef<str> for InlineStr<MIN, MAX> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<const MIN: usize, const MAX: usize> Deref for InlineStr<MIN, MAX> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl<const MIN: usize, const MAX: usize> PartialEq for InlineStr<MIN, MAX> {
fn eq(&self, other: &Self) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl<const MIN: usize, const MAX: usize> Eq for InlineStr<MIN, MAX> {}
impl<const MIN: usize, const MAX: usize> Hash for InlineStr<MIN, MAX> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_bytes().hash(state);
}
}
impl<const MIN: usize, const MAX: usize> PartialOrd for InlineStr<MIN, MAX> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<const MIN: usize, const MAX: usize> Ord for InlineStr<MIN, MAX> {
fn cmp(&self, other: &Self) -> Ordering {
self.as_bytes().cmp(other.as_bytes())
}
}
impl<const MIN: usize, const MAX: usize> PartialEq<str> for InlineStr<MIN, MAX> {
fn eq(&self, other: &str) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl<const MIN: usize, const MAX: usize> PartialEq<&str> for InlineStr<MIN, MAX> {
fn eq(&self, other: &&str) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl<const MIN: usize, const MAX: usize> TryFrom<&str> for InlineStr<MIN, MAX> {
type Error = PrimitiveError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl<const MIN: usize, const MAX: usize> FromStr for InlineStr<MIN, MAX> {
type Err = PrimitiveError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[cfg(test)]
mod tests {
use super::InlineStr;
use crate::PrimitiveError;
#[test]
fn accepts_valid_length() {
let value = InlineStr::<3, 12>::new("service").unwrap();
assert_eq!(value.as_str(), "service");
assert_eq!(value.len(), 7);
assert_eq!(value.min_len(), 3);
assert_eq!(value.max_len(), 12);
assert!(!value.is_empty());
}
#[test]
fn rejects_too_short() {
assert_eq!(
InlineStr::<3, 12>::new("ab").unwrap_err(),
PrimitiveError::TooShort { min: 3, actual: 2 }
);
}
#[test]
fn rejects_too_long() {
assert_eq!(
InlineStr::<1, 5>::new("service").unwrap_err(),
PrimitiveError::TooLong { max: 5, actual: 7 }
);
}
#[test]
fn counts_bytes_not_chars() {
let value = InlineStr::<4, 4>::new("éå").unwrap();
assert_eq!(value.len(), 4);
assert_eq!(value.as_str(), "éå");
assert_eq!(
InlineStr::<1, 1>::new("é").unwrap_err(),
PrimitiveError::TooLong { max: 1, actual: 2 }
);
}
#[test]
fn rejects_whitespace_only_when_min_positive() {
assert_eq!(
InlineStr::<1, 5>::new(" ").unwrap_err(),
PrimitiveError::Empty
);
}
#[test]
fn allows_zero_min_whitespace_only() {
let value = InlineStr::<0, 5>::new(" ").unwrap();
assert_eq!(value.as_str(), " ");
assert_eq!(value.len(), 3);
}
#[test]
fn allows_empty_when_min_zero() {
let value = InlineStr::<0, 4>::new("").unwrap();
assert!(value.is_empty());
assert_eq!(value.as_str(), "");
}
#[test]
fn zero_capacity_only_holds_empty() {
assert!(InlineStr::<0, 0>::new("").unwrap().is_empty());
assert_eq!(
InlineStr::<0, 0>::new("x").unwrap_err(),
PrimitiveError::TooLong { max: 0, actual: 1 }
);
}
#[test]
fn handles_invalid_bounds() {
assert_eq!(
InlineStr::<5, 3>::new("abcd").unwrap_err(),
PrimitiveError::OutOfRange {
min: 5,
max: 3,
actual: 4
}
);
}
#[test]
fn exact_min_and_max_fit() {
assert_eq!(InlineStr::<3, 3>::new("abc").unwrap().len(), 3);
assert!(InlineStr::<3, 3>::new("ab").is_err());
assert!(InlineStr::<3, 3>::new("abcd").is_err());
}
#[test]
fn is_copy() {
let a = InlineStr::<1, 8>::new("copy").unwrap();
let b = a;
assert_eq!(a.as_str(), "copy");
assert_eq!(b.as_str(), "copy");
}
#[test]
fn as_bytes_excludes_unused_tail() {
let value = InlineStr::<1, 16>::new("hi").unwrap();
assert_eq!(value.as_bytes(), b"hi");
}
#[test]
fn deref_and_as_ref_to_str() {
let value = InlineStr::<1, 8>::new("api").unwrap();
let s: &str = value.as_ref();
assert_eq!(s, "api");
assert!(value.starts_with("ap"));
}
#[test]
fn equality_and_ordering() {
let a = InlineStr::<1, 8>::new("alpha").unwrap();
let b = InlineStr::<1, 8>::new("beta").unwrap();
assert_eq!(a, InlineStr::<1, 8>::new("alpha").unwrap());
assert_ne!(a, b);
assert!(a < b);
assert_eq!(a, "alpha");
}
#[test]
fn from_str_and_try_from() {
let parsed: InlineStr<1, 8> = "node".parse().unwrap();
assert_eq!(parsed.as_str(), "node");
let converted = InlineStr::<1, 8>::try_from("node").unwrap();
assert_eq!(parsed, converted);
assert!("toolongforbudget".parse::<InlineStr<1, 8>>().is_err());
}
#[test]
fn display_and_debug() {
use alloc::string::ToString;
let value = InlineStr::<3, 12>::new("service").unwrap();
assert_eq!(value.to_string(), "service");
assert_eq!(alloc::format!("{value:?}"), "\"service\"");
}
#[test]
fn hash_distinguishes_contents() {
use core::hash::{Hash, Hasher};
fn fnv(value: &InlineStr<1, 8>) -> u64 {
struct Fnv(u64);
impl Hasher for Fnv {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
}
}
}
let mut hasher = Fnv(0xcbf2_9ce4_8422_2325);
value.hash(&mut hasher);
hasher.finish()
}
let a = InlineStr::<1, 8>::new("alpha").unwrap();
assert_eq!(fnv(&a), fnv(&InlineStr::<1, 8>::new("alpha").unwrap()));
assert_ne!(fnv(&a), fnv(&InlineStr::<1, 8>::new("beta").unwrap()));
}
#[test]
fn str_equality_both_impls() {
let a = InlineStr::<1, 8>::new("alpha").unwrap();
assert!(a == "alpha");
assert!(a != "other");
fn eq_str<T: PartialEq<str> + Copy>(value: T, s: &str) -> bool {
value == *s
}
assert!(eq_str(a, "alpha"));
assert!(!eq_str(a, "other"));
}
}