1pub fn snake_case(input: &str) -> String {
8 let mut out = String::with_capacity(input.len() + 4);
9 for (i, ch) in input.chars().enumerate() {
10 if ch.is_uppercase() {
11 if i != 0 {
12 out.push('_');
13 }
14 for lower in ch.to_lowercase() {
15 out.push(lower);
16 }
17 } else {
18 out.push(ch);
19 }
20 }
21 out
22}
23
24pub fn pluralize(word: &str) -> String {
26 if word.ends_with('y')
27 && !word.ends_with("ay")
28 && !word.ends_with("ey")
29 && !word.ends_with("oy")
30 && !word.ends_with("uy")
31 {
32 format!("{}ies", &word[..word.len() - 1])
33 } else if word.ends_with('s')
34 || word.ends_with("x")
35 || word.ends_with("ch")
36 || word.ends_with("sh")
37 {
38 format!("{word}es")
39 } else {
40 format!("{word}s")
41 }
42}
43
44pub fn table_name(model: &str) -> String {
46 pluralize(&snake_case(model))
47}
48
49pub fn pascal_case(input: &str) -> String {
54 let mut out = String::with_capacity(input.len());
55 for word in input.split('_').filter(|w| !w.is_empty()) {
56 let mut chars = word.chars();
57 if let Some(first) = chars.next() {
58 out.extend(first.to_uppercase());
59 out.push_str(chars.as_str());
60 }
61 }
62 out
63}
64
65pub fn singularize(word: &str) -> String {
71 if let Some(stem) = word.strip_suffix("ies") {
72 return format!("{stem}y");
74 }
75 if let Some(stem) = word.strip_suffix("es") {
76 if stem.ends_with('s')
79 || stem.ends_with('x')
80 || stem.ends_with("ch")
81 || stem.ends_with("sh")
82 {
83 return stem.to_string();
84 }
85 }
86 if let Some(stem) = word.strip_suffix('s') {
87 return stem.to_string();
88 }
89 word.to_string()
90}
91
92pub fn model_name(module: &str) -> String {
95 pascal_case(&singularize(module))
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn snake_cases() {
104 assert_eq!(snake_case("User"), "user");
105 assert_eq!(snake_case("BlogPost"), "blog_post");
106 assert_eq!(snake_case("OAuthToken"), "o_auth_token");
107 }
108
109 #[test]
110 fn pluralizes() {
111 assert_eq!(pluralize("user"), "users");
112 assert_eq!(pluralize("category"), "categories");
113 assert_eq!(pluralize("box"), "boxes");
114 assert_eq!(pluralize("day"), "days");
115 }
116
117 #[test]
118 fn builds_table_names() {
119 assert_eq!(table_name("User"), "users");
120 assert_eq!(table_name("BlogPost"), "blog_posts");
121 assert_eq!(table_name("Category"), "categories");
122 }
123
124 #[test]
125 fn pascal_cases() {
126 assert_eq!(pascal_case("user"), "User");
127 assert_eq!(pascal_case("blog_post"), "BlogPost");
128 assert_eq!(pascal_case("o_auth_token"), "OAuthToken");
129 }
130
131 #[test]
132 fn singularizes() {
133 assert_eq!(singularize("users"), "user");
134 assert_eq!(singularize("categories"), "category");
135 assert_eq!(singularize("boxes"), "box");
136 assert_eq!(singularize("dishes"), "dish");
137 assert_eq!(singularize("days"), "day");
138 assert_eq!(singularize("posts"), "post");
139 }
140
141 #[test]
142 fn model_name_inverts_table_name() {
143 for model in ["User", "BlogPost", "Category", "OAuthToken", "Product"] {
145 assert_eq!(model_name(&table_name(model)), model, "round-trip {model}");
146 }
147 }
148}