#[must_use]
pub fn split_words(input: &str) -> Vec<String> {
let mut words: Vec<String> = Vec::new();
let mut current = String::new();
let chars: Vec<char> = input.chars().collect();
for idx in 0..chars.len() {
let c = chars[idx];
if c == '_' || c == '-' || c == ' ' || c == '.' || c == '/' || c == '+' || c == ':' {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
continue;
}
let prev = if idx > 0 { Some(chars[idx - 1]) } else { None };
let next = chars.get(idx + 1).copied();
let boundary = c.is_ascii_uppercase()
&& !current.is_empty()
&& (prev.is_some_and(|p| p.is_ascii_lowercase() || p.is_ascii_digit())
|| (prev.is_some_and(|p| p.is_ascii_uppercase())
&& next.is_some_and(|n| n.is_ascii_lowercase())));
if boundary {
words.push(std::mem::take(&mut current));
}
current.push(c.to_ascii_lowercase());
}
if !current.is_empty() {
words.push(current);
}
words
}
#[must_use]
pub fn to_snake_case(input: &str) -> String {
let mut out = String::new();
for word in split_words(input) {
let after_digit_word = out.ends_with(|p: char| p.is_ascii_digit())
&& out.rsplit('_').next().is_some_and(|w| {
w.ends_with(|p: char| p.is_ascii_digit())
&& w.chars().any(|c| c.is_ascii_alphabetic())
});
let digit_led_word = word.starts_with(|p: char| p.is_ascii_digit())
&& word.chars().any(|c| c.is_ascii_alphabetic());
if !out.is_empty() && !after_digit_word && !digit_led_word {
out.push('_');
}
out.push_str(&word);
}
out
}
#[must_use]
pub fn prose_identifier(input: &str) -> String {
let folded: String = input
.chars()
.filter_map(|c| match c {
'\'' | '\u{2019}' => None,
c if c.is_ascii_alphanumeric() => Some(c),
_ => Some(' '),
})
.collect();
sanitize_identifier(&collapse_digit_boundaries(&to_snake_case(&folded)))
}
#[must_use]
pub fn summary_identifier(input: &str) -> String {
let snake = prose_identifier(&input.replace('.', ""));
let is_letter = |word: &str| word.len() == 1 && word.chars().all(|c| c.is_ascii_alphabetic());
let mut words: Vec<String> = Vec::new();
let mut previous_letter = false;
for (index, word) in snake.split('_').enumerate() {
let letter = is_letter(word);
match words.last_mut() {
Some(last) if index > 1 && letter && previous_letter => last.push_str(word),
_ => words.push(word.to_string()),
}
previous_letter = letter;
}
words.join("_")
}
#[must_use]
pub fn to_pascal_case(input: &str) -> String {
split_words(input)
.into_iter()
.map(|w| {
let mut out = String::with_capacity(w.len());
let mut after_digit = false;
for (index, c) in w.chars().enumerate() {
if index == 0 || after_digit && c.is_ascii_alphabetic() {
out.push(c.to_ascii_uppercase());
} else {
out.push(c);
}
after_digit = c.is_ascii_digit();
}
out
})
.collect()
}
#[must_use]
pub fn class_name(schema_key: &str) -> String {
let words: String = schema_key
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' {
c
} else {
' '
}
})
.collect();
let pascal =
to_pascal_case(&numeric_enum_identifier(schema_key).unwrap_or_else(|| words.clone()));
let expanded = pascal
.chars()
.next()
.and_then(|digit| digit.to_digit(10))
.map_or(pascal.clone(), |digit| {
let word = [
"Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine",
][digit as usize];
format!("{word}{}", &pascal[1..])
});
sanitize_identifier(&expanded)
}
#[must_use]
pub fn param_class_name(param: &str) -> String {
class_name(param.trim_start_matches(|c: char| !c.is_ascii_alphanumeric() && c != '_'))
}
#[must_use]
pub fn child_class_name(parent: &str, segment: &str) -> String {
to_pascal_case(&format!("{parent}{}", class_name(segment)))
}
#[must_use]
pub fn module_name(class_name: &str) -> String {
let name = to_snake_case(class_name);
if name.starts_with(|c: char| c.is_ascii_digit()) {
format!("_{name}")
} else if is_reserved(&name) {
format!("{name}_")
} else {
name
}
}
#[must_use]
pub fn field_name(wire_name: &str) -> String {
let snake = field_snake_case(wire_name);
if snake.starts_with(|c: char| c.is_ascii_digit()) {
format!("f_{snake}")
} else if is_reserved(&snake) {
format!("{snake}_")
} else {
snake
}
}
#[must_use]
pub fn request_field_name(wire_name: &str) -> String {
let snake = field_snake_case(wire_name);
if snake.starts_with(|c: char| c.is_ascii_digit()) {
format!("_{snake}")
} else {
field_name(wire_name)
}
}
fn field_snake_case(wire_name: &str) -> String {
wire_name
.split_whitespace()
.map(|chunk| collapse_field_digit_boundaries(&to_snake_case(&fold_non_identifier(chunk))))
.filter(|chunk| !chunk.is_empty())
.collect::<Vec<String>>()
.join("_")
}
fn collapse_field_digit_boundaries(name: &str) -> String {
let chars: Vec<char> = name.chars().collect();
chars
.iter()
.enumerate()
.filter_map(|(index, &ch)| {
if ch == '_'
&& (index.checked_sub(1).is_some_and(|i| {
chars[i].is_ascii_digit() && digit_suffix_absorbs_following(&chars, i)
}) || chars.get(index + 1).is_some_and(char::is_ascii_digit))
{
None
} else {
Some(ch)
}
})
.collect()
}
fn digit_suffix_absorbs_following(chars: &[char], digit_index: usize) -> bool {
let mut digit_start = digit_index;
while digit_start > 0 && chars[digit_start - 1].is_ascii_digit() {
digit_start -= 1;
}
(digit_start > 0 && chars[digit_start - 1] == '_')
|| chars[..digit_start]
.iter()
.rev()
.take_while(|&&candidate| candidate != '_')
.any(|candidate| candidate.is_ascii_alphabetic())
}
fn collapse_digit_boundaries(name: &str) -> String {
let chars: Vec<char> = name.chars().collect();
chars
.iter()
.enumerate()
.filter_map(|(index, &ch)| {
if ch == '_'
&& (index
.checked_sub(1)
.is_some_and(|i| chars[i].is_ascii_digit())
|| chars.get(index + 1).is_some_and(char::is_ascii_digit))
{
None
} else {
Some(ch)
}
})
.collect()
}
fn fold_non_identifier(name: &str) -> String {
name.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { ' ' })
.collect()
}
#[must_use]
pub fn sanitize_identifier(name: &str) -> String {
let mut out: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect();
if out.starts_with(|c: char| c.is_ascii_digit()) {
out.insert(0, '_');
}
out
}
fn digit_word(word: &str) -> Option<&'static str> {
let mut chars = word.chars();
let (first, rest) = (chars.next()?, chars.next());
if rest.is_some() {
return None;
}
Some(match first {
'0' => "zero",
'1' => "one",
'2' => "two",
'3' => "three",
'4' => "four",
'5' => "five",
'6' => "six",
'7' => "seven",
'8' => "eight",
'9' => "nine",
_ => return None,
})
}
fn whole_value_enum_words(value: &str) -> Option<&'static str> {
Some(match value {
"<" => "less_than",
">" => "greater_than",
">=" => "greater_than_or_equal_to",
"<=" => "less_than_or_equal_to",
"!=" => "not_equals",
"=" | "==" => "equal_to",
"*" => "all",
"\"\"" => "empty_string",
"-" => "hyphen",
"|" => "pipe",
"." => "dot",
"/" => "slash",
_ if value.eq_ignore_ascii_case("n/a") => "not_applicable",
_ => return None,
})
}
fn enum_words(value: &str) -> String {
if value.is_empty() {
return "empty".to_string();
}
if let Some(words) = whole_value_enum_words(value) {
return words.to_string();
}
if value.len() > 1 && value.starts_with('0') && value.bytes().all(|byte| byte.is_ascii_digit())
{
let number = value.trim_start_matches('0');
if let Some(words) = numeric_enum_identifier(if number.is_empty() { "0" } else { number }) {
return words;
}
}
let mut spaced = String::new();
for c in value.chars() {
if c == '\'' || c == '\u{2019}' {
} else if c.is_ascii_alphanumeric() {
spaced.push(c);
} else {
spaced.push(' ');
}
}
let mut words = split_words(&spaced);
let value_leads_with_digit = value.starts_with(|c: char| c.is_ascii_digit());
let leads_with_zero_led_digits = !value_leads_with_digit
&& value.contains('_')
&& words.first().is_some_and(|word| {
word.len() > 1
&& word.starts_with('0')
&& word.bytes().all(|byte| byte.is_ascii_digit())
});
if let Some(first) = words.first_mut() {
let digits = first.bytes().take_while(u8::is_ascii_digit).count();
if digits > 0 {
let suffix = first[digits..].to_string();
let run = &first[..digits];
let number = if value_leads_with_digit {
let significant = run.trim_start_matches('0');
numeric_enum_identifier(if significant.is_empty() {
"0"
} else {
significant
})
.or_else(|| (value.len() > digits).then(|| "undefined".to_string()))
} else {
numeric_enum_identifier(run)
};
if let Some(number) = number {
*first = if suffix.is_empty() {
number
} else {
format!("{number}_{suffix}")
};
}
}
}
let mut merged_words: Vec<String> = Vec::with_capacity(words.len());
let mut single_letter_run = false;
for word in words {
let is_single_letter =
word.len() == 1 && word.bytes().all(|byte| byte.is_ascii_alphabetic());
if is_single_letter && single_letter_run {
merged_words
.last_mut()
.expect("a single-letter run has a previous word")
.push_str(&word);
} else {
merged_words.push(word);
}
single_letter_run = is_single_letter;
}
let mut words = merged_words;
let mut index = 1usize;
while index < words.len() {
let previous = &words[index - 1];
let current = &words[index];
let current_is_short_letter_run =
current.len() <= 2 && current.bytes().all(|byte| byte.is_ascii_alphabetic());
let previous_is_numeric_letter =
previous
.as_bytes()
.split_last()
.is_some_and(|(last, prefix)| {
last.is_ascii_alphabetic()
&& !prefix.is_empty()
&& prefix.iter().all(u8::is_ascii_digit)
});
let previous_is_single_letter =
previous.len() == 1 && previous.bytes().all(|byte| byte.is_ascii_alphabetic());
let current_is_letter_digits =
current
.as_bytes()
.split_first()
.is_some_and(|(first, rest)| {
first.is_ascii_alphabetic()
&& !rest.is_empty()
&& rest.iter().all(u8::is_ascii_digit)
});
if current_is_short_letter_run && previous_is_numeric_letter
|| previous_is_single_letter && current_is_letter_digits
{
let current = words.remove(index);
words[index - 1].push_str(¤t);
} else {
index += 1;
}
}
let identifier = words.join("_");
if leads_with_zero_led_digits {
identifier
} else {
collapse_digit_boundaries(&identifier)
}
}
fn enum_identifier(value: &str) -> String {
let value = &deburr(value);
if is_uuid(value) {
uuid_enum_identifier(value)
} else {
enum_words(value)
}
}
fn deburr(value: &str) -> String {
let mut folded = String::with_capacity(value.len());
for character in value.chars() {
match deburr_letter(character) {
Some(ascii) => folded.push_str(ascii),
None => folded.push(character),
}
}
folded
}
fn deburr_letter(character: char) -> Option<&'static str> {
DEBURRED_LATIN
.binary_search_by(|(candidate, _)| candidate.cmp(&character))
.ok()
.map(|index| DEBURRED_LATIN[index].1)
}
const DEBURRED_LATIN: &[(char, &str)] = &[
('À', "A"),
('Á', "A"),
('Â', "A"),
('Ã', "A"),
('Ä', "A"),
('Å', "A"),
('Æ', "Ae"),
('Ç', "C"),
('È', "E"),
('É', "E"),
('Ê', "E"),
('Ë', "E"),
('Ì', "I"),
('Í', "I"),
('Î', "I"),
('Ï', "I"),
('Ð', "D"),
('Ñ', "N"),
('Ò', "O"),
('Ó', "O"),
('Ô', "O"),
('Õ', "O"),
('Ö', "O"),
('Ø', "O"),
('Ù', "U"),
('Ú', "U"),
('Û', "U"),
('Ü', "U"),
('Ý', "Y"),
('Þ', "Th"),
('ß', "ss"),
('à', "a"),
('á', "a"),
('â', "a"),
('ã', "a"),
('ä', "a"),
('å', "a"),
('æ', "ae"),
('ç', "c"),
('è', "e"),
('é', "e"),
('ê', "e"),
('ë', "e"),
('ì', "i"),
('í', "i"),
('î', "i"),
('ï', "i"),
('ð', "d"),
('ñ', "n"),
('ò', "o"),
('ó', "o"),
('ô', "o"),
('õ', "o"),
('ö', "o"),
('ø', "o"),
('ù', "u"),
('ú', "u"),
('û', "u"),
('ü', "u"),
('ý', "y"),
('þ', "th"),
('ÿ', "y"),
('Ā', "A"),
('ā', "a"),
('Ă', "A"),
('ă', "a"),
('Ą', "A"),
('ą', "a"),
('Ć', "C"),
('ć', "c"),
('Ĉ', "C"),
('ĉ', "c"),
('Ċ', "C"),
('ċ', "c"),
('Č', "C"),
('č', "c"),
('Ď', "D"),
('ď', "d"),
('Đ', "D"),
('đ', "d"),
('Ē', "E"),
('ē', "e"),
('Ĕ', "E"),
('ĕ', "e"),
('Ė', "E"),
('ė', "e"),
('Ę', "E"),
('ę', "e"),
('Ě', "E"),
('ě', "e"),
('Ĝ', "G"),
('ĝ', "g"),
('Ğ', "G"),
('ğ', "g"),
('Ġ', "G"),
('ġ', "g"),
('Ģ', "G"),
('ģ', "g"),
('Ĥ', "H"),
('ĥ', "h"),
('Ħ', "H"),
('ħ', "h"),
('Ĩ', "I"),
('ĩ', "i"),
('Ī', "I"),
('ī', "i"),
('Ĭ', "I"),
('ĭ', "i"),
('Į', "I"),
('į', "i"),
('İ', "I"),
('ı', "i"),
('IJ', "IJ"),
('ij', "ij"),
('Ĵ', "J"),
('ĵ', "j"),
('Ķ', "K"),
('ķ', "k"),
('ĸ', "k"),
('Ĺ', "L"),
('ĺ', "l"),
('Ļ', "L"),
('ļ', "l"),
('Ľ', "L"),
('ľ', "l"),
('Ŀ', "L"),
('ŀ', "l"),
('Ł', "L"),
('ł', "l"),
('Ń', "N"),
('ń', "n"),
('Ņ', "N"),
('ņ', "n"),
('Ň', "N"),
('ň', "n"),
('ʼn', "'n"),
('Ŋ', "N"),
('ŋ', "n"),
('Ō', "O"),
('ō', "o"),
('Ŏ', "O"),
('ŏ', "o"),
('Ő', "O"),
('ő', "o"),
('Œ', "Oe"),
('œ', "oe"),
('Ŕ', "R"),
('ŕ', "r"),
('Ŗ', "R"),
('ŗ', "r"),
('Ř', "R"),
('ř', "r"),
('Ś', "S"),
('ś', "s"),
('Ŝ', "S"),
('ŝ', "s"),
('Ş', "S"),
('ş', "s"),
('Š', "S"),
('š', "s"),
('Ţ', "T"),
('ţ', "t"),
('Ť', "T"),
('ť', "t"),
('Ŧ', "T"),
('ŧ', "t"),
('Ũ', "U"),
('ũ', "u"),
('Ū', "U"),
('ū', "u"),
('Ŭ', "U"),
('ŭ', "u"),
('Ů', "U"),
('ů', "u"),
('Ű', "U"),
('ű', "u"),
('Ų', "U"),
('ų', "u"),
('Ŵ', "W"),
('ŵ', "w"),
('Ŷ', "Y"),
('ŷ', "y"),
('Ÿ', "Y"),
('Ź', "Z"),
('ź', "z"),
('Ż', "Z"),
('ż', "z"),
('Ž', "Z"),
('ž', "z"),
('ſ', "s"),
];
fn numeric_enum_identifier(value: &str) -> Option<String> {
if value.is_empty()
|| (value.len() > 1 && value.starts_with('0'))
|| !value.bytes().all(|byte| byte.is_ascii_digit())
{
return None;
}
let value: u16 = value.parse().ok()?;
if value > 9_999 {
return None;
}
const SMALL: [&str; 20] = [
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
"thirteen",
"fourteen",
"fifteen",
"sixteen",
"seventeen",
"eighteen",
"nineteen",
];
const TENS: [&str; 10] = [
"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
];
fn below_hundred(value: u16) -> String {
if value < 20 {
return SMALL[usize::from(value)].to_string();
}
let mut words = TENS[usize::from(value / 10)].to_string();
if !value.is_multiple_of(10) {
words.push('_');
words.push_str(SMALL[usize::from(value % 10)]);
}
words
}
fn below_thousand(value: u16) -> String {
if value < 100 {
return below_hundred(value);
}
let mut words = format!("{}_hundred", SMALL[usize::from(value / 100)]);
if !value.is_multiple_of(100) {
words.push('_');
words.push_str(&below_hundred(value % 100));
}
words
}
if value < 1_000 {
return Some(below_thousand(value));
}
let mut words = format!("{}_thousand", SMALL[usize::from(value / 1_000)]);
if !value.is_multiple_of(1_000) {
words.push('_');
words.push_str(&below_thousand(value % 1_000));
}
Some(words)
}
fn is_uuid(value: &str) -> bool {
let parts: Vec<&str> = value.split('-').collect();
parts.len() == 5
&& parts.iter().zip([8, 4, 4, 4, 12]).all(|(part, len)| {
part.len() == len && part.bytes().all(|byte| byte.is_ascii_hexdigit())
})
}
fn uuid_enum_identifier(value: &str) -> String {
let mut words = split_words(value);
if let Some(first) = words.first_mut() {
let digit_len = first.bytes().take_while(u8::is_ascii_digit).count();
if digit_len > 0 {
let suffix = first[digit_len..].to_string();
let numeric = &first[..digit_len];
*first = digit_word(numeric).unwrap_or("undefined").to_string();
if !suffix.is_empty() {
words.insert(1, suffix);
}
}
}
let collapsed = collapse_digit_boundaries(&words.join("_"));
let chars: Vec<char> = collapsed.chars().collect();
chars
.iter()
.enumerate()
.filter_map(|(index, &ch)| {
let numeric_single_letter_bridge = ch == '_'
&& index >= 2
&& index + 2 < chars.len()
&& chars[index - 2].is_ascii_digit()
&& chars[index - 1].is_ascii_alphabetic()
&& chars[index + 1].is_ascii_alphabetic()
&& chars[index + 2].is_ascii_digit();
(!numeric_single_letter_bridge).then_some(ch)
})
.collect()
}
fn finalize_enum_ident(mut name: String) -> String {
if name.is_empty() {
name.push('_');
}
if name.starts_with(|c: char| c.is_ascii_digit()) {
name.insert(0, '_');
}
if is_reserved(&name) {
name.push('_');
}
name
}
#[must_use]
pub fn enum_member_name(value: &str) -> String {
finalize_enum_ident(enum_identifier(value).to_ascii_uppercase())
}
#[must_use]
pub fn enum_visit_param(value: &str) -> String {
let mut name = finalize_enum_ident(enum_identifier(value));
if name == "self" || name == "kwargs" {
name.push('_');
}
name
}
#[must_use]
pub fn is_reserved(name: &str) -> bool {
const RESERVED_BUILTINS: &[&str] = &[
"all", "bool", "complex", "float", "int", "list", "long", "map", "set", "uuid",
];
PYTHON_KEYWORDS.contains(&name) || RESERVED_BUILTINS.contains(&name)
}
#[must_use]
pub fn model_field_name(wire_name: &str) -> String {
let name = field_name(wire_name);
if matches!(
name.as_str(),
"construct" | "copy" | "json" | "kwargs" | "schema" | "self" | "validate"
) {
format!("{name}_")
} else {
name
}
}
#[must_use]
pub fn is_reserved_method(name: &str) -> bool {
is_reserved(name) && !matches!(name, "list" | "set")
}
#[must_use]
pub fn escape_python_keyword(mut name: String) -> String {
if PYTHON_KEYWORDS.contains(&name.as_str()) {
name.push('_');
}
name
}
#[must_use]
pub fn is_python_keyword(name: &str) -> bool {
PYTHON_KEYWORDS.contains(&name)
}
const PYTHON_KEYWORDS: &[&str] = &[
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue",
"def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",
"in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while",
"with", "yield",
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn method_reserved_set_is_narrower_than_general() {
assert!(is_reserved_method("all"));
assert!(is_reserved_method("import"));
assert!(is_reserved_method("bool"));
assert!(!is_reserved_method("list"));
assert!(!is_reserved_method("set"));
assert!(!is_reserved_method("dict"));
assert!(is_reserved("list"));
assert!(is_reserved("bool"));
assert!(is_reserved("all"));
}
#[test]
fn complex_is_reserved_in_every_naming_context() {
assert_eq!(module_name("Complex"), "complex_");
assert_eq!(field_name("complex"), "complex_");
assert!(is_reserved_method("complex"));
for name in ["str", "range", "dict", "type", "id", "object", "format"] {
assert_eq!(module_name(&to_pascal_case(name)), name);
assert_eq!(field_name(name), name);
assert!(!is_reserved_method(name));
}
}
#[test]
fn method_keyword_escape_leaves_builtins_unchanged() {
assert_eq!(escape_python_keyword("del".to_string()), "del_");
assert_eq!(escape_python_keyword("list".to_string()), "list");
assert_eq!(escape_python_keyword("all".to_string()), "all");
}
#[test]
fn snake_case_variants() {
assert_eq!(to_snake_case("ParcelNotice"), "parcel_notice");
assert_eq!(to_snake_case("SearchResponse"), "search_response");
assert_eq!(to_snake_case("name"), "name");
assert_eq!(to_snake_case("customFields"), "custom_fields");
assert_eq!(to_snake_case("custom_fields"), "custom_fields");
assert_eq!(to_snake_case("HTTPServer"), "http_server");
assert_eq!(to_snake_case("kebab-case-name"), "kebab_case_name");
}
#[test]
fn pascal_case_is_idempotent_on_pascal() {
assert_eq!(to_pascal_case("ParcelNotice"), "ParcelNotice");
assert_eq!(to_pascal_case("parcel_notice"), "ParcelNotice");
assert_eq!(to_pascal_case("search_response"), "SearchResponse");
}
#[test]
fn module_name_snakes_class() {
assert_eq!(module_name("ParcelNotice"), "parcel_notice");
assert_eq!(module_name("Class"), "class_");
assert_eq!(module_name("_5GmmCause"), "_5_gmm_cause");
}
#[test]
fn a_colon_separates_the_words_of_a_class_name() {
assert_eq!(class_name("osdb:output_type"), "OsdbOutputType");
assert_eq!(
split_words("osdb:output_type"),
vec!["osdb", "output", "type"]
);
}
#[test]
fn digit_leading_schema_names_get_legal_class_names() {
assert_eq!(class_name("5GmmCause"), "FiveGmmCause");
assert_eq!(class_name("Widget"), "Widget");
assert_eq!(class_name("400"), "FourHundred");
assert_eq!(class_name("422"), "FourHundredTwentyTwo");
}
#[test]
fn reserved_fields_get_suffixed() {
assert_eq!(field_name("list"), "list_");
assert_eq!(field_name("bool"), "bool_");
assert_eq!(field_name("uuid"), "uuid_");
assert_eq!(field_name("name"), "name");
assert_eq!(field_name("tags"), "tags");
assert_eq!(model_field_name("self"), "self_");
assert_eq!(model_field_name("construct"), "construct_");
assert_eq!(field_name("construct"), "construct");
}
#[test]
fn bracketed_property_names_fold_to_snake_identifiers() {
assert_eq!(field_name("filter[name]"), "filter_name");
assert_eq!(field_name("filter[color]"), "filter_color");
assert_eq!(field_name("page[size]"), "page_size");
assert_eq!(field_name("hours[day]"), "hours_day");
assert_eq!(field_name("filter[user][name]"), "filter_user_name");
assert_eq!(field_name("[2fa]enabled"), "f_2fa_enabled");
}
#[test]
fn digit_leading_fields_get_f_prefix() {
assert_eq!(field_name("2fa_enabled"), "f_2fa_enabled");
assert_eq!(field_name("3d"), "f_3d");
assert_eq!(field_name("2-factor"), "f_2_factor");
assert_eq!(field_name("v2"), "v2");
}
#[test]
fn numeric_field_boundaries_match_fern_without_changing_adjacent_word_rules() {
assert_eq!(field_name("address_line_1"), "address_line1");
assert_eq!(field_name("Cvc2Create"), "cvc2create");
assert_eq!(field_name("prop65_warning"), "prop65warning");
assert_eq!(field_name("prop_65_warning"), "prop65warning");
assert_eq!(field_name("2Factor"), "f_2_factor");
}
#[test]
fn whitespace_is_a_hard_boundary_for_field_digit_collapse() {
assert_eq!(field_name("EAN 1"), "ean_1");
assert_eq!(field_name("PF 0%"), "pf_0");
assert_eq!(field_name("CONFAZ 87"), "confaz_87");
assert_eq!(field_name("PF 17% ALC"), "pf_17_alc");
assert_eq!(field_name("PF 17,5%"), "pf_175");
assert_eq!(field_name("PF 17,5% ALC"), "pf_175_alc");
assert_eq!(field_name("PF 17 % ALC"), "pf_17_alc");
assert_eq!(field_name(" "), "");
assert_eq!(field_name("SUBSTÂNCIA"), "subst_ncia");
assert_eq!(field_name("COMERCIALIZAÇÃO 2019"), "comercializa_o_2019");
assert_eq!(
field_name("LISTA DE CONCESSÃO DE CRÉDITO TRIBUTÁRIO (PIS/COFINS)"),
"lista_de_concess_o_de_cr_dito_tribut_rio_pis_cofins"
);
assert_eq!(field_name("day_0_end_time"), "day0end_time");
assert_eq!(field_name("user_fields[1]"), "user_fields1");
assert_eq!(request_field_name("user_fields[1]"), "user_fields1");
assert_eq!(request_field_name("EAN 1"), "ean_1");
}
#[test]
fn enum_member_names_fold_latin_accents_the_property_path_drops() {
assert_eq!(enum_member_name("SUBSTÂNCIA"), "SUBSTANCIA");
assert_eq!(field_name("SUBSTÂNCIA"), "subst_ncia");
assert_eq!(enum_member_name("LABORATÓRIO"), "LABORATORIO");
assert_eq!(enum_member_name("CÓDIGO GGREM"), "CODIGO_GGREM");
assert_eq!(enum_member_name("APRESENTAÇÃO"), "APRESENTACAO");
assert_eq!(enum_member_name("CLASSE TERAPÊUTICA"), "CLASSE_TERAPEUTICA");
assert_eq!(enum_member_name("REGIME DE PREÇO"), "REGIME_DE_PRECO");
assert_eq!(
enum_member_name("COMERCIALIZAÇÃO 2019"),
"COMERCIALIZACAO2019"
);
assert_eq!(enum_visit_param("ANÁLISE RECURSAL"), "analise_recursal");
}
#[test]
fn the_latin_folding_table_is_sorted_and_spans_both_blocks() {
assert!(
DEBURRED_LATIN.windows(2).all(|pair| pair[0].0 < pair[1].0),
"DEBURRED_LATIN must stay sorted for the binary search in deburr_letter"
);
assert!(
DEBURRED_LATIN
.iter()
.all(|(accented, ascii)| !accented.is_ascii() && ascii.is_ascii()),
"every entry folds one non-ASCII letter to an ASCII spelling"
);
assert_eq!(deburr_letter('Ä'), Some("A"));
assert_eq!(deburr_letter('ß'), Some("ss"));
assert_eq!(deburr_letter('Æ'), Some("Ae"));
assert_eq!(deburr_letter('ʼn'), Some("'n"));
assert_eq!(deburr_letter('ž'), Some("z"));
assert_eq!(deburr_letter('×'), None);
assert_eq!(deburr_letter('a'), None);
assert_eq!(deburr_letter('気'), None);
assert_eq!(deburr("Ærøskøbing 気温"), "Aeroskobing 気温");
assert_eq!(enum_member_name("気温"), "_");
}
#[test]
fn digit_leading_request_fields_get_underscore_prefix() {
assert_eq!(request_field_name("5gMmCauseValue"), "_5g_mm_cause_value");
}
#[test]
fn zero_led_digit_runs_collapse_unless_they_lead_the_name() {
assert_eq!(
enum_member_name("ALIGN_PERCENTILE_05"),
"ALIGN_PERCENTILE05"
);
assert_eq!(
enum_member_name("ALIGN_PERCENTILE_99"),
"ALIGN_PERCENTILE99"
);
assert_eq!(
enum_visit_param("ALIGN_PERCENTILE_05"),
"align_percentile05"
);
assert_eq!(enum_member_name("_01_00_AM"), "_01_00_AM");
assert_eq!(enum_member_name("01_00_AM"), "ONE00AM");
assert_eq!(enum_visit_param("01_00_AM"), "one00am");
assert_eq!(enum_member_name("01"), "ONE");
assert_eq!(enum_visit_param("09"), "nine");
assert_eq!(enum_member_name("00"), "ZERO");
assert_eq!(enum_member_name("10001+"), "UNDEFINED");
assert_eq!(enum_visit_param("10001+"), "undefined");
assert_eq!(enum_member_name("u.s. virgin islands"), "US_VIRGIN_ISLANDS");
}
#[test]
fn enum_names_match_ferns_sanitized_identifiers() {
assert_eq!(enum_member_name("global"), "GLOBAL");
assert_eq!(enum_visit_param("global"), "global_");
assert_eq!(enum_member_name("practice"), "PRACTICE");
assert_eq!(enum_visit_param("practice"), "practice");
assert_eq!(enum_member_name("0: Active"), "ZERO_ACTIVE");
assert_eq!(enum_visit_param("0: Active"), "zero_active");
assert_eq!(enum_member_name("1: InActive"), "ONE_IN_ACTIVE");
assert_eq!(enum_member_name("oauth2.0"), "OAUTH20");
assert_eq!(enum_member_name("ID_CUSIP_8_CHR"), "ID_CUSIP8CHR");
assert_eq!(enum_visit_param("ID_CUSIP_8_CHR"), "id_cusip8chr");
assert_eq!(enum_member_name("SCHEDULE5MANUAL"), "SCHEDULE5MANUAL");
assert_eq!(enum_visit_param("SCHEDULE5MANUAL"), "schedule5manual");
assert_eq!(enum_member_name("200"), "TWO_HUNDRED");
assert_eq!(enum_visit_param("201"), "two_hundred_one");
assert_eq!(enum_member_name("1200 bps"), "ONE_THOUSAND_TWO_HUNDRED_BPS");
assert_eq!(enum_member_name("19.2 kbps"), "NINETEEN2KBPS");
assert_eq!(enum_member_name("DB-25"), "DB25");
assert_eq!(enum_member_name("SFP+ (10GE)"), "SFP10GE");
assert_eq!(enum_member_name("IEEE 802.11a"), "IEEE80211A");
assert_eq!(enum_member_name("IEEE 802.11b/g"), "IEEE80211BG");
assert_eq!(enum_member_name("10gbase-x-x2"), "TEN_GBASE_XX2");
assert_eq!(enum_member_name("P+N+E 4H"), "PNE4H");
assert_eq!(enum_member_name("2P+E 4H"), "TWO_PE4H");
assert_eq!(
enum_member_name("ITA Type E/F (CEE 7/7)"),
"ITA_TYPE_EF_CEE77"
);
assert_eq!(enum_member_name("iec-60309-3p-n-e-4h"), "IEC603093PNE4H");
assert_eq!(
enum_member_name("100BASE-FX (10/100ME FIBER)"),
"ONE_HUNDRED_BASE_FX10100ME_FIBER"
);
assert_eq!(
enum_member_name("fbf35668-96a0-4baa-bcde-ab18d6b1b329"),
"FBF3566896A04BAA_BCDE_AB18D6B1B329"
);
assert_eq!(
enum_member_name("6a9dfcad-600b-46c8-9e08-ce6e5057921e"),
"SIX_A9DFCAD600B46C89E08CE6E5057921E"
);
assert_eq!(
enum_member_name("98777886-76d0-44c8-865e-bb40e669e934"),
"UNDEFINED76D044C8865E_BB40E669E934"
);
assert_eq!(
enum_member_name("ac5b9c1e-dc78-466d-b0b3-7cf712967a48"),
"AC5B9C1E_DC78466DB0B37CF712967A48"
);
assert_eq!(
enum_visit_param("ac5b9c1e-dc78-466d-b0b3-7cf712967a48"),
"ac5b9c1e_dc78466db0b37cf712967a48"
);
assert_eq!(enum_member_name(""), "EMPTY");
assert_eq!(enum_visit_param(""), "empty");
assert_eq!(enum_visit_param("1: InActive"), "one_in_active");
}
#[test]
fn enum_receiver_value_escapes_the_visit_param() {
assert_eq!(enum_member_name("self"), "SELF");
assert_eq!(enum_visit_param("self"), "self_");
assert_eq!(enum_visit_param("Self"), "self_");
assert_eq!(enum_member_name("cls"), "CLS");
assert_eq!(enum_visit_param("cls"), "cls");
assert_eq!(enum_member_name("kwargs"), "KWARGS");
assert_eq!(enum_visit_param("kwargs"), "kwargs_");
assert_eq!(enum_visit_param("json"), "json");
assert_eq!(enum_visit_param("copy"), "copy");
}
#[test]
fn spelled_number_tables_cover_every_digit_and_stop_at_five_figures() {
for (digit, word) in [
("0", "zero"),
("1", "one"),
("2", "two"),
("3", "three"),
("4", "four"),
("5", "five"),
("6", "six"),
("7", "seven"),
("8", "eight"),
("9", "nine"),
] {
assert_eq!(digit_word(digit), Some(word));
}
assert_eq!(digit_word("12"), None);
assert_eq!(digit_word("x"), None);
assert_eq!(digit_word(""), None);
assert_eq!(enum_member_name("40"), "FORTY");
assert_eq!(enum_member_name("45"), "FORTY_FIVE");
assert_eq!(enum_member_name("19"), "NINETEEN");
assert_eq!(enum_member_name("999"), "NINE_HUNDRED_NINETY_NINE");
assert_eq!(
enum_member_name("9999"),
"NINE_THOUSAND_NINE_HUNDRED_NINETY_NINE"
);
assert_eq!(enum_member_name("10000"), "_10000");
assert_eq!(enum_member_name("*"), "ALL");
assert_eq!(enum_visit_param("*"), "all_");
assert_eq!(enum_member_name("image/*"), "IMAGE");
for (value, name) in [
("<", "LESS_THAN"),
(">", "GREATER_THAN"),
(">=", "GREATER_THAN_OR_EQUAL_TO"),
("<=", "LESS_THAN_OR_EQUAL_TO"),
("!=", "NOT_EQUALS"),
("=", "EQUAL_TO"),
("==", "EQUAL_TO"),
("\"\"", "EMPTY_STRING"),
("-", "HYPHEN"),
("|", "PIPE"),
(".", "DOT"),
("/", "SLASH"),
("n/a", "NOT_APPLICABLE"),
("N/A", "NOT_APPLICABLE"),
] {
assert_eq!(enum_member_name(value), name, "{value}");
}
assert_eq!(enum_visit_param("/"), "slash");
}
#[test]
fn enum_names_stay_legal_where_fern_would_error() {
assert_eq!(enum_member_name("_01_00_AM"), "_01_00_AM");
assert_eq!(enum_visit_param("_01_00_AM"), "_01_00_am");
assert_eq!(enum_member_name("2fa"), "TWO_FA");
assert_eq!(enum_visit_param("2fa"), "two_fa");
assert_eq!(enum_member_name("!!!"), "_");
assert_eq!(enum_visit_param("!!!"), "_");
assert_eq!(enum_member_name("="), "EQUAL_TO");
assert_eq!(enum_visit_param("="), "equal_to");
}
#[test]
fn class_names_break_words_on_non_identifier_characters() {
assert_eq!(
class_name("{{baseUrl}}/persons/:personId-Request"),
"BaseUrlPersonsPersonIdRequest"
);
assert_eq!(class_name("snake_case.Name"), "SnakeCaseName");
}
#[test]
fn sanitize_identifier_coerces_illegal_chars() {
assert_eq!(sanitize_identifier("get-all-widgets"), "get_all_widgets");
assert_eq!(sanitize_identifier("verify code"), "verify_code");
assert_eq!(sanitize_identifier("2fa"), "_2fa");
assert_eq!(to_pascal_case("ipam_l2vpns_list"), "IpamL2VpnsList");
assert_eq!(
sanitize_identifier("dispatchwithoutbadge"),
"dispatchwithoutbadge"
);
assert_eq!(
prose_identifier("The /content endpoint"),
"the_content_endpoint"
);
assert_eq!(
prose_identifier("Get seller's feedback"),
"get_sellers_feedback"
);
assert_eq!(
prose_identifier("Returns 200 on success or 422 on failure"),
"returns200on_success_or422on_failure"
);
assert_eq!(
sanitize_identifier("handling_manifest"),
"handling_manifest"
);
}
#[test]
fn summary_identifier_joins_one_letter_words_the_way_camel_casing_does() {
assert_eq!(
summary_identifier("load a gRPC proto descriptor set"),
"load_ag_rpc_proto_descriptor_set"
);
assert_eq!(
summary_identifier("retrieve gRPC health serving statuses"),
"retrieve_g_rpc_health_serving_statuses"
);
assert_eq!(summary_identifier("a b c"), "a_bc");
}
#[test]
fn summary_identifier_deletes_full_stops_but_keeps_other_separators() {
assert_eq!(
summary_identifier("Update a software statement certificate with the given CertificateOrKeyId e.g. revoke reason"),
"update_a_software_statement_certificate_with_the_given_certificate_or_key_id_eg_revoke_reason"
);
assert_eq!(
summary_identifier("Mark a conversation read/unread"),
"mark_a_conversation_read_unread"
);
assert_eq!(
summary_identifier("Disable vacation mode. All listings will be re-enabled."),
"disable_vacation_mode_all_listings_will_be_re_enabled"
);
assert_eq!(
prose_identifier("_proxy_openapi.json"),
"proxy_openapi_json"
);
}
}