Skip to main content

synta_codegen/
naming.rs

1//! Identifier naming helpers shared across code generators.
2//!
3//! The Rust codegen (`codegen.rs`) has its own copies that additionally
4//! handle Rust keyword escaping and differ slightly in capitalisation
5//! heuristics — those are intentionally kept separate.
6
7/// Convert an ASN.1 identifier to PascalCase for use as a C type name.
8///
9/// Each segment (separated by `-` or `_`) is capitalised independently.
10/// All-caps segments (every letter character is uppercase) have their
11/// non-first letters lowercased, so `KDC-REQ` becomes `KdcReq` rather than
12/// `KDCREQ`.  Mixed-case segments such as `Kerberos` are left unchanged.
13pub(crate) fn to_pascal_case(s: &str) -> String {
14    s.split(['-', '_']).map(pascal_segment).collect()
15}
16
17/// Capitalise one separator-delimited segment for PascalCase output.
18fn pascal_segment(seg: &str) -> String {
19    if seg.is_empty() {
20        return String::new();
21    }
22    let all_caps = seg.chars().all(|c| !c.is_alphabetic() || c.is_uppercase());
23    let mut out = String::with_capacity(seg.len());
24    for (i, c) in seg.chars().enumerate() {
25        if i == 0 {
26            out.push(c.to_ascii_uppercase());
27        } else if all_caps && c.is_alphabetic() {
28            out.push(c.to_ascii_lowercase());
29        } else {
30            out.push(c);
31        }
32    }
33    out
34}
35
36/// Convert an ASN.1 identifier to snake_case for use as a C function name.
37pub(crate) fn to_snake_case(s: &str) -> String {
38    let mut result = String::new();
39    let mut prev_was_upper = false;
40
41    for (i, c) in s.chars().enumerate() {
42        if c == '-' {
43            result.push('_');
44            prev_was_upper = false;
45        } else if c.is_uppercase() {
46            if i > 0 && !prev_was_upper {
47                result.push('_');
48            }
49            result.push(c.to_ascii_lowercase());
50            prev_was_upper = true;
51        } else {
52            result.push(c);
53            prev_was_upper = false;
54        }
55    }
56
57    result
58}
59
60/// Convert an ASN.1 identifier to SCREAMING_SNAKE_CASE for C enum constants.
61pub(crate) fn to_screaming_snake_case(s: &str) -> String {
62    to_snake_case(s).to_uppercase()
63}
64
65/// Return a filesystem-safe stem for the given module name.
66///
67/// Converts PascalCase or ALLCAPS module names to lowercase `snake_case`,
68/// e.g. `Certificate` → `certificate`, `RFC5280` → `rfc5280`,
69/// `AlgorithmIdentifier` → `algorithm_identifier`.
70///
71/// The stem is suitable for use as a base filename with any extension.
72pub fn module_file_stem(name: &str) -> String {
73    let mut out = String::with_capacity(name.len() + 4);
74    let mut prev_was_upper = true;
75    for c in name.chars() {
76        if c.is_uppercase() {
77            if !prev_was_upper && !out.is_empty() {
78                out.push('_');
79            }
80            out.extend(c.to_lowercase());
81            prev_was_upper = true;
82        } else if c.is_alphanumeric() {
83            out.push(c);
84            prev_was_upper = false;
85        } else {
86            if !out.ends_with('_') && !out.is_empty() {
87                out.push('_');
88            }
89            prev_was_upper = true;
90        }
91    }
92    out.trim_end_matches('_').to_string()
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_to_pascal_case() {
101        assert_eq!(to_pascal_case("test"), "Test");
102        assert_eq!(to_pascal_case("test-name"), "TestName");
103        assert_eq!(to_pascal_case("test_name"), "TestName");
104        assert_eq!(to_pascal_case("myType"), "MyType");
105    }
106
107    #[test]
108    fn test_to_pascal_case_all_caps_segments() {
109        // All-caps segments: each gets first-letter-upper, rest lower
110        assert_eq!(to_pascal_case("KDC-REQ"), "KdcReq");
111        assert_eq!(to_pascal_case("KDC-REQ-BODY"), "KdcReqBody");
112        assert_eq!(to_pascal_case("KRB-ERROR"), "KrbError");
113        assert_eq!(to_pascal_case("AP-REQ"), "ApReq");
114        assert_eq!(to_pascal_case("PA-DATA"), "PaData");
115        assert_eq!(to_pascal_case("TGS-REP"), "TgsRep");
116        assert_eq!(to_pascal_case("KRB-SAFE-BODY"), "KrbSafeBody");
117    }
118
119    #[test]
120    fn test_to_pascal_case_mixed_case_preserved() {
121        // Mixed-case segments (already have lowercase letters) are left as-is
122        assert_eq!(to_pascal_case("KerberosString"), "KerberosString");
123        assert_eq!(to_pascal_case("AlgorithmIdentifier"), "AlgorithmIdentifier");
124        assert_eq!(to_pascal_case("EncKDCRepPart"), "EncKDCRepPart");
125    }
126
127    #[test]
128    fn test_to_pascal_case_digits_in_segment() {
129        // Digits are neutral — they don't affect the all-caps determination
130        assert_eq!(to_pascal_case("ETYPE-INFO2"), "EtypeInfo2");
131        assert_eq!(to_pascal_case("ETYPE-INFO2-ENTRY"), "EtypeInfo2Entry");
132        assert_eq!(to_pascal_case("PA-ENC-TS-ENC"), "PaEncTsEnc");
133    }
134
135    #[test]
136    fn test_to_snake_case() {
137        assert_eq!(to_snake_case("Test"), "test");
138        assert_eq!(to_snake_case("TestName"), "test_name");
139        assert_eq!(to_snake_case("test-name"), "test_name");
140        assert_eq!(to_snake_case("camelCase"), "camel_case");
141    }
142
143    #[test]
144    fn test_to_screaming_snake_case() {
145        assert_eq!(to_screaming_snake_case("Test"), "TEST");
146        assert_eq!(to_screaming_snake_case("TestName"), "TEST_NAME");
147        assert_eq!(to_screaming_snake_case("test-name"), "TEST_NAME");
148    }
149
150    #[test]
151    fn test_stem_pascal_case() {
152        assert_eq!(module_file_stem("Certificate"), "certificate");
153        assert_eq!(
154            module_file_stem("AlgorithmIdentifier"),
155            "algorithm_identifier"
156        );
157    }
158
159    #[test]
160    fn test_stem_all_caps() {
161        assert_eq!(module_file_stem("RFC5280"), "rfc5280");
162    }
163
164    #[test]
165    fn test_stem_single_word() {
166        assert_eq!(module_file_stem("PKIX"), "pkix");
167        assert_eq!(module_file_stem("Kerberos"), "kerberos");
168    }
169}