use heck::{ToPascalCase, ToSnakeCase};
use proc_macro2::{Ident, Span};
const RAW_SAFE_KEYWORDS: &[&str] = &[
"as", "async", "await", "break", "const", "continue", "dyn", "else", "enum", "extern", "false",
"fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
"return", "static", "struct", "trait", "true", "type", "unsafe", "use", "where", "while",
"abstract", "become", "box", "do", "final", "macro", "override", "priv", "try", "typeof",
"unsized", "virtual", "yield",
];
const NON_RAW_KEYWORDS: &[&str] = &["crate", "self", "super", "Self"];
fn is_plain_ident(s: &str) -> bool {
if s.is_empty() || s == "_" {
return false;
}
let mut chars = s.chars();
let first = chars.next().expect("non-empty checked above");
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn safe_ident(converted: &str, original: &str, role: &str) -> Result<Ident, String> {
if converted.is_empty() {
return Err(format!(
"{role} `{original}` produces an empty Rust identifier; rename it"
));
}
if NON_RAW_KEYWORDS.contains(&converted) {
return Err(format!(
"{role} `{original}` maps to the reserved Rust keyword `{converted}`, which cannot be used as an identifier (not even as a raw identifier); rename it"
));
}
if RAW_SAFE_KEYWORDS.contains(&converted) {
return Ok(Ident::new_raw(converted, Span::call_site()));
}
if !is_plain_ident(converted) {
return Err(format!(
"{role} `{original}` produces the invalid Rust identifier `{converted}`; identifiers must start with a letter or underscore and contain only letters, digits, and underscores"
));
}
Ok(Ident::new(converted, Span::call_site()))
}
pub fn method_ident(operation_id: &str) -> Result<Ident, String> {
safe_ident(&operation_id.to_snake_case(), operation_id, "operationId")
}
pub fn field_ident(param_name: &str) -> Result<Ident, String> {
safe_ident(¶m_name.to_snake_case(), param_name, "parameter")
}
pub fn validate_type_base(operation_id: &str) -> Result<(), String> {
let pascal = operation_id.to_pascal_case();
if !is_plain_ident(&pascal) {
return Err(format!(
"operationId `{operation_id}` produces the invalid Rust type-name base `{pascal}`; it must start with a letter or underscore and contain only letters, digits, and underscores"
));
}
Ok(())
}
pub fn type_ident(name: &str, source: &str) -> Result<Ident, String> {
if !is_plain_ident(name) {
return Err(format!(
"synthesized type name `{name}` (from `{source}`) is not a valid Rust identifier"
));
}
Ok(Ident::new(name, Span::call_site()))
}
#[must_use]
pub fn keyword_safe_ident(name: &str) -> Ident {
if NON_RAW_KEYWORDS.contains(&name) {
return Ident::new(&format!("{name}_"), Span::call_site());
}
if RAW_SAFE_KEYWORDS.contains(&name) {
return Ident::new_raw(name, Span::call_site());
}
Ident::new(name, Span::call_site())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keyword_method_becomes_raw_ident() {
let id = method_ident("type").expect("keyword is raw-safe");
assert_eq!(id.to_string(), "r#type");
assert_eq!(method_ident("async").unwrap().to_string(), "r#async");
}
#[test]
fn hyphenated_and_clean_names_pass_through() {
assert_eq!(method_ident("list-pets").unwrap().to_string(), "list_pets");
assert_eq!(
method_ident("getPetById").unwrap().to_string(),
"get_pet_by_id"
);
}
#[test]
fn leading_digit_is_an_error() {
let err = method_ident("1pet").unwrap_err();
assert!(err.contains("operationId `1pet`"), "{err}");
}
#[test]
fn empty_after_conversion_is_an_error() {
assert!(method_ident("___").is_err());
}
#[test]
fn non_raw_keyword_is_an_error() {
let err = method_ident("self").unwrap_err();
assert!(err.contains("reserved Rust keyword"), "{err}");
}
#[test]
fn keyword_param_field_becomes_raw_ident() {
assert_eq!(field_ident("type").unwrap().to_string(), "r#type");
}
#[test]
fn type_base_keyword_is_fine_but_digit_is_not() {
assert!(validate_type_base("type").is_ok());
assert!(validate_type_base("1pet").is_err());
}
#[test]
fn infallible_helper_handles_non_raw_keyword() {
assert_eq!(keyword_safe_ident("self").to_string(), "self_");
assert_eq!(keyword_safe_ident("type").to_string(), "r#type");
assert_eq!(keyword_safe_ident("name").to_string(), "name");
}
}