inline_str/
lib.rs

1// Copyright 2025 Adam Gutglick
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// 	http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A string type that stores strings inline when small.
16//!
17//! `InlineStr` is a string type built on top of [`inline-array`] that can store small strings
18//! directly inline to avoid heap allocation, falling back to heap allocation for larger strings.
19//!
20//! This crate doesn't do any of the heavy lifting, if you want to better understand how it works
21//! its recommended to read through inline-array's docs and source code.
22//!
23//! # Examples
24//!
25//! ```
26//! use inline_str::InlineStr;
27//!
28//! let s = InlineStr::from("hello");
29//! assert_eq!(s, "hello");
30//! ```
31//!
32//! # Features
33//!
34//! - **serde**: Enable serialization/deserialization support with serde
35//!
36//! [`inline-array`]: https://crates.io/crates/inline-array
37
38#![deny(clippy::doc_markdown)]
39#![deny(missing_docs)]
40
41use core::str;
42use std::{
43    borrow::{Borrow, Cow},
44    cmp::Ordering,
45    ffi::OsStr,
46    ops::Deref,
47    path::Path,
48};
49
50#[cfg(feature = "serde")]
51mod serde;
52
53use inline_array::InlineArray;
54
55/// Immutable stack-inlinable string type that can be cheaply cloned and shared.
56#[derive(PartialEq, Eq, Clone)]
57pub struct InlineStr {
58    inner: InlineArray,
59}
60
61impl InlineStr {
62    /// Extracts a string slice containing the entire `InlineStr`.
63    pub fn as_str(&self) -> &str {
64        // Safety:
65        // InlineStr can only be created from valid UTF8 byte sequences
66        unsafe { str::from_utf8_unchecked(&self.inner) }
67    }
68
69    /// Returns the length of the `InlineStr` in **bytes**.
70    pub fn len(&self) -> usize {
71        self.inner.len()
72    }
73
74    /// Returns `true` if this `InlineStr` has a length of 0 (in bytes), otherwise `false`.
75    pub fn is_empty(&self) -> bool {
76        self.len() == 0
77    }
78}
79
80impl std::fmt::Display for InlineStr {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        std::fmt::Display::fmt(&**self, f)
83    }
84}
85
86impl std::fmt::Debug for InlineStr {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        std::fmt::Debug::fmt(&**self, f)
89    }
90}
91
92impl std::hash::Hash for InlineStr {
93    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
94        let as_str = &**self;
95        as_str.hash(state);
96    }
97}
98
99impl From<String> for InlineStr {
100    fn from(value: String) -> Self {
101        Self {
102            inner: InlineArray::from(value.as_bytes()),
103        }
104    }
105}
106
107impl From<&String> for InlineStr {
108    fn from(value: &String) -> Self {
109        Self {
110            inner: InlineArray::from(value.as_bytes()),
111        }
112    }
113}
114
115impl From<&str> for InlineStr {
116    fn from(value: &str) -> Self {
117        Self {
118            inner: InlineArray::from(value.as_bytes()),
119        }
120    }
121}
122
123impl PartialOrd for InlineStr {
124    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
125        Some(self.cmp(other))
126    }
127}
128
129impl Ord for InlineStr {
130    fn cmp(&self, other: &Self) -> Ordering {
131        self.as_str().cmp(other.as_str())
132    }
133}
134
135impl PartialEq<String> for InlineStr {
136    fn eq(&self, other: &String) -> bool {
137        self.as_str() == other
138    }
139}
140
141impl PartialEq<InlineStr> for String {
142    fn eq(&self, other: &InlineStr) -> bool {
143        self.as_str() == other.as_str()
144    }
145}
146
147impl PartialEq<&'_ str> for InlineStr {
148    fn eq(&self, other: &&str) -> bool {
149        self.as_str() == *other
150    }
151}
152
153impl PartialEq<InlineStr> for &str {
154    fn eq(&self, other: &InlineStr) -> bool {
155        *self == other.as_str()
156    }
157}
158
159impl PartialEq<&InlineStr> for &str {
160    fn eq(&self, other: &&InlineStr) -> bool {
161        self == *other
162    }
163}
164impl PartialEq<Cow<'_, str>> for InlineStr {
165    fn eq(&self, other: &Cow<'_, str>) -> bool {
166        self.as_str() == other
167    }
168}
169
170impl PartialEq<InlineStr> for Cow<'_, str> {
171    fn eq(&self, other: &InlineStr) -> bool {
172        self.as_ref() == other.as_str()
173    }
174}
175
176impl PartialEq<InlineStr> for &InlineStr {
177    fn eq(&self, other: &InlineStr) -> bool {
178        self.as_str() == other.as_str()
179    }
180}
181
182impl Deref for InlineStr {
183    type Target = str;
184
185    fn deref(&self) -> &Self::Target {
186        self.as_str()
187    }
188}
189
190impl AsRef<str> for InlineStr {
191    fn as_ref(&self) -> &str {
192        self
193    }
194}
195
196impl AsRef<Path> for InlineStr {
197    fn as_ref(&self) -> &Path {
198        self.as_str().as_ref()
199    }
200}
201
202impl AsRef<[u8]> for InlineStr {
203    fn as_ref(&self) -> &[u8] {
204        self.inner.as_ref()
205    }
206}
207
208impl AsRef<OsStr> for InlineStr {
209    fn as_ref(&self) -> &OsStr {
210        self.as_str().as_ref()
211    }
212}
213
214impl Borrow<str> for InlineStr {
215    fn borrow(&self) -> &str {
216        self.as_ref()
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use std::{
223        collections::HashMap,
224        hash::{BuildHasher, RandomState},
225    };
226
227    use super::*;
228
229    #[test]
230    fn test_basic_eq() {
231        let words = "the quick brown fox";
232        let inline_words = InlineStr::from(words);
233
234        assert_eq!(words, &*inline_words);
235        assert_eq!(words, inline_words);
236        assert_eq!(inline_words, words);
237    }
238
239    #[test]
240    fn test_basic_hash() {
241        let hasher = RandomState::new();
242
243        let words = "the quick brown fox";
244        let inline_words = InlineStr::from(words);
245
246        let words_hash = hasher.hash_one(words);
247        let words_hash_2 = hasher.hash_one(words);
248        let inline_hash = hasher.hash_one(inline_words);
249
250        assert_eq!(words_hash, words_hash_2);
251        assert_eq!(words_hash, inline_hash);
252    }
253
254    #[test]
255    fn test_borrow() {
256        let map = [(InlineStr::from("x"), 5)]
257            .into_iter()
258            .collect::<HashMap<InlineStr, i32>>();
259
260        let v = map.get("x");
261        assert_eq!(v, Some(&5));
262    }
263
264    #[cfg(feature = "serde")]
265    #[test]
266    fn test_serde() {
267        let s = "hello world";
268        let inline_s = InlineStr::from("hello world");
269        assert_eq!(s, inline_s);
270        let serialized_s = serde_json::to_value(s).unwrap();
271        let serialized_inline = serde_json::to_value(inline_s.as_str()).unwrap();
272        assert_eq!(serialized_s, serialized_inline);
273        let deserialized: InlineStr = serde_json::from_value(serialized_s).unwrap();
274        assert_eq!(deserialized, "hello world");
275    }
276}