naming_lib/naming_case.rs
1use std::fmt::{Display, Formatter, Result as FmtResult};
2
3use lazy_static::lazy_static;
4use regex::Regex;
5
6use crate::detector;
7
8/// Indicates which format the string belongs to,
9/// and acts as an intermediary between format conversions.
10///
11/// ## Create Instances
12///
13/// There are three ways to create an instance.
14/// These three are aliases of each other.
15///
16/// ```
17/// use naming_lib::{NamingCase, which_case, from};
18///
19/// let first = NamingCase::new("identifier");
20/// let second = which_case("identifier");
21/// let third = from("identifier");
22/// ```
23///
24/// ### Notice
25///
26/// Of course you can generate instances of a specific enum type directly,
27/// I can't stop you
28/// (in fact [there is a solution](https://stackoverflow.com/a/28090996/11397457),
29/// but it makes things more complex),
30/// but I **don't recommend using this approach**.
31///
32/// ```
33/// use naming_lib::NamingCase;
34///
35/// let direct_instance = NamingCase::Invalid("text".to_string());
36/// ```
37///
38/// I can't do an input valid check when you use this approach,
39/// type-related methods on these instances **may cause unexpected panic**.
40///
41/// I currently use this myself to:
42///
43/// 1. write document test cases
44/// (have to use this to "clearly express the state of the test values").
45///
46/// 2. generated instances of [Invalid](NamingCase::Invalid) enum type
47/// (it's safe, because conversion methods cannot be called on this enum type,
48/// there are no other type-related methods available now).
49///
50/// ## Get Origin String From An Instance
51///
52/// A [NamingCase] instance holds the given string value when created,
53/// which can be got by calling [to_string()](std::string::ToString).
54///
55/// ```
56/// use naming_lib::from;
57///
58/// assert_eq!("example",from("example").to_string())
59/// ```
60///
61/// ## Convert An Instance To Other Naming Case String
62///
63/// A [NamingCase] instance also can be converted to a string in another naming format,
64/// as long as it's not the [Invalid](NamingCase::Invalid) enum.
65///
66/// ```
67/// use naming_lib::from;
68///
69/// assert_eq!("camel_case", from("camelCase").to_snake().unwrap());
70/// ```
71///
72/// ### Notice
73///
74/// For ease of use,
75/// instead of implementing the conversion methods
76/// with [Invalid](NamingCase::Invalid) excluded,
77/// I have chosen that all conversion methods
78/// will return the [Result](core::result) type.
79///
80/// Calling any conversion method on an [Invalid](NamingCase::Invalid) enum
81/// will return an [Err](core::result::Result::Err).
82#[derive(PartialEq, Debug)]
83pub enum NamingCase {
84 /// A single word will be recognized as multiple formats
85 /// (snake, kebab, camel),
86 /// so it belongs to a separate category.
87 SingleWord(String),
88 ScreamingSnake(String),
89 Snake(String),
90 Kebab(String),
91 Camel(String),
92 Pascal(String),
93 /// Can't be recognized as a known format.
94 Invalid(String),
95}
96
97impl Display for NamingCase {
98 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
99 match self {
100 NamingCase::SingleWord(s) => { write!(f, "{}", s) }
101 NamingCase::ScreamingSnake(s) => { write!(f, "{}", s) }
102 NamingCase::Snake(s) => { write!(f, "{}", s) }
103 NamingCase::Kebab(s) => { write!(f, "{}", s) }
104 NamingCase::Camel(s) => { write!(f, "{}", s) }
105 NamingCase::Pascal(s) => { write!(f, "{}", s) }
106 NamingCase::Invalid(s) => { write!(f, "{}", s) }
107 }
108 }
109}
110
111impl NamingCase {
112 /// Create a [NamingCase] value from an identifier.
113 ///
114 /// Alias of [which_case()](crate::detector::which_case()) and [from()](crate::naming_case::from()).
115 pub fn new(identifier: &str) -> NamingCase {
116 detector::which_case(identifier)
117 }
118
119 /// Check if this is an [Invalid](NamingCase::Invalid) instance.
120 pub fn is_invalid(&self) -> bool {
121 if let NamingCase::Invalid(_) = self {
122 true
123 } else {
124 false
125 }
126 }
127
128 /// Convert the included string to screaming snake case.
129 ///
130 /// # Examples
131 ///
132 /// ```
133 /// use naming_lib::{from};
134 ///
135 /// assert_eq!("SCREAMING", from("Screaming").to_screaming_snake().unwrap());
136 /// assert_eq!("CAMEL_CASE", from("camelCase").to_screaming_snake().unwrap());
137 /// assert_eq!("SNAKE_CASE", from("snake_case").to_screaming_snake().unwrap());
138 /// ```
139 /// # Errors
140 ///
141 /// Perform this on [Invalid](NamingCase::Invalid) enum
142 /// will get an [Err](core::result::Result::Err).
143 pub fn to_screaming_snake(&self) -> Result<String, &'static str> {
144 let words = extract_words_from(self)?;
145 Ok(words.into_iter()
146 .map(|word| word.to_ascii_uppercase())
147 .collect::<Vec<String>>()
148 .join("_"))
149 }
150
151 /// Convert the included string to snake case.
152 ///
153 /// # Examples
154 ///
155 /// ```
156 /// use naming_lib::{from};
157 ///
158 /// assert_eq!("snake", from("Snake").to_snake().unwrap());
159 /// assert_eq!("kebab_case", from("kebab-case").to_snake().unwrap());
160 /// assert_eq!("camel_case", from("camelCase").to_snake().unwrap());
161 /// ```
162 /// # Errors
163 ///
164 /// Perform this on [Invalid](NamingCase::Invalid) enum
165 /// will get an [Err](core::result::Result::Err).
166 pub fn to_snake(&self) -> Result<String, &'static str> {
167 let words = extract_words_from(self)?;
168 Ok(words.into_iter()
169 .map(|word| word.to_ascii_lowercase())
170 .collect::<Vec<String>>()
171 .join("_"))
172 }
173
174 /// Convert the included string to kebab case.
175 ///
176 /// # Examples
177 ///
178 /// ```
179 /// use naming_lib::{from};
180 ///
181 /// assert_eq!("kebab", from("Kebab").to_kebab().unwrap());
182 /// assert_eq!("camel-case", from("camelCase").to_kebab().unwrap());
183 /// assert_eq!("snake-case", from("snake_case").to_kebab().unwrap());
184 /// ```
185 /// # Errors
186 ///
187 /// Perform this on [Invalid](NamingCase::Invalid) enum
188 /// will get an [Err](core::result::Result::Err).
189 pub fn to_kebab(&self) -> Result<String, &'static str> {
190 let words = extract_words_from(self)?;
191 Ok(words.into_iter()
192 .map(|word| word.to_ascii_lowercase())
193 .collect::<Vec<String>>()
194 .join("-"))
195 }
196
197 /// Convert the included string to camel case.
198 ///
199 /// # Examples
200 ///
201 /// ```
202 /// use naming_lib::{from};
203 ///
204 /// assert_eq!("camel", from("Camel").to_camel().unwrap());
205 /// assert_eq!("pascalCase", from("PascalCase").to_camel().unwrap());
206 /// assert_eq!("snakeCase", from("snake_case").to_camel().unwrap());
207 /// ```
208 /// # Errors
209 ///
210 /// Perform this on [Invalid](NamingCase::Invalid) enum
211 /// will get an [Err](core::result::Result::Err).
212 ///
213 pub fn to_camel(&self) -> Result<String, &'static str> {
214 let words = extract_words_from(self)?;
215 let mut iter = words.into_iter();
216 let first_word = iter.next().unwrap();
217 Ok(first_word.to_ascii_lowercase() + &compose_words_to_pascal(iter.collect()))
218 }
219
220 /// Convert the included string to pascal case.
221 ///
222 /// # Examples
223 ///
224 /// ```
225 /// use naming_lib::{from};
226 ///
227 /// assert_eq!("Pascal", from("Pascal").to_pascal().unwrap());
228 /// assert_eq!("CamelCase", from("camelCase").to_pascal().unwrap());
229 /// assert_eq!("SnakeCase", from("snake_case").to_pascal().unwrap());
230 /// ```
231 /// # Errors
232 ///
233 /// Perform this on [Invalid](NamingCase::Invalid) enum
234 /// will get an [Err](core::result::Result::Err).
235 pub fn to_pascal(&self) -> Result<String, &'static str> {
236 let words = extract_words_from(self)?;
237 Ok(compose_words_to_pascal(words))
238 }
239}
240
241/// Create a [NamingCase] value from an identifier.
242///
243/// Alias of [which_case()](crate::detector::which_case()) and [NamingCase::new()].
244pub fn from(identifier: &str) -> NamingCase {
245 detector::which_case(identifier)
246}
247
248/// Return a [Pascal](NamingCase::Pascal) enum for a hungarian notation identifier,
249/// remove the first word which representing the variable type.
250///
251/// Or return a [Invalid](NamingCase::Invalid) enum for other inputs.
252///
253/// # Examples
254///
255/// ```
256/// use naming_lib::{from_hungarian_notation,NamingCase};
257///
258/// let valid = from_hungarian_notation("iPageSize");
259/// assert_eq!(valid, NamingCase::Pascal("PageSize".to_string()));
260/// assert_eq!(valid.to_string(), "PageSize");
261///
262/// // A hungarian notation identifier will be recognized as a camel case.
263/// // Even though this is a valid pascal case, it will still be treated as invalid.
264/// let invalid = from_hungarian_notation("NotACamelCase");
265/// assert_eq!(invalid, NamingCase::Invalid("NotACamelCase".to_string()));
266/// ```
267pub fn from_hungarian_notation(identifier: &str) -> NamingCase {
268 let real_case = detector::which_case(identifier);
269 if real_case != NamingCase::Camel(identifier.to_string()) {
270 return NamingCase::Invalid(identifier.to_string());
271 }
272
273 let mut iter = extract_words_from(&real_case).unwrap().into_iter();
274 // discard first word
275 iter.next();
276 // return remains as a pascal case.
277 NamingCase::Pascal(iter.collect::<Vec<String>>().join(""))
278}
279
280lazy_static! {
281 static ref LOWER_CASE_REGEX:Regex=Regex::new(r"^[a-z]+\d*").unwrap();
282 static ref FIRST_UPPER_CASE_REGEX:Regex=Regex::new(r"[A-Z][a-z]*\d*").unwrap();
283}
284
285fn extract_words_from(case: &NamingCase) -> Result<Vec<String>, &'static str> {
286 return match case {
287 NamingCase::SingleWord(ori) => { Ok(vec![ori.to_string()]) }
288 NamingCase::ScreamingSnake(ori) => {
289 Ok(ori.split('_').map(|word| word.to_string()).collect())
290 }
291 NamingCase::Snake(ori) => {
292 Ok(ori.split('_').map(|word| word.to_string()).collect())
293 }
294 NamingCase::Kebab(ori) => {
295 Ok(ori.split('-').map(|word| word.to_string()).collect())
296 }
297 NamingCase::Camel(ori) => {
298 let mut words = Vec::new();
299
300 let first_word = LOWER_CASE_REGEX.captures(&ori).unwrap()
301 .get(0).unwrap().as_str().to_string();
302
303 let other_words = ori.strip_prefix(&first_word).unwrap();
304 let mut other_words = extract_words_from_pascal(&other_words);
305
306 words.push(first_word.to_ascii_lowercase());
307 words.append(&mut other_words);
308
309 Ok(words)
310 }
311 NamingCase::Pascal(ori) => { Ok(extract_words_from_pascal(&ori)) }
312 NamingCase::Invalid(_) => { Err("Can't extract words from this type.") }
313 };
314}
315
316fn extract_words_from_pascal(s: &str) -> Vec<String> {
317 FIRST_UPPER_CASE_REGEX.find_iter(s)
318 .map(|mat| mat.as_str().to_string())
319 .collect()
320}
321
322fn compose_words_to_pascal(words: Vec<String>) -> String {
323 words.into_iter()
324 .map(|word| to_first_uppercase(word))
325 .collect::<Vec<String>>()
326 .join("")
327}
328
329fn to_first_uppercase(s: String) -> String {
330 let (first, other) = s.split_at(1);
331 first.to_ascii_uppercase() + &other.to_ascii_lowercase()
332}