use heck::{ToLowerCamelCase, ToPascalCase, ToShoutySnakeCase, ToSnakeCase};
use crate::ir::NormalizedName;
pub fn normalize_name(name: &str) -> NormalizedName {
let sanitized = sanitize_identifier(name);
NormalizedName {
original: name.to_string(),
pascal_case: sanitized.to_pascal_case(),
camel_case: sanitized.to_lower_camel_case(),
snake_case: sanitized.to_snake_case(),
screaming_snake: sanitized.to_shouty_snake_case(),
}
}
pub fn route_to_name(method: &str, path: &str) -> String {
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut resource_parts: Vec<String> = Vec::new();
let mut ends_with_param = false;
for seg in &segments {
if seg.starts_with('{') && seg.ends_with('}') {
ends_with_param = true;
} else {
resource_parts.push(seg.to_string());
ends_with_param = false;
}
}
let method_upper = method.to_uppercase();
let prefix = match method_upper.as_str() {
"GET" if ends_with_param => "get",
"GET" => "list",
"POST" => "create",
"PUT" => "update",
"DELETE" => "delete",
"PATCH" => "patch",
"OPTIONS" => "options",
"HEAD" => "head",
"TRACE" => "trace",
other => other,
};
if resource_parts.is_empty() {
return prefix.to_string();
}
let mut pascal_parts = String::new();
for (i, part) in resource_parts.iter().enumerate() {
let is_last = i == resource_parts.len() - 1;
let word = if is_last && ends_with_param {
singularize(part)
} else {
part.to_string()
};
pascal_parts.push_str(&word.to_pascal_case());
}
format!("{prefix}{pascal_parts}")
}
fn singularize(word: &str) -> String {
if word.ends_with("ies") && word.len() > 3 {
format!("{}y", &word[..word.len() - 3])
} else if word.ends_with("sses")
|| word.ends_with("ses")
|| word.ends_with("xes")
|| word.ends_with("zes")
{
word[..word.len() - 2].to_string()
} else if word.ends_with('s') && !word.ends_with("ss") && word.len() > 1 {
word[..word.len() - 1].to_string()
} else {
word.to_string()
}
}
fn sanitize_identifier(name: &str) -> String {
let mut result = String::with_capacity(name.len());
let mut prev_was_separator = false;
for (i, ch) in name.chars().enumerate() {
if ch.is_alphanumeric() {
if i == 0 && ch.is_ascii_digit() {
result.push('_');
}
if prev_was_separator && !result.is_empty() {
result.push('_');
}
result.push(ch);
prev_was_separator = false;
} else {
prev_was_separator = true;
}
}
if result.is_empty() {
return "unnamed".to_string();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_name() {
let n = normalize_name("listModels");
assert_eq!(n.pascal_case, "ListModels");
assert_eq!(n.camel_case, "listModels");
assert_eq!(n.snake_case, "list_models");
assert_eq!(n.screaming_snake, "LIST_MODELS");
}
#[test]
fn test_kebab_case() {
let n = normalize_name("pet-store");
assert_eq!(n.pascal_case, "PetStore");
assert_eq!(n.camel_case, "petStore");
}
#[test]
fn test_leading_number() {
let n = normalize_name("3dModel");
assert_eq!(n.pascal_case, "3dModel");
assert_eq!(n.snake_case, "3d_model");
}
#[test]
fn test_special_chars() {
let n = normalize_name("application/json");
assert_eq!(n.pascal_case, "ApplicationJson");
}
#[test]
fn test_route_to_name_list() {
assert_eq!(route_to_name("GET", "/users"), "listUsers");
}
#[test]
fn test_route_to_name_create() {
assert_eq!(route_to_name("POST", "/users"), "createUsers");
}
#[test]
fn test_route_to_name_get_single() {
assert_eq!(route_to_name("GET", "/users/{userId}"), "getUser");
}
#[test]
fn test_route_to_name_update() {
assert_eq!(route_to_name("PUT", "/users/{userId}"), "updateUser");
}
#[test]
fn test_route_to_name_delete() {
assert_eq!(route_to_name("DELETE", "/users/{userId}"), "deleteUser");
}
#[test]
fn test_route_to_name_patch() {
assert_eq!(route_to_name("PATCH", "/users/{userId}"), "patchUser");
}
#[test]
fn test_route_to_name_nested() {
assert_eq!(
route_to_name("POST", "/users/{userId}/messages"),
"createUsersMessages"
);
}
#[test]
fn test_route_to_name_nested_get() {
assert_eq!(
route_to_name("GET", "/users/{userId}/messages"),
"listUsersMessages"
);
}
#[test]
fn test_route_to_name_nested_single() {
assert_eq!(
route_to_name("GET", "/users/{userId}/messages/{messageId}"),
"getUsersMessage"
);
}
#[test]
fn test_singularize_glasses() {
assert_eq!(singularize("glasses"), "glass");
}
#[test]
fn test_singularize_addresses() {
assert_eq!(singularize("addresses"), "address");
}
}