pub fn to_snake_case(value: &str) -> String {
let mut output = String::new();
for (index, character) in value.chars().enumerate() {
if character.is_uppercase() {
if index > 0 {
output.push('_');
}
for lowercase in character.to_lowercase() {
output.push(lowercase);
}
} else {
output.push(character);
}
}
output
}
pub fn pluralize(value: &str) -> String {
if value.ends_with('s') {
return format!("{value}es");
}
if let Some(stem) = value.strip_suffix('y') {
let preceded_by_vowel =
matches!(stem.chars().next_back(), Some('a' | 'e' | 'i' | 'o' | 'u'));
if !preceded_by_vowel {
return format!("{stem}ies");
}
}
format!("{value}s")
}
pub fn model_route_segment(model_name: &str) -> String {
pluralize(&to_snake_case(model_name))
}
#[cfg(test)]
mod tests {
use super::*;
const CASES: &[(&str, &str, &str)] = &[
("UserGroup", "user_group", "user_groups"),
("User_Group", "user__group", "user__groups"),
("User_group", "user_group", "user_groups"),
("HTTPServer", "h_t_t_p_server", "h_t_t_p_servers"),
("User_", "user_", "user_s"),
("_User", "__user", "__users"),
("User__Group", "user___group", "user___groups"),
("User-Group", "user-_group", "user-_groups"),
("User Group", "user _group", "user _groups"),
("Bus", "bus", "buses"),
("Class", "class", "classes"),
("Category", "category", "categories"),
("WebhookDelivery", "webhook_delivery", "webhook_deliveries"),
("Entry", "entry", "entries"),
("Day", "day", "days"),
("Holiday", "holiday", "holidays"),
("already_snake", "already_snake", "already_snakes"),
];
#[test]
fn to_snake_case_matches_table() {
for (input, expected_snake, _) in CASES {
assert_eq!(
to_snake_case(input),
*expected_snake,
"to_snake_case({input:?})"
);
}
}
#[test]
fn pluralize_matches_table() {
for (_, snake, expected_plural) in CASES {
assert_eq!(pluralize(snake), *expected_plural, "pluralize({snake:?})");
}
}
#[test]
fn model_route_segment_composes_snake_then_pluralize() {
for (input, _, expected_plural) in CASES {
assert_eq!(
model_route_segment(input),
*expected_plural,
"model_route_segment({input:?})"
);
}
}
#[test]
fn user_group_with_underscore_is_not_the_naive_tokenized_form() {
assert_ne!(model_route_segment("User_Group"), "user_groups");
assert_eq!(model_route_segment("User_Group"), "user__groups");
}
#[test]
fn webhook_delivery_pluralizes_to_the_grammatically_correct_form() {
assert_eq!(model_route_segment("WebhookDelivery"), "webhook_deliveries");
assert_ne!(model_route_segment("WebhookDelivery"), "webhook_deliverys");
}
#[test]
fn pluralize_consonant_plus_y_becomes_ies() {
assert_eq!(pluralize("category"), "categories");
assert_eq!(pluralize("delivery"), "deliveries");
}
#[test]
fn pluralize_vowel_plus_y_becomes_plain_s() {
assert_eq!(pluralize("day"), "days");
assert_eq!(pluralize("key"), "keys");
}
#[test]
fn pluralize_trailing_s_becomes_es() {
assert_eq!(pluralize("bus"), "buses");
assert_eq!(pluralize("class"), "classes");
}
#[test]
fn pluralize_plain_word_gets_bare_s() {
assert_eq!(pluralize("customer"), "customers");
assert_eq!(pluralize("order"), "orders");
}
}