Skip to main content

rdbi_codegen/codegen/
naming.rs

1//! Naming utilities for code generation
2
3use heck::{ToPascalCase, ToSnakeCase};
4
5/// Convert a table name to a struct name (PascalCase)
6pub fn to_struct_name(table_name: &str) -> String {
7    table_name.to_pascal_case()
8}
9
10/// Convert a column name to a field name (snake_case)
11pub fn to_field_name(column_name: &str) -> String {
12    column_name.to_snake_case()
13}
14
15/// Generate an enum name for a column's ENUM type
16/// e.g., table "users" + column "status" -> "UsersStatus"
17pub fn to_enum_name(table_name: &str, column_name: &str) -> String {
18    format!(
19        "{}{}",
20        table_name.to_pascal_case(),
21        column_name.to_pascal_case()
22    )
23}
24
25/// Generate a find_by method name for columns
26/// e.g., ["user_id", "device_type"] -> "find_by_user_id_and_device_type"
27pub fn generate_find_by_method_name(columns: &[String]) -> String {
28    let parts: Vec<String> = columns.iter().map(|c| c.to_snake_case()).collect();
29    format!("find_by_{}", parts.join("_and_"))
30}
31
32/// Generate a find_by method name for list parameters (pluralized)
33/// e.g., "status" -> "find_by_statuses"
34/// If singular equals plural (e.g., "published"), adds "_list" suffix
35pub fn generate_find_by_list_method_name(column: &str) -> String {
36    let snake = column.to_snake_case();
37    let plural = pluralize(&snake);
38    if plural == snake {
39        // Word doesn't change in plural form, add "_list" to avoid conflict
40        format!("find_by_{}_list", snake)
41    } else {
42        format!("find_by_{}", plural)
43    }
44}
45
46/// Generate a delete_by method name for columns
47pub fn generate_delete_by_method_name(columns: &[String]) -> String {
48    let parts: Vec<String> = columns.iter().map(|c| c.to_snake_case()).collect();
49    format!("delete_by_{}", parts.join("_and_"))
50}
51
52/// Generate an update_by method name for columns
53pub fn generate_update_by_method_name(columns: &[String]) -> String {
54    let parts: Vec<String> = columns.iter().map(|c| c.to_snake_case()).collect();
55    format!("update_by_{}", parts.join("_and_"))
56}
57
58/// Generate a partial update_by method name for columns
59pub fn generate_update_partial_by_method_name(columns: &[String]) -> String {
60    let parts: Vec<String> = columns.iter().map(|c| c.to_snake_case()).collect();
61    format!("update_partial_by_{}", parts.join("_and_"))
62}
63
64/// Generate a count_by method name for columns
65pub fn generate_count_by_method_name(columns: &[String]) -> String {
66    let parts: Vec<String> = columns.iter().map(|c| c.to_snake_case()).collect();
67    format!("count_by_{}", parts.join("_and_"))
68}
69
70/// Convert an enum value to a Rust variant name
71/// Handles cases like "ACTIVE", "active", "PendingReview", "IN_PROGRESS"
72pub fn to_enum_variant(value: &str) -> String {
73    // Remove quotes if present
74    let value = value.trim_matches('\'').trim_matches('"');
75
76    // Convert to PascalCase
77    value.to_pascal_case()
78}
79
80/// Pluralize a word using English grammar rules
81pub fn pluralize(word: &str) -> String {
82    if word.is_empty() {
83        return word.to_string();
84    }
85
86    // Irregular plurals (common in database contexts)
87    let irregulars: &[(&str, &str)] = &[
88        ("person", "people"),
89        ("child", "children"),
90        ("man", "men"),
91        ("woman", "women"),
92        ("foot", "feet"),
93        ("tooth", "teeth"),
94        ("mouse", "mice"),
95        ("index", "indices"),
96    ];
97
98    for (singular, plural) in irregulars {
99        if word == *singular {
100            return plural.to_string();
101        }
102    }
103
104    // Words ending in -is → -es (analysis → analyses, basis → bases)
105    if word.ends_with("is") && word.len() > 2 {
106        return format!("{}es", &word[..word.len() - 2]);
107    }
108
109    // Words ending in -f or -fe → -ves (leaf → leaves, knife → knives)
110    if let Some(stripped) = word.strip_suffix("fe") {
111        return format!("{}ves", stripped);
112    }
113    let f_to_ves: &[&str] = &[
114        "leaf", "knife", "wife", "life", "shelf", "self", "half", "calf", "loaf", "thief",
115    ];
116    for &fword in f_to_ves {
117        if word == fword {
118            return format!("{}ves", &word[..word.len() - 1]);
119        }
120    }
121
122    // Words ending in -o: some take -es
123    let o_to_oes: &[&str] = &["hero", "potato", "tomato", "echo", "veto"];
124    for &oword in o_to_oes {
125        if word == oword {
126            return format!("{}es", word);
127        }
128    }
129
130    // Words ending in -ed (past participles as adjectives) - don't pluralize naturally
131    // e.g., "published", "deleted", "updated" - keep as is
132    if word.ends_with("ed") && word.len() > 2 {
133        return word.to_string();
134    }
135
136    // Standard rules: -s, -x, -z, -ch, -sh → add -es
137    if word.ends_with("s")
138        || word.ends_with("x")
139        || word.ends_with("z")
140        || word.ends_with("ch")
141        || word.ends_with("sh")
142    {
143        return format!("{}es", word);
144    }
145
146    // Words ending in consonant + y → -ies
147    if word.ends_with("y") && word.len() > 1 {
148        let before_y = word.chars().nth(word.len() - 2).unwrap_or('_');
149        if !"aeiou".contains(before_y) {
150            return format!("{}ies", &word[..word.len() - 1]);
151        }
152    }
153
154    // Default: just add -s
155    format!("{}s", word)
156}
157
158/// Check if a name is a Rust reserved keyword
159pub fn is_rust_keyword(name: &str) -> bool {
160    matches!(
161        name,
162        "as" | "async"
163            | "await"
164            | "break"
165            | "const"
166            | "continue"
167            | "crate"
168            | "dyn"
169            | "else"
170            | "enum"
171            | "extern"
172            | "false"
173            | "fn"
174            | "for"
175            | "if"
176            | "impl"
177            | "in"
178            | "let"
179            | "loop"
180            | "match"
181            | "mod"
182            | "move"
183            | "mut"
184            | "pub"
185            | "ref"
186            | "return"
187            | "self"
188            | "Self"
189            | "static"
190            | "struct"
191            | "super"
192            | "trait"
193            | "true"
194            | "type"
195            | "unsafe"
196            | "use"
197            | "where"
198            | "while"
199            | "abstract"
200            | "become"
201            | "box"
202            | "do"
203            | "final"
204            | "macro"
205            | "override"
206            | "priv"
207            | "try"
208            | "typeof"
209            | "unsized"
210            | "virtual"
211            | "yield"
212    )
213}
214
215/// Escape a field name if it's a Rust keyword
216pub fn escape_field_name(name: &str) -> String {
217    let snake = name.to_snake_case();
218    if is_rust_keyword(&snake) {
219        format!("r#{}", snake)
220    } else {
221        snake
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_to_struct_name() {
231        assert_eq!(to_struct_name("users"), "Users");
232        assert_eq!(to_struct_name("user_settings"), "UserSettings");
233        assert_eq!(to_struct_name("order_items"), "OrderItems");
234    }
235
236    #[test]
237    fn test_to_field_name() {
238        assert_eq!(to_field_name("userId"), "user_id");
239        assert_eq!(to_field_name("first_name"), "first_name");
240        assert_eq!(to_field_name("CreatedAt"), "created_at");
241    }
242
243    #[test]
244    fn test_to_enum_name() {
245        assert_eq!(to_enum_name("users", "status"), "UsersStatus");
246        assert_eq!(
247            to_enum_name("order_items", "payment_type"),
248            "OrderItemsPaymentType"
249        );
250    }
251
252    #[test]
253    fn test_to_enum_variant() {
254        assert_eq!(to_enum_variant("ACTIVE"), "Active");
255        assert_eq!(to_enum_variant("'active'"), "Active");
256        assert_eq!(to_enum_variant("IN_PROGRESS"), "InProgress");
257        assert_eq!(to_enum_variant("PendingReview"), "PendingReview");
258    }
259
260    #[test]
261    fn test_pluralize() {
262        // Basic -s
263        assert_eq!(pluralize("id"), "ids");
264        assert_eq!(pluralize("user"), "users");
265        assert_eq!(pluralize("email"), "emails");
266
267        // -es for -s, -x, -z, -ch, -sh
268        assert_eq!(pluralize("status"), "statuses");
269        assert_eq!(pluralize("box"), "boxes");
270        assert_eq!(pluralize("match"), "matches");
271        assert_eq!(pluralize("dish"), "dishes");
272
273        // -y → -ies (consonant + y)
274        assert_eq!(pluralize("category"), "categories");
275        assert_eq!(pluralize("company"), "companies");
276        // -y → -ys (vowel + y)
277        assert_eq!(pluralize("key"), "keys");
278        assert_eq!(pluralize("day"), "days");
279
280        // -is → -es
281        assert_eq!(pluralize("analysis"), "analyses");
282        assert_eq!(pluralize("basis"), "bases");
283
284        // -f/-fe → -ves
285        assert_eq!(pluralize("leaf"), "leaves");
286        assert_eq!(pluralize("knife"), "knives");
287
288        // Irregulars
289        assert_eq!(pluralize("person"), "people");
290        assert_eq!(pluralize("child"), "children");
291        assert_eq!(pluralize("index"), "indices");
292
293        // -o words
294        assert_eq!(pluralize("hero"), "heroes");
295        assert_eq!(pluralize("photo"), "photos");
296
297        // -ed words (past participles) - don't pluralize
298        assert_eq!(pluralize("published"), "published");
299        assert_eq!(pluralize("deleted"), "deleted");
300        assert_eq!(pluralize("updated"), "updated");
301    }
302
303    #[test]
304    fn test_generate_find_by_method_name() {
305        assert_eq!(
306            generate_find_by_method_name(&["id".to_string()]),
307            "find_by_id"
308        );
309        assert_eq!(
310            generate_find_by_method_name(&["user_id".to_string(), "device_type".to_string()]),
311            "find_by_user_id_and_device_type"
312        );
313    }
314
315    #[test]
316    fn test_escape_field_name() {
317        assert_eq!(escape_field_name("type"), "r#type");
318        assert_eq!(escape_field_name("name"), "name");
319        assert_eq!(escape_field_name("async"), "r#async");
320    }
321}