codama_nodes/shared/
camel_case_string.rs

1use serde::{Deserialize, Serialize};
2use std::ops::Deref;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
5pub struct CamelCaseString(String);
6
7impl CamelCaseString {
8    pub fn new<T>(string: T) -> Self
9    where
10        T: AsRef<str>,
11    {
12        Self(to_camel_case(string.as_ref()))
13    }
14}
15
16impl From<CamelCaseString> for String {
17    fn from(val: CamelCaseString) -> Self {
18        val.0
19    }
20}
21
22impl From<String> for CamelCaseString {
23    fn from(string: String) -> Self {
24        Self::new(string)
25    }
26}
27
28impl From<&str> for CamelCaseString {
29    fn from(string: &str) -> Self {
30        Self::new(string)
31    }
32}
33
34impl Deref for CamelCaseString {
35    type Target = String;
36
37    fn deref(&self) -> &Self::Target {
38        &self.0
39    }
40}
41
42impl AsRef<str> for CamelCaseString {
43    fn as_ref(&self) -> &str {
44        &self.0
45    }
46}
47
48fn to_camel_case(input: &str) -> String {
49    let mut result = String::new();
50    let mut new_word = true;
51
52    let chars: Vec<char> = input.chars().collect();
53    let mut i = 0;
54    while i < chars.len() {
55        let c = chars[i];
56
57        if c.is_alphanumeric() {
58            if new_word && !result.is_empty() {
59                // Capitalize the first letter of each new word (except the first word)
60                result.extend(c.to_uppercase());
61            } else {
62                // Lowercase the first letter of the first word and other letters
63                result.extend(c.to_lowercase());
64            }
65            new_word = false;
66        } else {
67            new_word = true;
68        }
69
70        // Treat numbers as their own "words" to start a new word afterward
71        if c.is_numeric() {
72            new_word = true;
73        }
74
75        // Handle transitions from lowercase to uppercase (e.g., PascalCase)
76        if i + 1 < chars.len() && c.is_lowercase() && chars[i + 1].is_uppercase() {
77            new_word = true;
78        }
79
80        i += 1;
81    }
82
83    result
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn parse_from_title_case() {
92        let value = CamelCaseString::new(String::from("Hello This is a Long Title!"));
93        assert_eq!(value.0, "helloThisIsALongTitle");
94    }
95
96    #[test]
97    fn parse_from_numbers() {
98        let value = CamelCaseString::new(String::from("This123 str1ng has 456n numbers"));
99        assert_eq!(value.0, "this123Str1NgHas456NNumbers");
100    }
101
102    #[test]
103    fn parse_from_snake_case() {
104        let value = CamelCaseString::new(String::from("hello_this_is__a_snake_case"));
105        assert_eq!(value.0, "helloThisIsASnakeCase");
106    }
107
108    #[test]
109    fn parse_from_pascal_case() {
110        let value = CamelCaseString::new(String::from("HelloThisIs7PascalCaseWords"));
111        assert_eq!(value.0, "helloThisIs7PascalCaseWords");
112    }
113
114    #[test]
115    fn parse_from_special_chars() {
116        let value = CamelCaseString::new(String::from("crate::hello:world?*,this+is!a#test"));
117        assert_eq!(value.0, "crateHelloWorldThisIsATest");
118    }
119
120    #[test]
121    fn double_parse() {
122        let value = to_camel_case("my_value");
123        let value = to_camel_case(&value);
124        assert_eq!(value, "myValue");
125    }
126
127    #[test]
128    fn new_from_string() {
129        let value = CamelCaseString::new(String::from("my_value"));
130        assert_eq!(value.0, "myValue");
131    }
132
133    #[test]
134    fn new_from_str() {
135        let value = CamelCaseString::new("my_value");
136        assert_eq!(value.0, "myValue");
137    }
138
139    #[test]
140    fn new_from_self() {
141        let value = CamelCaseString::new(CamelCaseString::new("my_value"));
142        assert_eq!(value.0, "myValue");
143    }
144
145    #[test]
146    fn from_string() {
147        let value: CamelCaseString = String::from("my_value").into();
148        assert_eq!(value.0, "myValue");
149    }
150
151    #[test]
152    fn from_str() {
153        let value: CamelCaseString = "my_value".into();
154        assert_eq!(value.0, "myValue");
155    }
156
157    #[test]
158    fn into_string() {
159        let value: String = CamelCaseString::new("my_value").into();
160        assert_eq!(value, "myValue");
161    }
162
163    #[test]
164    fn deref() {
165        let value = CamelCaseString::new("Hello World!");
166        assert_eq!(*value, "helloWorld");
167    }
168
169    #[test]
170    fn as_ref() {
171        let value = CamelCaseString::new("Hello World!");
172        assert_eq!(value.as_ref(), "helloWorld");
173    }
174
175    #[test]
176    fn to_json() {
177        let value = CamelCaseString::new("helloWorld");
178        let json = serde_json::to_string(&value).unwrap();
179        assert_eq!(json, "\"helloWorld\"");
180    }
181
182    #[test]
183    fn from_json() {
184        let json = "\"helloWorld\"";
185        let value: CamelCaseString = serde_json::from_str(json).unwrap();
186        assert_eq!(value, CamelCaseString::new("helloWorld"));
187    }
188}