Skip to content

Commit be19eac

Browse files
Add CowStr clone-on-write string type
1 parent 5e5fe55 commit be19eac

File tree

3 files changed

+206
-0
lines changed

3 files changed

+206
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99

10+
- Add `CowStr` (Clone-On-Write string type)
1011
- Minor fixes to module docs.
1112
- Make MSRV of 1.87.0 explicit.
1213
- Implement `Default` for `CapacityError`.

src/cow.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ pub use vec::{Vec, VecView};
166166
mod test_helpers;
167167

168168
pub mod c_string;
169+
pub mod cow;
169170
pub mod deque;
170171
pub mod history_buf;
171172
pub mod index_map;

0 commit comments

Comments
 (0)