|
| 1 | +//! Clone-on-write string type for heapless. |
| 2 | +//! |
| 3 | +//! Provides `CowStr`, a heapless clone-on-write string that can be |
| 4 | +//! borrowed, static, or owned. Useful for efficiently handling |
| 5 | +//! temporary string references and owned strings. |
| 6 | +//! |
| 7 | +//! NOTE: Unlike `std::borrow::Cow<'a, str>` this type does NOT provide a |
| 8 | +//! smaller in-memory representation for the borrowed variants. The enum is |
| 9 | +//! at least as large as the owned `String<N, LenT>` because the owned |
| 10 | +//! variant carries the inline buffer. The motivation is purely to avoid |
| 11 | +//! paying an O(len) copy when a string is very often reused unchanged, while |
| 12 | +//! still allowing the API to return an owned form when a (rare) mutation or |
| 13 | +//! normalization is required. If the caller always needs an owned `String` |
| 14 | +//! anyway, `CowStr` offers no benefit and should not be used. |
| 15 | +
|
| 16 | +use crate::len_type::LenType; |
| 17 | +use crate::string::StringView; |
| 18 | +use crate::String; |
| 19 | +use core::borrow::Borrow; |
| 20 | + |
| 21 | +/// A clone-on-write (COW) string type specialized for heapless strings. |
| 22 | +/// |
| 23 | +/// `CowStr` can be either: |
| 24 | +/// - `Borrowed(&'a StringView<LenT>)` for a non-`'static` borrowed view, |
| 25 | +/// - `Static(&'static StringView<LenT>)` for a `'static` borrowed view (no deep clone needed), |
| 26 | +/// - `Owned(String<N, LenT>)` for an owned heapless `String`. |
| 27 | +/// |
| 28 | +/// `N` is the inline buffer capacity; `LenT` is the length type (must implement [`LenType`]). |
| 29 | +/// We add `LenT: 'static` because the `Static` variant stores `&'static StringView<LenT>`. |
| 30 | +#[derive(Debug)] |
| 31 | +pub enum CowStr<'a, const N: usize, LenT: LenType = usize> |
| 32 | +where |
| 33 | + LenT: 'static, |
| 34 | +{ |
| 35 | + /// A borrowed view with lifetime `'a`. |
| 36 | + Borrowed(&'a StringView<LenT>), |
| 37 | + |
| 38 | + /// A `'static` borrowed view. |
| 39 | + Static(&'static StringView<LenT>), |
| 40 | + |
| 41 | + /// An owned `String` with inline storage of size `N`. |
| 42 | + Owned(String<N, LenT>), |
| 43 | +} |
| 44 | + |
| 45 | +impl<'a, const N: usize, LenT: LenType> CowStr<'a, N, LenT> |
| 46 | +where |
| 47 | + LenT: 'static, |
| 48 | +{ |
| 49 | + /// Convert the `CowStr` into an owned `String<N, LenT>`. |
| 50 | + /// |
| 51 | + /// This uses `String::try_from(&str)` and will `panic!` on capacity overflow. |
| 52 | + pub fn to_owned(&self) -> String<N, LenT> { |
| 53 | + match self { |
| 54 | + CowStr::Borrowed(sv) => { |
| 55 | + String::try_from(sv.as_str()).expect("capacity too small for CowStr::to_owned") |
| 56 | + } |
| 57 | + CowStr::Static(sv) => { |
| 58 | + String::try_from(sv.as_str()).expect("capacity too small for CowStr::to_owned") |
| 59 | + } |
| 60 | + CowStr::Owned(s) => s.clone(), |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + /// Return the inner value as `&str`. |
| 65 | + pub fn as_str(&self) -> &str { |
| 66 | + match self { |
| 67 | + CowStr::Borrowed(sv) => sv.as_str(), |
| 68 | + CowStr::Static(sv) => sv.as_str(), |
| 69 | + CowStr::Owned(s) => s.as_str(), |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + /// Is this a non-`'static` borrowed view? |
| 74 | + pub fn is_borrowed(&self) -> bool { |
| 75 | + matches!(self, CowStr::Borrowed(_)) |
| 76 | + } |
| 77 | + |
| 78 | + /// Is this a `'static` borrowed view? |
| 79 | + pub fn is_static(&self) -> bool { |
| 80 | + matches!(self, CowStr::Static(_)) |
| 81 | + } |
| 82 | + |
| 83 | + /// Is this an owned string? |
| 84 | + pub fn is_owned(&self) -> bool { |
| 85 | + matches!(self, CowStr::Owned(_)) |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +impl<'a, const N: usize, LenT: LenType> From<&'a StringView<LenT>> for CowStr<'a, N, LenT> |
| 90 | +where |
| 91 | + LenT: 'static, |
| 92 | +{ |
| 93 | + fn from(sv: &'a StringView<LenT>) -> Self { |
| 94 | + CowStr::Borrowed(sv) |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +impl<const N: usize, LenT: LenType> From<String<N, LenT>> for CowStr<'_, N, LenT> |
| 99 | +where |
| 100 | + LenT: 'static, |
| 101 | +{ |
| 102 | + fn from(s: String<N, LenT>) -> Self { |
| 103 | + CowStr::Owned(s) |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +impl<const N: usize, LenT: LenType> CowStr<'static, N, LenT> |
| 108 | +where |
| 109 | + LenT: 'static, |
| 110 | +{ |
| 111 | + /// Construct a `CowStr` that holds a `'static` `StringView`. |
| 112 | + pub const fn from_static(sv: &'static StringView<LenT>) -> Self { |
| 113 | + CowStr::Static(sv) |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +impl<'a, const N: usize, LenT: LenType> Borrow<str> for CowStr<'a, N, LenT> |
| 118 | +where |
| 119 | + LenT: 'static, |
| 120 | +{ |
| 121 | + fn borrow(&self) -> &str { |
| 122 | + self.as_str() |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +// ---------------------- UNIT TESTS ---------------------- |
| 127 | +#[cfg(test)] |
| 128 | +mod tests { |
| 129 | + use super::*; |
| 130 | + use crate::string::StringView; |
| 131 | + use crate::String; |
| 132 | + |
| 133 | + #[test] |
| 134 | + fn test_borrowed_variant() { |
| 135 | + let s: String<16> = String::try_from("hello").unwrap(); |
| 136 | + let view = s.as_view(); |
| 137 | + let cow: CowStr<16> = CowStr::Borrowed(view); |
| 138 | + |
| 139 | + assert!(cow.is_borrowed()); |
| 140 | + assert!(!cow.is_static()); |
| 141 | + assert!(!cow.is_owned()); |
| 142 | + assert_eq!(cow.as_str(), "hello"); |
| 143 | + |
| 144 | + let owned = cow.to_owned(); |
| 145 | + assert_eq!(owned.as_str(), "hello"); |
| 146 | + } |
| 147 | + |
| 148 | + #[test] |
| 149 | + fn test_static_variant() { |
| 150 | + let s: String<16> = String::try_from("world").unwrap(); |
| 151 | + let view: &'static StringView<usize> = Box::leak(Box::new(s)); |
| 152 | + let cow: CowStr<16> = CowStr::Static(view.as_view()); |
| 153 | + |
| 154 | + assert!(!cow.is_borrowed()); |
| 155 | + assert!(cow.is_static()); |
| 156 | + assert!(!cow.is_owned()); |
| 157 | + assert_eq!(cow.as_str(), "world"); |
| 158 | + |
| 159 | + let owned = cow.to_owned(); |
| 160 | + assert_eq!(owned.as_str(), "world"); |
| 161 | + } |
| 162 | + |
| 163 | + #[test] |
| 164 | + fn test_owned_variant() { |
| 165 | + let s: String<16> = String::try_from("heapless").unwrap(); |
| 166 | + let cow: CowStr<16> = CowStr::Owned(s.clone()); |
| 167 | + |
| 168 | + assert!(!cow.is_borrowed()); |
| 169 | + assert!(!cow.is_static()); |
| 170 | + assert!(cow.is_owned()); |
| 171 | + assert_eq!(cow.as_str(), "heapless"); |
| 172 | + |
| 173 | + let owned = cow.to_owned(); |
| 174 | + assert_eq!(owned.as_str(), "heapless"); |
| 175 | + } |
| 176 | + |
| 177 | + #[test] |
| 178 | + fn test_from_stringview() { |
| 179 | + let s: String<16> = String::try_from("from_borrowed").unwrap(); |
| 180 | + let view = s.as_view(); |
| 181 | + let cow: CowStr<16> = CowStr::from(view); |
| 182 | + |
| 183 | + assert!(cow.is_borrowed()); |
| 184 | + assert_eq!(cow.as_str(), "from_borrowed"); |
| 185 | + } |
| 186 | + |
| 187 | + #[test] |
| 188 | + fn test_from_string() { |
| 189 | + let s: String<16> = String::try_from("from_owned").unwrap(); |
| 190 | + let cow: CowStr<16> = CowStr::from(s.clone()); |
| 191 | + |
| 192 | + assert!(cow.is_owned()); |
| 193 | + assert_eq!(cow.as_str(), "from_owned"); |
| 194 | + } |
| 195 | + |
| 196 | + #[test] |
| 197 | + fn test_borrow_trait() { |
| 198 | + let s: String<16> = String::try_from("borrow_trait").unwrap(); |
| 199 | + let cow: CowStr<16> = CowStr::Owned(s); |
| 200 | + |
| 201 | + let b: &str = cow.borrow(); |
| 202 | + assert_eq!(b, "borrow_trait"); |
| 203 | + } |
| 204 | +} |
0 commit comments