rkyv_js_codegen/
casing.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum Casing {
23 #[default]
25 Preserve,
26 Camel,
28 Pascal,
30 Snake,
32}
33
34impl Casing {
35 pub fn apply(self, name: &str) -> String {
40 if self == Casing::Preserve {
41 return name.to_string();
42 }
43
44 let trimmed = name.trim_start_matches('_');
45 let prefix = &name[..name.len() - trimmed.len()];
46 let words = split_words(trimmed);
47 if words.is_empty() {
48 return name.to_string();
49 }
50
51 let mut out = String::with_capacity(name.len() + prefix.len());
52 out.push_str(prefix);
53 match self {
54 Casing::Preserve => unreachable!("handled above"),
55 Casing::Camel => {
56 for (index, word) in words.iter().enumerate() {
57 if index == 0 {
58 out.push_str(&word.to_lowercase());
59 } else {
60 push_capitalized(&mut out, word);
61 }
62 }
63 }
64 Casing::Pascal => {
65 for word in &words {
66 push_capitalized(&mut out, word);
67 }
68 }
69 Casing::Snake => {
70 for (index, word) in words.iter().enumerate() {
71 if index > 0 {
72 out.push('_');
73 }
74 out.push_str(&word.to_lowercase());
75 }
76 }
77 }
78 out
79 }
80}
81
82fn push_capitalized(out: &mut String, word: &str) {
83 let mut chars = word.chars();
84 let Some(first) = chars.next() else {
85 return;
86 };
87 out.extend(first.to_uppercase());
88 out.push_str(&chars.as_str().to_lowercase());
89}
90
91fn split_words(name: &str) -> Vec<String> {
96 let chars: Vec<char> = name.chars().collect();
97 let mut words = Vec::new();
98 let mut current = String::new();
99
100 for (index, &ch) in chars.iter().enumerate() {
101 if ch == '_' {
102 if !current.is_empty() {
103 words.push(std::mem::take(&mut current));
104 }
105 continue;
106 }
107 if ch.is_uppercase() && !current.is_empty() {
108 let previous = chars[index - 1];
109 let next_is_lower = chars.get(index + 1).is_some_and(|next| next.is_lowercase());
110 if !previous.is_uppercase() || next_is_lower {
113 words.push(std::mem::take(&mut current));
114 }
115 }
116 current.push(ch);
117 }
118 if !current.is_empty() {
119 words.push(current);
120 }
121 words
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn preserve_is_the_identity() {
130 for name in ["created_at", "createdAt", "HTTPStatus", "_private", "__"] {
131 assert_eq!(Casing::Preserve.apply(name), name);
132 }
133 }
134
135 #[test]
136 fn camel_case_conversion() {
137 assert_eq!(Casing::Camel.apply("created_at"), "createdAt");
138 assert_eq!(Casing::Camel.apply("id"), "id");
139 assert_eq!(Casing::Camel.apply("a_b_c"), "aBC");
140 assert_eq!(Casing::Camel.apply("already_Mixed"), "alreadyMixed");
141 assert_eq!(Casing::Camel.apply("createdAt"), "createdAt");
142 assert_eq!(Casing::Camel.apply("CreatedAt"), "createdAt");
143 }
144
145 #[test]
146 fn pascal_case_conversion() {
147 assert_eq!(Casing::Pascal.apply("created_at"), "CreatedAt");
148 assert_eq!(Casing::Pascal.apply("createdAt"), "CreatedAt");
149 assert_eq!(Casing::Pascal.apply("CreatedAt"), "CreatedAt");
150 }
151
152 #[test]
153 fn snake_case_conversion() {
154 assert_eq!(Casing::Snake.apply("createdAt"), "created_at");
155 assert_eq!(Casing::Snake.apply("CreatedAt"), "created_at");
156 assert_eq!(Casing::Snake.apply("created_at"), "created_at");
157 }
158
159 #[test]
160 fn acronyms_stay_whole() {
161 assert_eq!(Casing::Camel.apply("HTTP_status"), "httpStatus");
162 assert_eq!(Casing::Camel.apply("HTTPStatus"), "httpStatus");
163 assert_eq!(Casing::Camel.apply("user_ID"), "userId");
164 assert_eq!(Casing::Snake.apply("HTTPStatus"), "http_status");
165 assert_eq!(Casing::Pascal.apply("http_status"), "HttpStatus");
166 }
167
168 #[test]
169 fn digits_join_the_preceding_word() {
170 assert_eq!(Casing::Camel.apply("field_2"), "field2");
171 assert_eq!(Casing::Camel.apply("x2_y3"), "x2Y3");
172 assert_eq!(Casing::Snake.apply("field2"), "field2");
173 }
174
175 #[test]
176 fn leading_underscores_are_preserved() {
177 assert_eq!(Casing::Camel.apply("_private_field"), "_privateField");
178 assert_eq!(Casing::Snake.apply("__internalValue"), "__internal_value");
179 assert_eq!(Casing::Camel.apply("_"), "_");
181 assert_eq!(Casing::Camel.apply("___"), "___");
182 }
183
184 #[test]
185 fn trailing_underscores_are_dropped() {
186 assert_eq!(Casing::Camel.apply("type_"), "type");
189 assert_eq!(Casing::Snake.apply("type_"), "type");
190 }
191}