pub fn to_snake_case(camel: &str) -> String {
let chars: Vec<char> = camel.chars().collect();
let mut out = String::with_capacity(camel.len() + 4);
for (i, &c) in chars.iter().enumerate() {
if c.is_ascii_uppercase() {
let prev = if i == 0 { None } else { Some(chars[i - 1]) };
let next = chars.get(i + 1).copied();
let prev_lower_or_digit =
matches!(prev, Some(p) if p.is_ascii_lowercase() || p.is_ascii_digit());
let run_break = prev.map(|p| p.is_ascii_uppercase()).unwrap_or(false)
&& matches!(next, Some(n) if n.is_ascii_lowercase());
if i != 0 && (prev_lower_or_digit || run_break) {
out.push('_');
}
out.push(c.to_ascii_lowercase());
} else {
out.push(c);
}
}
out
}
pub fn pascal_case_from_table(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut upper_next = true;
for ch in input.chars() {
if ch == '_' || ch == ' ' || ch == '-' {
upper_next = true;
continue;
}
if !ch.is_alphanumeric() {
continue;
}
if upper_next {
for u in ch.to_uppercase() {
out.push(u);
}
upper_next = false;
} else {
for l in ch.to_lowercase() {
out.push(l);
}
}
}
out
}
pub fn pascal_case_from_ident(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut next_upper = true;
for c in name.chars() {
if c == '-' || c == '_' {
next_upper = true;
} else if next_upper {
out.push(c.to_ascii_uppercase());
next_upper = false;
} else {
out.push(c);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snake_simple_pascal() {
assert_eq!(to_snake_case("BlogPost"), "blog_post");
assert_eq!(to_snake_case("UserProfile"), "user_profile");
assert_eq!(to_snake_case("Comment"), "comment");
}
#[test]
fn snake_acronym_handling() {
assert_eq!(to_snake_case("HTTPRequest"), "http_request");
assert_eq!(to_snake_case("URLParser"), "url_parser");
assert_eq!(to_snake_case("JSONBody"), "json_body");
assert_eq!(to_snake_case("APIKey"), "api_key");
}
#[test]
fn snake_trailing_acronym() {
assert_eq!(to_snake_case("ParseHTTP"), "parse_http");
assert_eq!(to_snake_case("RequestURL"), "request_url");
}
#[test]
fn snake_digits() {
assert_eq!(to_snake_case("Post2"), "post2");
assert_eq!(to_snake_case("S3Upload"), "s3_upload");
assert_eq!(to_snake_case("post2Tag"), "post2_tag");
}
#[test]
fn snake_already_snake() {
assert_eq!(to_snake_case("post_tag"), "post_tag");
assert_eq!(to_snake_case("blog_post"), "blog_post");
}
#[test]
fn snake_leading_underscore() {
assert_eq!(to_snake_case("_Hidden"), "_hidden");
}
#[test]
fn snake_single_char() {
assert_eq!(to_snake_case("A"), "a");
assert_eq!(to_snake_case("a"), "a");
}
#[test]
fn snake_empty() {
assert_eq!(to_snake_case(""), "");
}
#[test]
fn table_simple() {
assert_eq!(pascal_case_from_table("post"), "Post");
assert_eq!(pascal_case_from_table("tag"), "Tag");
assert_eq!(pascal_case_from_table("blog_post"), "BlogPost");
assert_eq!(pascal_case_from_table("auth_user_groups"), "AuthUserGroups");
}
#[test]
fn table_normalises_allcaps() {
assert_eq!(pascal_case_from_table("BLOG_POST"), "BlogPost");
assert_eq!(pascal_case_from_table("USER"), "User");
}
#[test]
fn table_handles_hyphen_and_space() {
assert_eq!(pascal_case_from_table("blog-post"), "BlogPost");
assert_eq!(pascal_case_from_table("blog post"), "BlogPost");
}
#[test]
fn table_skips_non_alphanumeric() {
assert_eq!(pascal_case_from_table("foo!bar"), "Foobar");
}
#[test]
fn table_empty() {
assert_eq!(pascal_case_from_table(""), "");
}
#[test]
fn ident_simple() {
assert_eq!(pascal_case_from_ident("posts"), "Posts");
assert_eq!(pascal_case_from_ident("blog_engine"), "BlogEngine");
assert_eq!(pascal_case_from_ident("blog-engine"), "BlogEngine");
assert_eq!(pascal_case_from_ident("task_queue"), "TaskQueue");
}
#[test]
fn ident_digits_preserved() {
assert_eq!(pascal_case_from_ident("api2"), "Api2");
}
#[test]
fn ident_already_pascal() {
assert_eq!(pascal_case_from_ident("BlogPost"), "BlogPost");
assert_eq!(pascal_case_from_ident("UserProfile"), "UserProfile");
}
#[test]
fn ident_empty() {
assert_eq!(pascal_case_from_ident(""), "");
}
#[test]
fn snake_then_table_roundtrip() {
let cases = ["BlogPost", "UserProfile", "AuthGroup", "Comment"];
for &name in &cases {
let snake = to_snake_case(name);
let back = pascal_case_from_table(&snake);
assert_eq!(back, name, "roundtrip failed for {name}");
}
}
}