Skip to content

Commit 78f5376

Browse files
committed
Add more traits that String also has
These include - Index - Add - AddAssign - Extend - Write - PartialEq with `str` & `String`
1 parent ea722bb commit 78f5376

File tree

6 files changed

+258
-40
lines changed

6 files changed

+258
-40
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- More traits from `String` implemented on `NonEmptyString`
13+
- Index
14+
- Add
15+
- AddAssign
16+
- Extend
17+
- Write
18+
- PartialEq with `str` & `String`
19+
1220
### Changed
1321

1422
### Removed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ description = "A simple type for non empty Strings, similar to NonZeroUsize and
88
repository = "https://github.com/MidasLamb/non-empty-string"
99
keywords = ["nonemptystring", "string", "str", "non-empty", "nonempty"]
1010

11+
[package.metadata."docs.rs"]
12+
all-features = true
13+
1114
[lib]
1215
name = "non_empty_string"
1316

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,42 @@ assert_eq!(result.unwrap_err(), "".to_owned())
2323
```
2424

2525
## Methods of std::string::String
26+
2627
`NonEmptyString` implements a subset of the functions of `std::string::String`, only the ones which are guaranteed to leave the `NonEmptyString` in a valid state.
2728
This means i.e. `push()` is implemented, but `pop()` is not.
2829

2930
This allows you to mostly treat it as a String without having to constantly turn it into the inner `String` before you can do any sort of operation on it and then having to reconstruct a `NonEmptyString` afterwards.
3031

32+
If a method is missing that you think upholds the invariant of `NonEmptyString`, don't hesitate to [open an issue].
33+
34+
## Traits
35+
36+
`NonEmptyString` implements quite a few of the traits that `String` implements, where it simply forwards it to the underlying string,
37+
which allows e.g. indexing with ranges:
38+
39+
```rust
40+
use non_empty_string::NonEmptyString;
41+
42+
let non_empty = NonEmptyString::new("ABC".to_owned()).unwrap();
43+
assert_eq!(&non_empty[1..], "BC");
44+
45+
```
46+
47+
If a trait is missing that you think upholds the invariant of `NonEmptyString`, don't hesitate to [open an issue].
3148

3249
## Serde Support
3350

3451
[serde] support is available behind the `serde` feature flag:
52+
3553
```toml
3654
[dependencies]
3755
serde = { version = "1", features = ["derive"] }
3856
non-empty-string = { version = "*", features = ["serde"]}
3957
```
4058

4159
Afterwards you can use it in a struct:
42-
```rust
60+
61+
```ignore
4362
use serde::{Serialize, Deserialize};
4463
use non_empty_string::NonEmptyString;
4564
@@ -55,10 +74,10 @@ Deserialization will fail if the field is present as a String, but the length of
5574

5675
Licensed under either of
5776

58-
* Apache License, Version 2.0
59-
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
60-
* MIT license
61-
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
77+
- Apache License, Version 2.0
78+
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
79+
- MIT license
80+
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
6281

6382
at your option.
6483

@@ -68,4 +87,5 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
6887
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
6988
dual licensed as above, without any additional terms or conditions.
7089

71-
[serde]: https://docs.rs/serde
90+
[serde]: https://docs.rs/serde
91+
[open an issue]: https://github.com/MidasLamb/non-empty-string/issues

src/lib.rs

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ mod test_readme {
99
#[doc = include_str!("../README.md")]
1010
mod something {}
1111
}
12-
mod error;
12+
13+
use std::str::FromStr;
14+
15+
use delegate::delegate;
16+
1317
#[cfg(feature = "serde")]
1418
mod serde_support;
1519

16-
use delegate::delegate;
17-
pub use error::EmptyString;
18-
use std::{fmt::Display, str::FromStr};
20+
mod trait_impls;
1921

2022
/// A simple String wrapper type, similar to NonZeroUsize and friends.
2123
/// Guarantees that the String contained inside is not of length 0.
@@ -149,21 +151,11 @@ impl TryFrom<String> for NonEmptyString {
149151
}
150152
}
151153

152-
impl Display for NonEmptyString {
153-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154-
Display::fmt(&self.0, f)
155-
}
156-
}
157-
158154
impl FromStr for NonEmptyString {
159-
type Err = EmptyString;
155+
type Err = <NonEmptyString as TryFrom<String>>::Error;
160156

161157
fn from_str(s: &str) -> Result<Self, Self::Err> {
162-
if s.is_empty() {
163-
return Err(EmptyString);
164-
}
165-
166-
Ok(Self(s.to_string()))
158+
<Self as TryFrom<String>>::try_from(s.to_string())
167159
}
168160
}
169161

@@ -175,8 +167,7 @@ impl From<NonEmptyString> for String {
175167

176168
#[cfg(test)]
177169
mod tests {
178-
use std::collections::HashMap;
179-
use std::collections::HashSet;
170+
use std::hash::Hash;
180171

181172
use super::*;
182173

@@ -231,12 +222,9 @@ mod tests {
231222
#[test]
232223
fn from_str_works() {
233224
let valid_str = "string";
234-
235-
let _non_empty_string = NonEmptyString::from_str("").expect_err("operation must be failed");
236-
237225
let non_empty_string = NonEmptyString::from_str(valid_str).unwrap();
238-
assert_eq!(non_empty_string.as_str(), valid_str);
239-
assert_eq!(non_empty_string, valid_str.parse().unwrap());
226+
let parsed: NonEmptyString = valid_str.parse().unwrap();
227+
assert_eq!(non_empty_string, parsed);
240228
}
241229

242230
#[test]
@@ -250,16 +238,7 @@ mod tests {
250238

251239
#[test]
252240
fn hash_works() {
253-
let mut map = HashMap::new();
254-
map.insert(NonEmptyString::from_str("id.1").unwrap(), 1);
255-
map.insert(NonEmptyString::from_str("id.2").unwrap(), 2);
256-
257-
assert_eq!(map.len(), 2);
258-
259-
let mut set = HashSet::new();
260-
set.insert(NonEmptyString::from_str("1").unwrap());
261-
set.insert(NonEmptyString::from_str("2").unwrap());
262-
263-
assert_eq!(set.len(), 2);
241+
fn is_hash<T: Hash>() {}
242+
is_hash::<NonEmptyString>();
264243
}
265244
}

src/trait_impls/delegated_traits.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//! This module contains traits that are simply delegated to the underlying [`NonEmptyString`].
2+
//!
3+
//! The order of traits implemented here follow the same structure as defined in the standard library.
4+
//! When adding new ones, add them in the same order, so it's easy to keep track.
5+
//!
6+
//! The source at the moment of writing is [here](https://github.com/rust-lang/rust/blob/22d41ae90facbffdef9115809e8b6c1f71ebbf7c/library/alloc/src/string.rs#L2019).
7+
//! The link to master is [here](https://github.com/rust-lang/rust/blob/master/library/alloc/src/string.rs#L2019).
8+
//!
9+
10+
use crate::NonEmptyString;
11+
use std::borrow::Cow;
12+
use std::fmt::Display;
13+
use std::ops::{self, Add, AddAssign};
14+
15+
#[cfg(not(no_global_oom_handling))]
16+
impl Extend<char> for NonEmptyString {
17+
fn extend<I: IntoIterator<Item = char>>(&mut self, iter: I) {
18+
self.0.extend(iter)
19+
}
20+
}
21+
22+
#[cfg(not(no_global_oom_handling))]
23+
impl<'a> Extend<&'a char> for NonEmptyString {
24+
fn extend<I: IntoIterator<Item = &'a char>>(&mut self, iter: I) {
25+
self.extend(iter.into_iter().cloned());
26+
}
27+
}
28+
29+
#[cfg(not(no_global_oom_handling))]
30+
impl<'a> Extend<&'a str> for NonEmptyString {
31+
fn extend<I: IntoIterator<Item = &'a str>>(&mut self, iter: I) {
32+
iter.into_iter().for_each(move |s| self.push_str(s));
33+
}
34+
}
35+
36+
#[cfg(not(no_global_oom_handling))]
37+
impl Extend<Box<str>> for NonEmptyString {
38+
fn extend<I: IntoIterator<Item = Box<str>>>(&mut self, iter: I) {
39+
iter.into_iter().for_each(move |s| self.push_str(&s));
40+
}
41+
}
42+
43+
#[cfg(not(no_global_oom_handling))]
44+
impl Extend<String> for NonEmptyString {
45+
fn extend<I: IntoIterator<Item = String>>(&mut self, iter: I) {
46+
iter.into_iter().for_each(move |s| self.0.push_str(&s));
47+
}
48+
}
49+
50+
#[cfg(not(no_global_oom_handling))]
51+
impl<'a> Extend<Cow<'a, str>> for NonEmptyString {
52+
fn extend<I: IntoIterator<Item = Cow<'a, str>>>(&mut self, iter: I) {
53+
iter.into_iter().for_each(move |s| self.push_str(&s));
54+
}
55+
}
56+
57+
macro_rules! impl_eq {
58+
($lhs:ty, $rhs: ty) => {
59+
#[allow(unused_lifetimes)]
60+
impl<'a, 'b> PartialEq<$rhs> for $lhs {
61+
#[inline]
62+
fn eq(&self, other: &$rhs) -> bool {
63+
PartialEq::eq(&self[..], &other[..])
64+
}
65+
}
66+
67+
#[allow(unused_lifetimes)]
68+
impl<'a, 'b> PartialEq<$lhs> for $rhs {
69+
#[inline]
70+
fn eq(&self, other: &$lhs) -> bool {
71+
PartialEq::eq(&self[..], &other[..])
72+
}
73+
}
74+
};
75+
}
76+
77+
impl_eq! { NonEmptyString, str }
78+
impl_eq! { NonEmptyString, &'a str }
79+
#[cfg(not(no_global_oom_handling))]
80+
impl_eq! { Cow<'a, str>, NonEmptyString }
81+
#[cfg(not(no_global_oom_handling))]
82+
impl_eq! { String, NonEmptyString }
83+
84+
// No sensible implementation for Default
85+
// impl Default for NonEmptyString {
86+
// }
87+
88+
impl Display for NonEmptyString {
89+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90+
Display::fmt(&self.0, f)
91+
}
92+
}
93+
94+
// Derived:
95+
// impl fmt::Debug for String {
96+
// }
97+
98+
// Derived:
99+
// impl hash::Hash for NonEmptyString {
100+
// }
101+
102+
#[cfg(not(no_global_oom_handling))]
103+
impl Add<&str> for NonEmptyString {
104+
type Output = NonEmptyString;
105+
106+
#[inline]
107+
fn add(mut self, other: &str) -> NonEmptyString {
108+
self.push_str(other);
109+
self
110+
}
111+
}
112+
113+
#[cfg(not(no_global_oom_handling))]
114+
impl AddAssign<&str> for NonEmptyString {
115+
#[inline]
116+
fn add_assign(&mut self, other: &str) {
117+
self.push_str(other);
118+
}
119+
}
120+
121+
macro_rules! index_impl {
122+
($t:ty) => {
123+
impl ops::Index<$t> for NonEmptyString {
124+
type Output = str;
125+
126+
#[inline]
127+
fn index(&self, index: $t) -> &str {
128+
<String as ops::Index<$t>>::index(&self.0, index)
129+
}
130+
}
131+
};
132+
}
133+
134+
index_impl!(ops::Range<usize>);
135+
index_impl!(ops::RangeTo<usize>);
136+
index_impl!(ops::RangeFrom<usize>);
137+
index_impl!(ops::RangeFull);
138+
index_impl!(ops::RangeInclusive<usize>);
139+
index_impl!(ops::RangeToInclusive<usize>);
140+
141+
// Not 100% sure if index_mut allows turning a NonEmptyString into an empty string or not, let's leave it out until sure.
142+
// macro_rules! index_mut_impl {
143+
// ($t:ty) => {
144+
// impl ops::IndexMut<$t> for NonEmptyString {
145+
// #[inline]
146+
// fn index_mut(&mut self, index: $t) -> &mut str {
147+
// <String as ops::IndexMut<$t>>::index_mut(&mut self.0, index)
148+
// }
149+
// }
150+
// };
151+
// }
152+
153+
// index_mut_impl!(ops::Range<usize>);
154+
// index_mut_impl!(ops::RangeTo<usize>);
155+
// index_mut_impl!(ops::RangeFrom<usize>);
156+
// index_mut_impl!(ops::RangeFull);
157+
// index_mut_impl!(ops::RangeInclusive<usize>);
158+
// index_mut_impl!(ops::RangeToInclusive<usize>);
159+
160+
// Point of discussion, see https://github.com/MidasLamb/non-empty-string/pull/11
161+
// impl ops::Deref for NonEmptyString {
162+
// }
163+
164+
// This would mean people could empty out the string, so no.
165+
// impl ops::DerefMut for NonEmptyString {
166+
// }
167+
168+
#[cfg(not(no_global_oom_handling))]
169+
impl std::fmt::Write for NonEmptyString {
170+
#[inline]
171+
fn write_str(&mut self, s: &str) -> std::fmt::Result {
172+
self.push_str(s);
173+
Ok(())
174+
}
175+
176+
#[inline]
177+
fn write_char(&mut self, c: char) -> std::fmt::Result {
178+
self.push(c);
179+
Ok(())
180+
}
181+
}

src/trait_impls/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use std::borrow::Borrow;
2+
3+
mod delegated_traits;
4+
5+
use crate::NonEmptyString;
6+
7+
#[cfg(not(no_global_oom_handling))]
8+
impl From<char> for NonEmptyString {
9+
#[inline]
10+
fn from(c: char) -> Self {
11+
let string = c.to_string();
12+
NonEmptyString::new(string).expect("since there is a singular char, the string will not be empty as it will contain exactly one char")
13+
}
14+
}
15+
16+
// Defined in the file for [`str`], not [`String`]
17+
impl Borrow<str> for NonEmptyString {
18+
fn borrow(&self) -> &str {
19+
<String as Borrow<str>>::borrow(&self.0)
20+
}
21+
}
22+
23+
impl Borrow<String> for NonEmptyString {
24+
fn borrow(&self) -> &String {
25+
&self.0
26+
}
27+
}

0 commit comments

Comments
 (0)