sqlx_gen/codegen/
naming.rs1const UNCOUNTABLES: &[&str] = &[
12 "data",
13 "news",
14 "equipment",
15 "information",
16 "software",
17 "statistics",
18 "analytics",
19 "series",
20 "species",
21 "means",
22 "audio",
23 "video",
24 "metadata",
25];
26
27fn is_uncountable(word: &str) -> bool {
28 let lower = word.to_ascii_lowercase();
29 UNCOUNTABLES.contains(&lower.as_str())
30}
31
32pub fn singularize_word(word: &str) -> String {
35 if word.is_empty() || is_uncountable(word) {
36 return word.to_string();
37 }
38 let lower = word.to_ascii_lowercase();
39
40 if lower.ends_with("ies") && word.len() > 3 {
42 return format!("{}y", &word[..word.len() - 3]);
43 }
44
45 if lower.ends_with("xes")
47 || lower.ends_with("ches")
48 || lower.ends_with("shes")
49 || lower.ends_with("sses")
50 || lower.ends_with("zes")
51 {
52 return word[..word.len() - 2].to_string();
53 }
54
55 if lower.ends_with("is") || lower.ends_with("us") {
57 return word.to_string();
58 }
59
60 if lower.ends_with('s') && !lower.ends_with("ss") {
62 return word[..word.len() - 1].to_string();
63 }
64
65 word.to_string()
66}
67
68pub fn singularize(name: &str) -> String {
71 if let Some(idx) = name.rfind('_') {
72 let (prefix, last) = name.split_at(idx + 1);
73 return format!("{}{}", prefix, singularize_word(last));
74 }
75 singularize_word(name)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn strips_trailing_s() {
84 assert_eq!(singularize_word("users"), "user");
85 assert_eq!(singularize_word("posts"), "post");
86 assert_eq!(singularize_word("voices"), "voice");
87 }
88
89 #[test]
90 fn handles_ies_to_y() {
91 assert_eq!(singularize_word("categories"), "category");
92 assert_eq!(singularize_word("queries"), "query");
93 assert_eq!(singularize_word("cities"), "city");
94 }
95
96 #[test]
97 fn handles_xes_ches_shes_sses() {
98 assert_eq!(singularize_word("boxes"), "box");
99 assert_eq!(singularize_word("churches"), "church");
100 assert_eq!(singularize_word("dishes"), "dish");
101 assert_eq!(singularize_word("classes"), "class");
102 assert_eq!(singularize_word("addresses"), "address");
103 }
104
105 #[test]
106 fn preserves_uncountables() {
107 assert_eq!(singularize_word("data"), "data");
108 assert_eq!(singularize_word("news"), "news");
109 assert_eq!(singularize_word("series"), "series");
110 assert_eq!(singularize_word("metadata"), "metadata");
111 }
112
113 #[test]
114 fn preserves_latin_endings() {
115 assert_eq!(singularize_word("analysis"), "analysis");
116 assert_eq!(singularize_word("basis"), "basis");
117 assert_eq!(singularize_word("status"), "status");
118 assert_eq!(singularize_word("virus"), "virus");
119 }
120
121 #[test]
122 fn preserves_double_s_words() {
123 assert_eq!(singularize_word("address"), "address");
124 assert_eq!(singularize_word("process"), "process");
125 assert_eq!(singularize_word("class"), "class");
126 }
127
128 #[test]
129 fn preserves_already_singular() {
130 assert_eq!(singularize_word("user"), "user");
131 assert_eq!(singularize_word("category"), "category");
132 assert_eq!(singularize_word("box"), "box");
133 }
134
135 #[test]
136 fn preserves_empty() {
137 assert_eq!(singularize_word(""), "");
138 }
139
140 #[test]
141 fn singularize_snake_case_targets_last_word() {
142 assert_eq!(singularize("user_accounts"), "user_account");
143 assert_eq!(singularize("agent_connector"), "agent_connector");
144 assert_eq!(singularize("audit_logs"), "audit_log");
145 assert_eq!(singularize("category_translations"), "category_translation");
146 }
147
148 #[test]
149 fn singularize_preserves_double_underscore() {
150 assert_eq!(singularize("agent__connector"), "agent__connector");
152 assert_eq!(singularize("agent__connectors"), "agent__connector");
153 }
154
155 #[test]
156 fn singularize_uppercase_unaffected_by_lower_check() {
157 assert_eq!(singularize_word("USERS"), "USER");
159 }
160}