Skip to main content

uika_codegen/
naming.rs

1// Name conversion utilities for codegen.
2
3/// Convert a PascalCase or UPPER_CASE name to snake_case.
4pub fn to_snake_case(name: &str) -> String {
5    let mut result = String::with_capacity(name.len() + 8);
6    let chars: Vec<char> = name.chars().collect();
7
8    for (i, &ch) in chars.iter().enumerate() {
9        if ch.is_ascii_uppercase() {
10            if i > 0 {
11                let prev = chars[i - 1];
12                // Insert underscore before uppercase if preceded by lowercase/digit,
13                // or if it starts a new word in an acronym (e.g., "HTTPServer" -> "http_server").
14                if prev.is_ascii_lowercase() || prev.is_ascii_digit() {
15                    result.push('_');
16                } else if prev.is_ascii_uppercase()
17                    && i + 1 < chars.len()
18                    && chars[i + 1].is_ascii_lowercase()
19                {
20                    result.push('_');
21                }
22            }
23            result.push(ch.to_ascii_lowercase());
24        } else {
25            result.push(ch);
26        }
27    }
28
29    result
30}
31
32/// Strip the 'b' prefix from boolean property names (e.g., "bNetTemporary" -> "net_temporary").
33pub fn strip_bool_prefix(name: &str) -> String {
34    if name.starts_with('b') && name.len() > 1 && name.as_bytes()[1].is_ascii_uppercase() {
35        to_snake_case(&name[1..])
36    } else {
37        to_snake_case(name)
38    }
39}
40
41const RESERVED_WORDS: &[&str] = &[
42    "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false",
43    "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
44    "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super",
45    "trait", "true", "type", "unsafe", "use", "where", "while", "async",
46    "await", "dyn", "abstract", "become", "box", "do", "final", "macro",
47    "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
48];
49
50/// Check if a name is a Rust reserved word.
51pub fn is_reserved(name: &str) -> bool {
52    RESERVED_WORDS.contains(&name)
53}
54
55/// Escape Rust reserved words by prepending `r#`.
56pub fn escape_reserved(name: &str) -> String {
57    if is_reserved(name) {
58        format!("r#{name}")
59    } else {
60        name.to_string()
61    }
62}
63
64/// Strip the UE prefix from a class name (A for actors, U for objects).
65/// The JSON `name` field typically already has this stripped, but cpp_name doesn't.
66#[allow(dead_code)]
67pub fn strip_ue_prefix(cpp_name: &str) -> &str {
68    if cpp_name.len() > 1 {
69        let first = cpp_name.as_bytes()[0];
70        let second = cpp_name.as_bytes()[1];
71        if (first == b'A' || first == b'U') && second.is_ascii_uppercase() {
72            return &cpp_name[1..];
73        }
74    }
75    cpp_name
76}
77
78/// Strip the F prefix from struct names.
79#[allow(dead_code)]
80pub fn strip_struct_prefix(cpp_name: &str) -> &str {
81    if cpp_name.len() > 1 && cpp_name.as_bytes()[0] == b'F' && cpp_name.as_bytes()[1].is_ascii_uppercase() {
82        &cpp_name[1..]
83    } else {
84        cpp_name
85    }
86}
87
88/// Convert a UE module/package name to a Rust module name.
89#[allow(dead_code)]
90pub fn to_module_name(name: &str) -> String {
91    // Already snake_case or lowercase is common for module names
92    let snake = to_snake_case(name);
93    escape_reserved(&snake)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_to_snake_case() {
102        assert_eq!(to_snake_case("AActor"), "a_actor");
103        assert_eq!(to_snake_case("GetObjectCount"), "get_object_count");
104        assert_eq!(to_snake_case("bNetTemporary"), "b_net_temporary");
105        assert_eq!(to_snake_case("HTTPServer"), "http_server");
106        assert_eq!(to_snake_case("FVector"), "f_vector");
107        assert_eq!(to_snake_case("ECollisionChannel"), "e_collision_channel");
108        assert_eq!(to_snake_case("URL"), "url");
109        assert_eq!(to_snake_case("K2_GetActorLocation"), "k2_get_actor_location");
110    }
111
112    #[test]
113    fn test_strip_bool_prefix() {
114        assert_eq!(strip_bool_prefix("bNetTemporary"), "net_temporary");
115        assert_eq!(strip_bool_prefix("bHidden"), "hidden");
116        assert_eq!(strip_bool_prefix("boolean"), "boolean"); // no strip
117    }
118
119    #[test]
120    fn test_escape_reserved() {
121        assert_eq!(escape_reserved("type"), "r#type");
122        assert_eq!(escape_reserved("r#move"), "r#move"); // already escaped? no — "r#move" is not a keyword
123        assert_eq!(escape_reserved("move"), "r#move");
124        assert_eq!(escape_reserved("actor"), "actor");
125    }
126
127    #[test]
128    fn test_strip_ue_prefix() {
129        assert_eq!(strip_ue_prefix("AActor"), "Actor");
130        assert_eq!(strip_ue_prefix("UObject"), "Object");
131        assert_eq!(strip_ue_prefix("FVector"), "FVector"); // F is not stripped here
132    }
133}