use openusd::tf;
const RESERVED: &[&str] = &[
"alignas",
"alignof",
"and",
"and_eq",
"asm",
"atomic_cancel",
"atomic_commit",
"atomic_noexcept",
"auto",
"bitand",
"bitor",
"bool",
"break",
"case",
"catch",
"char",
"char8_t",
"char16_t",
"char32_t",
"class",
"compl",
"concept",
"const",
"consteval",
"constexpr",
"constinit",
"const_cast",
"continue",
"co_await",
"co_return",
"co_yield",
"decltype",
"default",
"delete",
"do",
"double",
"dynamic_cast",
"else",
"enum",
"explicit",
"export",
"extern",
"false",
"float",
"for",
"friend",
"goto",
"if",
"inline",
"int",
"long",
"mutable",
"namespace",
"new",
"noexcept",
"not",
"not_eq",
"nullptr",
"operator",
"or",
"or_eq",
"private",
"protected",
"public",
"reflexpr",
"register",
"reinterpret_cast",
"requires",
"return",
"short",
"signed",
"sizeof",
"static",
"static_assert",
"static_cast",
"struct",
"switch",
"synchronized",
"template",
"this",
"thread_local",
"throw",
"true",
"try",
"typedef",
"typeid",
"typename",
"union",
"unsigned",
"using",
"virtual",
"void",
"volatile",
"wchar_t",
"while",
"xor",
"xor_eq",
"False",
"None",
"True",
"as",
"assert",
"async",
"await",
"def",
"del",
"elif",
"except",
"finally",
"from",
"global",
"import",
"in",
"is",
"lambda",
"nonlocal",
"pass",
"raise",
"with",
"yield",
"interface",
];
pub fn token_id(name: &str, literal_identifiers: bool) -> String {
let mut id = valid_token(name, literal_identifiers);
if RESERVED.contains(&id.as_str()) {
id.push('_');
}
id
}
pub fn method_name(name: &str) -> String {
let mut method = snake_case(name);
if KEYWORDS.contains(&method.as_str()) {
method.push('_');
}
method
}
const KEYWORDS: &[&str] = &[
"abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate", "do", "dyn", "else",
"enum", "extern", "false", "final", "fn", "for", "gen", "if", "impl", "in", "let", "loop", "macro", "match", "mod",
"move", "mut", "override", "priv", "pub", "ref", "return", "self", "static", "struct", "super", "trait", "true",
"try", "type", "typeof", "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
];
pub fn valid_token(name: &str, literal_identifiers: bool) -> String {
let mut id = if name.starts_with(|c: char| c.is_ascii_digit()) {
format!("_{name}")
} else {
name.to_owned()
};
if !literal_identifiers || id.contains(':') {
id = camel_case(&id);
}
if !tf::is_valid_identifier(&id) {
id = valid_identifier(&id);
}
id
}
pub fn proper_case(name: &str) -> String {
if name.chars().count() <= 1 {
return name.to_uppercase();
}
let mut out = String::with_capacity(name.len());
for word in name.split(|c: char| !c.is_alphanumeric() && c != '_') {
let mut chars = word.chars();
if let Some(first) = chars.next() {
out.extend(first.to_uppercase());
out.push_str(chars.as_str());
}
}
out
}
pub fn camel_case(name: &str) -> String {
if name.chars().count() <= 1 {
return name.to_lowercase();
}
let proper = proper_case(name);
let mut chars = proper.chars();
match chars.next() {
Some(first) => first.to_lowercase().chain(chars).collect(),
None => proper,
}
}
pub fn is_rust_identifier(name: &str) -> bool {
tf::is_valid_identifier(name) && name != "_" && !RUST_KEYWORDS.contains(&name)
}
pub fn valid_identifier(name: &str) -> String {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return "_".to_owned();
};
let mut out = String::with_capacity(name.len());
out.push(if is_identifier_start(first) { first } else { '_' });
out.extend(chars.map(|c| if is_identifier_cont(c) { c } else { '_' }));
out
}
pub fn screaming_snake(id: &str) -> String {
snake_case(id).to_ascii_uppercase()
}
pub fn snake_case(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 4);
let mut previous: Option<char> = None;
let mut chars = name.chars().peekable();
while let Some(c) = chars.next() {
if !c.is_alphanumeric() && c != '_' {
if !out.is_empty() && !out.ends_with('_') {
out.push('_');
}
previous = None;
continue;
}
let opens_word = c.is_uppercase()
&& previous.is_some_and(|p| {
p.is_lowercase()
|| p.is_ascii_digit()
|| (p.is_uppercase() && chars.peek().is_some_and(|n| n.is_lowercase()))
});
if opens_word && !out.ends_with('_') {
out.push('_');
}
out.push(c.to_ascii_lowercase());
previous = Some(c);
}
out
}
const RUST_KEYWORDS: &[&str] = &[
"Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate", "do", "dyn",
"else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", "impl", "in", "let", "loop", "macro",
"match", "mod", "move", "mut", "override", "priv", "pub", "ref", "return", "self", "static", "struct", "super",
"trait", "true", "try", "type", "typeof", "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
];
fn is_identifier_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
fn is_identifier_cont(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keywords_are_not_identifiers() {
assert!(tf::is_valid_identifier("type") && !is_rust_identifier("type"));
assert!(tf::is_valid_identifier("_") && !is_rust_identifier("_"));
assert!(is_rust_identifier("Sphere") && is_rust_identifier("type_"));
}
#[test]
fn snake_namespaced() {
assert_eq!(snake_case("shaping:focus"), "shaping_focus");
assert_eq!(snake_case("inputs:shaping:ies:file"), "inputs_shaping_ies_file");
assert_eq!(snake_case("primvars:displayColor"), "primvars_display_color");
}
#[test]
fn proper_case_words() {
assert_eq!(proper_case("myFooBar"), "MyFooBar");
assert_eq!(proper_case("my-foo bar"), "MyFooBar");
assert_eq!(proper_case("a"), "A");
assert_eq!(
proper_case("hw_primvar"),
"Hw_primvar",
"an underscore is a word character"
);
}
#[test]
fn camel_case_words() {
assert_eq!(camel_case("MyFooBar"), "myFooBar");
assert_eq!(camel_case("my:foo:bar"), "myFooBar");
assert_eq!(camel_case("A"), "a");
}
#[test]
fn valid_token_digit() {
assert_eq!(valid_token("1stName", true), "_1stName");
assert_eq!(valid_identifier("1stName"), "_stName", "the raw rule loses the digit");
}
#[test]
fn valid_token_namespaced() {
assert_eq!(valid_token("inputs:file", true), "inputsFile");
assert_eq!(token_id("inputs:file", true), "inputsFile");
}
#[test]
fn token_id_reserved() {
assert_eq!(token_id("default", true), "default_");
assert_eq!(token_id("None", true), "None_");
assert_eq!(token_id("interface", true), "interface_");
assert_eq!(
token_id("match", true),
"match",
"a Rust keyword is left alone: the constant it reaches is MATCH"
);
assert_eq!(
screaming_snake(&token_id("loop", true)),
"LOOP",
"a shipped token keeps the spelling every other generator gives it"
);
}
#[test]
fn token_id_camel() {
assert_eq!(token_id("MyToken", false), "myToken");
assert_eq!(token_id("MyToken", true), "MyToken");
}
#[test]
fn identifier_shape() {
assert_eq!(valid_identifier(""), "_");
}
#[test]
fn screaming_words() {
assert_eq!(screaming_snake("modelDrawMode"), "MODEL_DRAW_MODE");
assert_eq!(screaming_snake("default_"), "DEFAULT_");
assert_eq!(
screaming_snake("collection_MultipleApplyTemplate_ExpansionRule"),
"COLLECTION_MULTIPLE_APPLY_TEMPLATE_EXPANSION_RULE"
);
}
#[test]
fn snake_initialisms() {
assert_eq!(snake_case("faceVertexCounts"), "face_vertex_counts");
assert_eq!(snake_case("dataWindowNDC"), "data_window_ndc");
assert_eq!(snake_case("NDCWindow"), "ndc_window");
assert_eq!(snake_case("localPos0"), "local_pos0");
}
}