1#![allow(clippy::manual_range_contains)]
2
3use std::{convert::TryFrom, fmt};
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Deserializer, Serialize};
7
8pub const fn is_snake_case(string: &str) -> bool {
13 let (len, bytes) = (string.len(), string.as_bytes());
16 const fn valid_start(b: u8) -> bool {
17 b == b'_' || b'a' <= b && b <= b'z'
18 };
19 const fn is_snake_case_character(c: u8) -> bool {
20 b'a' <= c && c <= b'z' || b'0' <= c && c <= b'9' || c == b'_'
21 }
22 if bytes.is_empty() || !valid_start(bytes[0]) {
24 return false;
25 }
26 let mut i = 1; loop {
29 if i >= len - 1 {
30 break true;
31 }
32 if !is_snake_case_character(bytes[i]) {
33 break false;
34 }
35 i += 1;
36 }
37}
38
39#[derive(Clone, Debug)]
43pub struct InvalidSnakeCase;
44
45#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
53#[cfg_attr(feature = "serde", derive(Serialize))]
54pub struct SnakeCase(String);
55
56impl SnakeCase {
57 pub fn try_from_str(s: &str) -> Result<SnakeCase, InvalidSnakeCase> {
58 if is_snake_case(s) {
59 Ok(SnakeCase(s.to_string()))
60 } else {
61 Err(InvalidSnakeCase)
62 }
63 }
64
65 pub fn try_from_string(s: String) -> Result<SnakeCase, InvalidSnakeCase> {
66 if is_snake_case(&s) {
67 Ok(SnakeCase(s))
68 } else {
69 Err(InvalidSnakeCase)
70 }
71 }
72
73 pub fn as_str(&self) -> &str {
74 &self.0
75 }
76
77 pub fn as_ref(&self) -> SnakeCaseRef {
78 SnakeCaseRef(&self.0)
79 }
80}
81
82impl TryFrom<&str> for SnakeCase {
83 type Error = InvalidSnakeCase;
84
85 fn try_from(s: &str) -> Result<Self, Self::Error> {
86 SnakeCase::try_from_str(s)
87 }
88}
89
90impl TryFrom<String> for SnakeCase {
91 type Error = InvalidSnakeCase;
92
93 fn try_from(s: String) -> Result<Self, Self::Error> {
94 SnakeCase::try_from_string(s)
95 }
96}
97
98impl std::borrow::Borrow<str> for SnakeCase {
99 fn borrow(&self) -> &str {
100 &self.0
101 }
102}
103
104impl fmt::Debug for SnakeCase {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 self.as_str().fmt(f)
107 }
108}
109
110impl fmt::Display for SnakeCase {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 self.as_str().fmt(f)
113 }
114}
115
116#[cfg(feature = "serde")]
117impl<'de> Deserialize<'de> for SnakeCase {
118 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
119 where
120 D: Deserializer<'de>,
121 {
122 let string = String::deserialize(deserializer)?;
123 SnakeCase::try_from_str(&string).map_err(|_: InvalidSnakeCase| {
124 serde::de::Error::custom(format!("Expected snake_case, got '{}'", string))
125 })
126 }
127}
128
129impl std::cmp::PartialEq<SnakeCase> for &str {
130 fn eq(&self, other: &SnakeCase) -> bool {
131 *self == other.as_str()
132 }
133}
134
135impl std::cmp::PartialEq<str> for SnakeCase {
136 fn eq(&self, other: &str) -> bool {
137 self.as_str() == other
138 }
139}
140
141impl std::cmp::PartialEq<&str> for SnakeCase {
142 fn eq(&self, other: &&str) -> bool {
143 self.as_str() == *other
144 }
145}
146
147impl std::cmp::PartialEq<String> for SnakeCase {
148 fn eq(&self, other: &String) -> bool {
149 self.as_str() == *other
150 }
151}
152
153#[derive(Copy, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
161#[cfg_attr(feature = "serde", derive(Serialize))]
162pub struct SnakeCaseRef<'a>(&'a str);
163
164impl<'a> SnakeCaseRef<'a> {
165 pub const fn try_from_str(s: &str) -> Result<SnakeCaseRef, InvalidSnakeCase> {
166 if is_snake_case(s) {
167 Ok(SnakeCaseRef(s))
168 } else {
169 Err(InvalidSnakeCase)
170 }
171 }
172
173 pub const fn as_str(&self) -> &'a str {
174 self.0
175 }
176
177 pub fn to_owned(&self) -> SnakeCase {
178 SnakeCase(self.0.to_string())
179 }
180}
181
182impl<'a> TryFrom<&'a str> for SnakeCaseRef<'a> {
183 type Error = InvalidSnakeCase;
184
185 fn try_from(s: &'a str) -> Result<Self, Self::Error> {
186 SnakeCaseRef::try_from_str(s)
187 }
188}
189
190impl std::borrow::Borrow<str> for SnakeCaseRef<'_> {
191 fn borrow(&self) -> &str {
192 &self.0
193 }
194}
195
196impl fmt::Debug for SnakeCaseRef<'_> {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 self.as_str().fmt(f)
199 }
200}
201
202impl fmt::Display for SnakeCaseRef<'_> {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 self.as_str().fmt(f)
205 }
206}
207
208impl std::cmp::PartialEq<SnakeCaseRef<'_>> for str {
209 fn eq(&self, other: &SnakeCaseRef<'_>) -> bool {
210 self == other.0
211 }
212}
213
214impl std::cmp::PartialEq<SnakeCaseRef<'_>> for &str {
215 fn eq(&self, other: &SnakeCaseRef<'_>) -> bool {
216 *self == other.0
217 }
218}
219
220impl std::cmp::PartialEq<str> for SnakeCaseRef<'_> {
221 fn eq(&self, other: &str) -> bool {
222 self.as_str() == other
223 }
224}
225
226impl std::cmp::PartialEq<&str> for SnakeCaseRef<'_> {
227 fn eq(&self, other: &&str) -> bool {
228 self.as_str() == *other
229 }
230}
231
232impl std::cmp::PartialEq<String> for SnakeCaseRef<'_> {
233 fn eq(&self, other: &String) -> bool {
234 self.as_str() == *other
235 }
236}
237
238#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn snake_case() {
246 assert_eq!(SnakeCase::try_from_str("_hello42").unwrap(), "_hello42");
247 assert_eq!(
248 SnakeCase::try_from_str("_hello42").unwrap(),
249 "_hello42".to_string()
250 );
251 assert_eq!("_hello42", SnakeCase::try_from_str("_hello42").unwrap());
252 assert!(SnakeCase::try_from_str("").is_err());
253 assert!(SnakeCase::try_from_str("42").is_err());
254 assert!(SnakeCase::try_from_str("_").is_ok());
255 }
256
257 #[test]
258 fn snake_case_ref() {
259 assert_eq!(SnakeCaseRef::try_from_str("_hello42").unwrap(), "_hello42");
260 assert_eq!(
261 SnakeCaseRef::try_from_str("_hello42").unwrap(),
262 "_hello42".to_string()
263 );
264 assert_eq!("_hello42", SnakeCaseRef::try_from_str("_hello42").unwrap());
265 assert!(SnakeCaseRef::try_from_str("").is_err());
266 assert!(SnakeCaseRef::try_from_str("42").is_err());
267 assert!(SnakeCaseRef::try_from_str("_").is_ok());
268 }
269
270 #[test]
271 fn snake_case_conversions() {
272 let sc = SnakeCase::try_from_str("hello_world").unwrap();
273 let scr: SnakeCaseRef = sc.as_ref();
274 assert_eq!(scr, "hello_world");
275 let sc2: SnakeCase = scr.to_owned();
276 assert_eq!(sc2, "hello_world");
277
278 use std::collections::HashSet;
279 let mut set: HashSet<SnakeCase> = HashSet::new();
280 set.insert(SnakeCase::try_from_str("hello_world").unwrap());
281 assert!(set.contains(SnakeCaseRef::try_from_str("hello_world").unwrap().as_str()));
282 }
283}