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