Skip to main content

krabka_protocol_codegen/
name_conv.rs

1//! Convert Kafka schema identifiers (camelCase, `PascalCase`) into idiomatic
2//! Rust identifiers (`snake_case` for fields/modules, `PascalCase` for types),
3//! with reserved-keyword escape and acronym handling.
4
5/// `errorCode` -> `error_code`, `apiKeys` -> `api_keys`,
6/// `ZkMigrationReady` -> `zk_migration_ready`,
7/// `type` -> `type_` (reserved keyword).
8#[must_use]
9pub fn field_name(s: &str) -> String {
10    let mut out = String::with_capacity(s.len() + 2);
11    let bytes = s.as_bytes();
12    for (i, &b) in bytes.iter().enumerate() {
13        if b.is_ascii_uppercase() {
14            let is_first = i == 0;
15            let prev_upper = i
16                .checked_sub(1)
17                .and_then(|prev| bytes.get(prev))
18                .is_some_and(u8::is_ascii_uppercase);
19            let next_lower = bytes.get(i + 1).is_some_and(u8::is_ascii_lowercase);
20            if !is_first && (!prev_upper || next_lower) {
21                out.push('_');
22            }
23            out.push(b.to_ascii_lowercase() as char);
24        } else {
25            out.push(b as char);
26        }
27    }
28    if is_reserved_keyword(&out) {
29        out.push('_');
30    }
31    out
32}
33
34/// `ApiVersionsRequest` -> `api_versions_request` (used for module file names).
35#[must_use]
36pub fn module_name(s: &str) -> String {
37    field_name(s)
38}
39
40/// `ApiVersionsRequest` -> `ApiVersionsRequest` (type name, unchanged).
41/// This function exists for symmetry. It is trivial today, but it is the
42/// single place to change if the rules evolve.
43#[must_use]
44pub fn type_name(s: &str) -> String {
45    s.to_owned()
46}
47
48fn is_reserved_keyword(s: &str) -> bool {
49    matches!(
50        s,
51        "as" | "break"
52            | "const"
53            | "continue"
54            | "crate"
55            | "else"
56            | "enum"
57            | "extern"
58            | "false"
59            | "fn"
60            | "for"
61            | "if"
62            | "impl"
63            | "in"
64            | "let"
65            | "loop"
66            | "match"
67            | "mod"
68            | "move"
69            | "mut"
70            | "pub"
71            | "ref"
72            | "return"
73            | "self"
74            | "Self"
75            | "static"
76            | "struct"
77            | "super"
78            | "trait"
79            | "true"
80            | "type"
81            | "unsafe"
82            | "use"
83            | "where"
84            | "while"
85            | "async"
86            | "await"
87            | "dyn"
88            | "abstract"
89            | "become"
90            | "box"
91            | "do"
92            | "final"
93            | "macro"
94            | "override"
95            | "priv"
96            | "typeof"
97            | "unsized"
98            | "virtual"
99            | "yield"
100            | "try"
101            | "union"
102            | "gen"
103    )
104}
105
106#[cfg(test)]
107mod tests {
108    use assert2::assert;
109
110    use super::*;
111
112    #[test]
113    fn camel_to_snake() {
114        for (input, want) in [
115            ("errorCode", "error_code"),
116            ("apiKeys", "api_keys"),
117            ("aclEntries", "acl_entries"),
118            ("zkMigrationReady", "zk_migration_ready"),
119        ] {
120            assert!(field_name(input) == want);
121        }
122    }
123
124    #[test]
125    fn pascal_to_snake() {
126        assert!(field_name("ZkMigrationReady") == "zk_migration_ready");
127        assert!(field_name("ApiVersionsRequest") == "api_versions_request");
128    }
129
130    #[test]
131    fn acronym_runs_stay_together() {
132        // KafkaClusterID -> kafka_cluster_id (acronym ID at the end)
133        assert!(field_name("KafkaClusterID") == "kafka_cluster_id");
134        // HTTPSEndpoint -> https_endpoint (acronym followed by Title)
135        assert!(field_name("HTTPSEndpoint") == "https_endpoint");
136    }
137
138    #[test]
139    fn reserved_keywords_get_underscore() {
140        for (input, want) in [("type", "type_"), ("Match", "match_"), ("loop", "loop_")] {
141            assert!(field_name(input) == want);
142        }
143    }
144
145    #[test]
146    fn module_name_uses_snake_case() {
147        assert!(module_name("ApiVersionsRequest") == "api_versions_request");
148        assert!(module_name("OffsetCommitResponse") == "offset_commit_response");
149    }
150}