use regex::Regex;
use std::collections::HashSet;
pub struct Inflections {
pub(crate) plurals: Vec<(Regex, String)>,
pub(crate) singulars: Vec<(Regex, String)>,
pub(crate) irregulars: Vec<(String, String)>,
pub(crate) uncountables: HashSet<String>,
pub(crate) acronyms: HashSet<String>,
}
impl Inflections {
pub fn new() -> Self {
Self {
plurals: Vec::new(),
singulars: Vec::new(),
irregulars: Vec::new(),
uncountables: HashSet::new(),
acronyms: HashSet::new(),
}
}
pub fn plural(&mut self, pattern: &str, replacement: &str) {
self.plurals.push((
Regex::new(pattern)
.unwrap_or_else(|e| panic!("invalid plural pattern `{pattern}`: {e}")),
replacement.to_string(),
));
}
pub fn singular(&mut self, pattern: &str, replacement: &str) {
self.singulars.push((
Regex::new(pattern)
.unwrap_or_else(|e| panic!("invalid singular pattern `{pattern}`: {e}")),
replacement.to_string(),
));
}
pub fn irregular(&mut self, singular: &str, plural: &str) {
self.irregulars
.push((singular.to_lowercase(), plural.to_lowercase()));
}
pub fn uncountable(&mut self, word: &str) {
self.uncountables.insert(word.to_lowercase());
}
pub fn acronym(&mut self, word: &str) {
self.acronyms.insert(word.to_uppercase());
}
pub fn pluralize(&self, word: &str) -> String {
let lower = word.to_lowercase();
if self.uncountables.contains(&lower) {
return word.to_string();
}
for (singular, plural) in &self.irregulars {
if singular == &lower {
return plural.clone();
}
if plural == &lower {
return plural.clone();
}
}
for (pattern, replacement) in self.plurals.iter().rev() {
if pattern.is_match(&lower) {
return pattern.replace(&lower, replacement.as_str()).to_string();
}
}
word.to_string()
}
pub fn camelize(&self, s: &str) -> String {
s.split('_')
.map(|word| {
let up = word.to_uppercase();
if self.acronyms.contains(&up) {
up
} else {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
})
.collect()
}
pub fn camelize_lower(&self, s: &str) -> String {
let camelized = self.camelize(s);
let mut chars = camelized.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
}
}
pub fn underscore(&self, s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len() + 4);
for (i, &c) in chars.iter().enumerate() {
if c.is_uppercase() && i > 0 {
let prev = chars[i - 1];
let next = chars.get(i + 1);
let next_is_lower = next.is_some_and(|n| n.is_lowercase());
let prev_is_lower = prev.is_lowercase() || prev.is_numeric();
if prev_is_lower || (prev.is_uppercase() && next_is_lower) {
out.push('_');
}
}
out.extend(c.to_lowercase());
}
out.replace('-', "_")
}
pub fn dasherize(&self, s: &str) -> String {
s.replace('_', "-")
}
pub fn humanize(&self, s: &str) -> String {
let s = s.strip_suffix("_id").unwrap_or(s);
let s = s.replace('_', " ");
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
pub fn constantize(&self, s: &str) -> String {
s.replace('-', "_").to_uppercase()
}
pub fn singularize(&self, word: &str) -> String {
let lower = word.to_lowercase();
if self.uncountables.contains(&lower) {
return word.to_string();
}
for (singular, plural) in &self.irregulars {
if plural == &lower {
return singular.clone();
}
if singular == &lower {
return singular.clone();
}
}
for (pattern, replacement) in self.singulars.iter().rev() {
if pattern.is_match(&lower) {
return pattern.replace(&lower, replacement.as_str()).to_string();
}
}
word.to_string()
}
pub fn tableize(&self, s: &str) -> String {
self.pluralize(&self.underscore(s))
}
pub fn classify(&self, s: &str) -> String {
self.camelize(&self.singularize(s))
}
pub fn foreign_key(&self, s: &str) -> String {
format!("{}_id", self.underscore(s))
}
}
impl Default for Inflections {
fn default() -> Self {
crate::inflector::rules::defaults()
}
}