Skip to main content

non_non_full/
string.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! This module contains the [`NonEmptyString`] type
8
9/// A string that is guaranteed to contain at least one element.
10///
11/// [`NonEmptyString`] provides a safe wrapper around `String` that maintains the invariant
12/// that the string can never be empty.
13///
14/// # Examples
15///
16/// ```
17/// use non_non_full::NonEmptyString;
18///
19/// let string = "Hello, World!".to_owned();
20/// let non_empty = NonEmptyString::new(string).unwrap();
21///
22/// // Empty String returns None
23/// assert!(NonEmptyString::new(String::new()).is_none());
24/// ```
25#[cfg_attr(feature = "serde", derive(serde::Serialize))]
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub struct NonEmptyString(String);
28
29#[cfg(feature = "serde")]
30impl<'de> serde::Deserialize<'de> for NonEmptyString {
31	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
32	where
33		D: serde::Deserializer<'de>,
34	{
35		let string = String::deserialize(deserializer)?;
36		NonEmptyString::new(string).ok_or_else(|| {
37			serde::de::Error::custom("cannot deserialize empty string as NonEmptyString")
38		})
39	}
40}
41
42impl NonEmptyString {
43	/// Creates a new [`NonEmptyString`] from a [`String`].
44	/// Returns `None` if the input [`String`] is empty.
45	#[must_use]
46	pub fn new(string: String) -> Option<Self> {
47		if string.is_empty() {
48			None
49		} else {
50			Some(Self(string))
51		}
52	}
53
54	/// Gets a reference to the underlying [`String`]
55	#[must_use]
56	pub fn as_string(&self) -> &String {
57		&self.0
58	}
59
60	/// Converts the [`NonEmptyString`] back into a [`String`], consuming self
61	#[must_use]
62	pub fn into_string(self) -> String {
63		self.0
64	}
65
66	/// Returns a string slice containing the entire string
67	#[must_use]
68	pub fn as_str(&self) -> &str {
69		self.0.as_str()
70	}
71
72	/// Pushes a char onto the end of the string
73	pub fn push(&mut self, ch: char) {
74		self.0.push(ch);
75	}
76
77	/// Pushes a string slice onto the end of the string
78	pub fn push_str(&mut self, string: &str) {
79		self.0.push_str(string);
80	}
81
82	/// Inserts a char at the given byte index
83	pub fn insert(&mut self, idx: usize, ch: char) {
84		self.0.insert(idx, ch);
85	}
86
87	/// Inserts a string slice at the given byte index
88	pub fn insert_str(&mut self, idx: usize, string: &str) {
89		self.0.insert_str(idx, string);
90	}
91
92	/// Removes the char at the given byte index.
93	///
94	/// Returns None if removing would make the string empty.
95	pub fn remove(&mut self, idx: usize) -> Option<char> {
96		if self.0.len() == 1 {
97			None
98		} else {
99			Some(self.0.remove(idx))
100		}
101	}
102
103	/// Removes the last char from the string.
104	///
105	/// Returns None if this would make the string empty.
106	pub fn pop(&mut self) -> Option<char> {
107		if self.0.len() == 1 {
108			None
109		} else {
110			self.0.pop()
111		}
112	}
113
114	/// Returns the length of the string in bytes
115	#[expect(clippy::len_without_is_empty, reason = "NonEmptyString is never empty")] // it's literally in the name!
116	#[must_use]
117	pub fn len(&self) -> usize {
118		self.0.len()
119	}
120
121	/// Clears all chars except the first one
122	pub fn clear_except_first(&mut self) {
123		let first = self.0.remove(0);
124		self.0.clear();
125		self.0.push(first);
126	}
127
128	/// Returns an iterator over the chars in the string
129	pub fn chars(&self) -> std::str::Chars<'_> {
130		self.0.chars()
131	}
132
133	/// Returns an iterator over the char indices in the string
134	pub fn char_indices(&self) -> std::str::CharIndices<'_> {
135		self.0.char_indices()
136	}
137}
138
139#[cfg(test)]
140mod tests {
141	use super::*;
142
143	#[test]
144	fn new_with_empty_string() {
145		let empty = String::new();
146		assert!(NonEmptyString::new(empty).is_none());
147	}
148
149	#[test]
150	fn new_with_non_empty_string() {
151		let non_empty = String::from("hello");
152		let non_empty_string = NonEmptyString::new(non_empty.clone()).unwrap();
153		assert_eq!(non_empty_string.as_string(), &non_empty);
154	}
155
156	#[test]
157	fn push_and_pop() {
158		let mut string = NonEmptyString::new(String::from("a")).unwrap();
159		string.push('b');
160		string.push('c');
161		assert_eq!(string.len(), 3);
162		assert_eq!(string.pop(), Some('c'));
163		assert_eq!(string.pop(), Some('b'));
164		assert_eq!(string.pop(), None);
165	}
166
167	#[test]
168	fn remove() {
169		let mut string = NonEmptyString::new(String::from("abc")).unwrap();
170		assert_eq!(string.remove(1), Some('b'));
171		assert_eq!(string.as_str(), "ac");
172		assert_eq!(string.remove(0), Some('a'));
173		assert_eq!(string.remove(0), None);
174	}
175
176	#[test]
177	fn clear_except_first() {
178		let mut string = NonEmptyString::new(String::from("abcd")).unwrap();
179		string.clear_except_first();
180		assert_eq!(string.as_str(), "a");
181	}
182
183	#[test]
184	fn push_str() {
185		let mut string = NonEmptyString::new(String::from("hello")).unwrap();
186		string.push_str(" world");
187		assert_eq!(string.as_str(), "hello world");
188	}
189}