use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};
pub fn rust_path_to_tokens(path: &str) -> TokenStream {
debug_assert!(
!path.is_empty(),
"rust_path_to_tokens called with empty path"
);
let (prefix, rest) = if let Some(stripped) = path.strip_prefix("::") {
(quote! { :: }, stripped)
} else {
(TokenStream::new(), path)
};
let segments: Vec<Ident> = rest
.split("::")
.map(|seg| {
if is_rust_keyword(seg) && can_be_raw_ident(seg) {
Ident::new_raw(seg, Span::call_site())
} else {
Ident::new(seg, Span::call_site())
}
})
.collect();
quote! { #prefix #(#segments)::* }
}
pub fn make_field_ident(name: &str) -> Ident {
if is_rust_keyword(name) {
if can_be_raw_ident(name) {
Ident::new_raw(name, Span::call_site())
} else {
format_ident!("{}_", name)
}
} else {
format_ident!("{}", name)
}
}
#[must_use]
pub fn to_upper_camel_case(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::new();
let mut start_of_word = true;
for (i, &ch) in chars.iter().enumerate() {
if ch == '_' {
start_of_word = true;
continue;
}
if !start_of_word && i > 0 {
let prev = chars[i - 1];
let lower_to_upper = prev.is_lowercase() && ch.is_uppercase();
let acronym_end = prev.is_uppercase()
&& ch.is_uppercase()
&& chars.get(i + 1).is_some_and(|c| c.is_lowercase());
if lower_to_upper || acronym_end {
start_of_word = true;
}
}
if start_of_word {
out.extend(ch.to_uppercase());
start_of_word = false;
} else {
out.extend(ch.to_lowercase());
}
}
out
}
#[must_use]
pub fn to_shouty_snake_case(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::new();
for (i, &ch) in chars.iter().enumerate() {
if ch == '_' {
out.push('_');
continue;
}
if i > 0 && ch.is_uppercase() && chars[i - 1] != '_' {
let prev = chars[i - 1];
let prev_starts_word = prev.is_lowercase() || prev.is_ascii_digit();
let acronym_boundary =
prev.is_uppercase() && chars.get(i + 1).is_some_and(|c| c.is_lowercase());
if prev_starts_word || acronym_boundary {
out.push('_');
}
}
out.extend(ch.to_uppercase());
}
out
}
pub fn escape_mod_ident(name: &str) -> String {
if is_rust_keyword(name) {
if can_be_raw_ident(name) {
format!("r#{name}")
} else {
format!("{name}_")
}
} else {
name.to_string()
}
}
pub fn is_rust_keyword(name: &str) -> bool {
matches!(
name,
"as" | "break"
| "const"
| "continue"
| "crate"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "Self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
| "async"
| "await"
| "dyn"
| "gen"
| "abstract"
| "become"
| "box"
| "do"
| "final"
| "macro"
| "override"
| "priv"
| "try"
| "typeof"
| "unsized"
| "virtual"
| "yield"
)
}
fn can_be_raw_ident(name: &str) -> bool {
!matches!(name, "self" | "super" | "Self" | "crate")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rust_path_simple() {
assert_eq!(rust_path_to_tokens("Foo").to_string(), "Foo");
}
#[test]
fn rust_path_nested() {
assert_eq!(
rust_path_to_tokens("foo::bar::Baz").to_string(),
"foo :: bar :: Baz"
);
}
#[test]
fn rust_path_keyword_segment() {
assert_eq!(
rust_path_to_tokens("google::type::LatLng").to_string(),
"google :: r#type :: LatLng"
);
}
#[test]
fn rust_path_absolute() {
assert_eq!(
rust_path_to_tokens("::buffa::Message").to_string(),
":: buffa :: Message"
);
}
#[test]
fn rust_path_super_segment() {
assert_eq!(
rust_path_to_tokens("super::super::Foo").to_string(),
"super :: super :: Foo"
);
}
#[test]
fn field_ident_normal() {
assert_eq!(make_field_ident("foo").to_string(), "foo");
}
#[test]
fn field_ident_keyword() {
assert_eq!(make_field_ident("type").to_string(), "r#type");
}
#[test]
fn field_ident_non_raw_keyword() {
assert_eq!(make_field_ident("self").to_string(), "self_");
assert_eq!(make_field_ident("super").to_string(), "super_");
assert_eq!(make_field_ident("crate").to_string(), "crate_");
assert_eq!(make_field_ident("Self").to_string(), "Self_");
}
#[test]
fn escape_mod_normal() {
assert_eq!(escape_mod_ident("foo"), "foo");
}
#[test]
fn escape_mod_keyword() {
assert_eq!(escape_mod_ident("type"), "r#type");
assert_eq!(escape_mod_ident("async"), "r#async");
}
#[test]
fn escape_mod_non_raw_keyword() {
assert_eq!(escape_mod_ident("self"), "self_");
assert_eq!(escape_mod_ident("super"), "super_");
}
#[test]
fn upper_camel_basic() {
assert_eq!(to_upper_camel_case("RULE_LEVEL_HIGH"), "RuleLevelHigh");
assert_eq!(to_upper_camel_case("UNKNOWN"), "Unknown");
assert_eq!(to_upper_camel_case("low_priority"), "LowPriority");
assert_eq!(to_upper_camel_case("HTTP_SERVER"), "HttpServer");
}
#[test]
fn upper_camel_lossy_collisions() {
assert_eq!(to_upper_camel_case("FOO_BAR"), "FooBar");
assert_eq!(to_upper_camel_case("FOO__BAR"), "FooBar");
assert_eq!(to_upper_camel_case("HTTPServer"), "HttpServer");
assert_eq!(to_upper_camel_case("HTTP_SERVER"), "HttpServer");
}
#[test]
fn upper_camel_mixed_case_input() {
assert_eq!(to_upper_camel_case("MyValue"), "MyValue");
assert_eq!(to_upper_camel_case("fooBar"), "FooBar");
assert_eq!(to_upper_camel_case("Active"), "Active");
}
#[test]
fn upper_camel_digit_and_empty() {
assert_eq!(to_upper_camel_case("2"), "2");
assert_eq!(to_upper_camel_case(""), "");
assert_eq!(to_upper_camel_case("FOO_2"), "Foo2");
}
#[test]
fn upper_camel_keyword_source() {
assert_eq!(to_upper_camel_case("SELF"), "Self");
}
#[test]
fn shouty_snake_basic() {
assert_eq!(to_shouty_snake_case("RuleLevel"), "RULE_LEVEL");
assert_eq!(to_shouty_snake_case("NullValue"), "NULL_VALUE");
assert_eq!(to_shouty_snake_case("Type"), "TYPE");
}
#[test]
fn shouty_snake_acronym() {
assert_eq!(to_shouty_snake_case("HTTPServer"), "HTTP_SERVER");
}
#[test]
fn shouty_snake_already_snakey() {
assert_eq!(to_shouty_snake_case("RULE_LEVEL"), "RULE_LEVEL");
}
#[test]
fn keyword_coverage() {
assert!(is_rust_keyword("type"));
assert!(is_rust_keyword("async"));
assert!(is_rust_keyword("gen")); assert!(is_rust_keyword("yield")); assert!(!is_rust_keyword("foo"));
assert!(!is_rust_keyword("Type")); }
}