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}