fn to_snake_case(name: &str) -> String {
if name.is_empty() {
return String::new();
}
let mut result = String::with_capacity(name.len() + 4);
let chars: Vec<char> = name.chars().collect();
let mut prev_was_separator = true;
for i in 0..chars.len() {
let ch = chars[i];
if ch == '_' || ch == '-' || ch == ' ' || ch == '.' {
if !prev_was_separator && !result.is_empty() {
result.push('_');
}
prev_was_separator = true;
} else if ch.is_ascii_uppercase() {
if !prev_was_separator && i > 0 {
let prev = chars[i - 1];
let next = chars.get(i + 1);
if prev.is_ascii_lowercase()
|| (prev.is_ascii_uppercase() && next.is_some_and(|&n| n.is_ascii_lowercase()))
{
result.push('_');
}
}
result.push(ch.to_ascii_lowercase());
prev_was_separator = false;
} else {
result.push(ch.to_ascii_lowercase());
prev_was_separator = false;
}
}
result
}
pub fn default_through_table(source_table: &str, field_name: &str) -> String {
format!(
"{}_{}",
source_table.to_lowercase(),
to_snake_case(field_name)
)
}
pub fn default_m2m_columns(source_table: &str, target_table: &str) -> (String, String) {
let source = source_table.to_lowercase();
let target = target_table.to_lowercase();
if source == target {
(format!("from_{}_id", source), format!("to_{}_id", target))
} else {
(format!("{}_id", source), format!("{}_id", target))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn through_table_basic() {
assert_eq!(
default_through_table("auth_user", "groups"),
"auth_user_groups"
);
}
#[test]
fn through_table_lowercases_mixed_case() {
assert_eq!(
default_through_table("Auth_User", "Groups"),
"auth_user_groups"
);
assert_eq!(
default_through_table("AUTH_USER", "GROUPS"),
"auth_user_groups"
);
}
#[test]
fn through_table_self_referential_field() {
assert_eq!(
default_through_table("auth_user", "following"),
"auth_user_following"
);
}
#[test]
fn m2m_columns_basic() {
assert_eq!(
default_m2m_columns("auth_user", "auth_group"),
("auth_user_id".to_string(), "auth_group_id".to_string()),
);
}
#[test]
fn m2m_columns_self_referential_stays_distinct() {
let (from, to) = default_m2m_columns("auth_user", "auth_user");
assert_eq!(from, "from_auth_user_id");
assert_eq!(to, "to_auth_user_id");
assert_ne!(from, to);
}
#[test]
fn m2m_columns_lowercases_inputs() {
assert_eq!(
default_m2m_columns("Auth_User", "Auth_Group"),
("auth_user_id".to_string(), "auth_group_id".to_string()),
);
}
#[cfg(feature = "migrations")]
mod cross_validation {
use super::to_snake_case as orm_copy;
use crate::migrations::autodetector::to_snake_case as autodetector_copy;
#[test]
fn to_snake_case_matches_autodetector() {
let cases = [
"",
"a",
"A",
"User",
"BlogPost",
"HTTPRequest",
"APIKey",
"XMLHTTPRequest",
"already_snake",
"Mixed-Case_Name",
"public.users",
"With Space",
"ALLCAPS",
"camelCase",
"PascalCase",
];
for input in &cases {
assert_eq!(
orm_copy(input),
autodetector_copy(input),
"to_snake_case diverged for {input:?}"
);
}
}
}
}