use proc_macro2::Ident;
use proc_macro2::Span;
pub(crate) const X_RUST_NAME: &str = "x-rust-name";
mod casing;
pub mod operations;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RustIdent {
text: String,
raw: bool,
}
impl RustIdent {
pub fn logical(&self) -> &str {
return &self.text;
}
pub fn to_token(&self) -> Ident {
let ident = if self.raw {
Ident::new_raw(&self.text, Span::call_site())
} else {
Ident::new(&self.text, Span::call_site())
};
return ident;
}
}
#[derive(Debug, Clone, Copy)]
pub enum Case {
Pascal,
Snake,
ScreamingSnake,
}
pub fn to_ident(name: &str, case: Case) -> RustIdent {
let cased = match case {
Case::Pascal => casing::to_upper_camel_case(name),
Case::Snake => casing::to_snake_case(name),
Case::ScreamingSnake => casing::to_screaming_snake_case(name),
};
let cased = if cased.is_empty() { "Unnamed".to_owned() } else { cased };
let starts_with_digit = cased.chars().next().map(char::is_numeric).unwrap_or(false);
let cased = if starts_with_digit { format!("_{cased}") } else { cased };
match classify_ident(&cased) {
IdentForm::Plain => {
return RustIdent {
text: cased,
raw: false,
};
}
IdentForm::Raw => {
return RustIdent { text: cased, raw: true };
}
IdentForm::Suffix => {
return RustIdent {
text: format!("{cased}_"),
raw: false,
};
}
}
}
pub fn rename_for(wire: &str, ident: &RustIdent) -> Option<String> {
if ident.logical() == wire {
return None;
}
return Some(wire.to_owned());
}
pub fn deconflict_ident(ident: RustIdent, seen: &mut std::collections::HashSet<String>) -> RustIdent {
if seen.insert(ident.logical().to_owned()) {
return ident;
}
let mut suffix: u32 = 2;
loop {
let candidate = to_ident(&format!("{} {suffix}", ident.logical()), Case::Pascal);
if seen.insert(candidate.logical().to_owned()) {
return candidate;
}
suffix += 1;
}
}
enum IdentForm {
Plain,
Raw,
Suffix,
}
fn classify_ident(s: &str) -> IdentForm {
if syn::parse_str::<syn::Ident>(s).is_ok() && !is_edition_2024_keyword(s) {
return IdentForm::Plain;
}
if syn::parse_str::<syn::Ident>(&format!("r#{s}")).is_ok() {
return IdentForm::Raw;
}
return IdentForm::Suffix;
}
fn is_edition_2024_keyword(s: &str) -> bool {
return matches!(s, "gen");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pascal_case_types() {
let cases = [
("ErrorResponse", "ErrorResponse", false),
("payment_form", "PaymentForm", false),
("da", "Da", false),
("PET_SHOP_SIGNUP_REQUEST", "PetShopSignupRequest", false),
];
for (input, expected, raw) in cases {
let ident = to_ident(input, Case::Pascal);
assert_eq!(ident.logical(), expected, "input {input}");
assert_eq!(ident.raw, raw, "input {input}");
}
}
#[test]
fn snake_case_fields_and_keywords() {
let ty = to_ident("type", Case::Snake);
assert_eq!(ty.logical(), "type");
assert!(ty.raw, "`type` should be a raw identifier");
assert_eq!(rename_for("type", &ty), None);
let email = to_ident("customer_email", Case::Snake);
assert_eq!(email.logical(), "customer_email");
assert!(!email.raw);
assert_eq!(rename_for("customer_email", &email), None);
}
#[test]
fn rename_when_casing_differs() {
let ident = to_ident("da", Case::Pascal);
assert_eq!(rename_for("da", &ident), Some("da".to_owned()));
}
#[test]
fn keyword_that_cannot_be_raw_is_suffixed() {
let ident = to_ident("self", Case::Snake);
assert_eq!(ident.logical(), "self_");
assert!(!ident.raw);
}
#[test]
fn edition_2024_keyword_is_raw() {
let ident = to_ident("gen", Case::Snake);
assert_eq!(ident.logical(), "gen");
assert!(ident.raw, "`gen` should be a raw identifier under edition 2024");
}
#[test]
fn deconflict_ident_suffixes_collisions() {
let mut seen = std::collections::HashSet::new();
let first = deconflict_ident(to_ident("in-progress", Case::Pascal), &mut seen);
let second = deconflict_ident(to_ident("inProgress", Case::Pascal), &mut seen);
let third = deconflict_ident(to_ident("In_Progress", Case::Pascal), &mut seen);
assert_eq!(first.logical(), "InProgress");
assert_eq!(second.logical(), "InProgress2");
assert_eq!(third.logical(), "InProgress3");
let other = deconflict_ident(to_ident("done", Case::Pascal), &mut seen);
assert_eq!(other.logical(), "Done");
}
#[test]
fn casing_word_boundaries() {
let snake = [
("CamelCase", "camel_case"),
("XMLHttpRequest", "xml_http_request"),
("FIELD_NAME11", "field_name11"),
(
"this-contains_ ALLKinds OfWord_Boundaries",
"this_contains_all_kinds_of_word_boundaries",
),
];
for (input, expected) in snake {
assert_eq!(casing::to_snake_case(input), expected, "snake {input}");
}
let pascal = [
("CamelCase", "CamelCase"),
("XMLHttpRequest", "XmlHttpRequest"),
("SHOUTY_SNAKE_CASE", "ShoutySnakeCase"),
(
"this-contains_ ALLKinds OfWord_Boundaries",
"ThisContainsAllKindsOfWordBoundaries",
),
];
for (input, expected) in pascal {
assert_eq!(casing::to_upper_camel_case(input), expected, "pascal {input}");
}
}
}