pub(crate) fn type_short_name<T: ?Sized>() -> &'static str {
let name = std::any::type_name::<T>();
name.rsplit("::").next().unwrap_or(name)
}
pub(crate) fn type_stem<T: ?Sized>(suffix: &str) -> &'static str {
let name = type_short_name::<T>();
name.strip_suffix(suffix)
.filter(|stem| !stem.is_empty())
.unwrap_or(name)
}
pub(crate) fn derived_id(kind: &str, name: &str) -> String {
let name: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
c
} else {
'-'
}
})
.collect();
format!("{kind}-{name}")
}
pub(crate) fn sentence_case(name: &str) -> String {
let mut source = name.chars().filter(|c| c.is_alphanumeric());
let words: Vec<String> = kebab_case(name)
.split('-')
.map(|word| {
let original: String = source.by_ref().take(word.chars().count()).collect();
let acronym = original.chars().filter(|c| c.is_alphabetic()).count() > 1
&& !original.chars().any(char::is_lowercase);
if acronym && original.to_lowercase() == word {
original
} else {
word.to_string()
}
})
.collect();
capitalize(&words.join(" "))
}
pub(crate) fn pluralize(label: &str) -> String {
match label.rsplit_once(' ') {
Some((head, last)) => format!("{head} {}", pluralize_word(last)),
None => pluralize_word(label),
}
}
fn pluralize_word(word: &str) -> String {
let lower = word.to_lowercase();
let plural = pluralizer::pluralize(&lower, 2, false);
let acronym = word.chars().filter(|c| c.is_alphabetic()).count() > 1
&& !word.chars().any(char::is_lowercase);
match plural.strip_prefix(lower.as_str()) {
Some(suffix) if acronym => format!("{word}{suffix}"),
_ if acronym => plural.to_uppercase(),
_ if word.starts_with(char::is_uppercase) => capitalize(&plural),
_ => plural,
}
}
pub(crate) fn slug_case(phrase: &str) -> String {
phrase.to_lowercase().replace(' ', "-")
}
pub(crate) fn kebab_case(name: &str) -> String {
use heck::ToKebabCase;
name.to_kebab_case()
}
pub(crate) fn capitalize(s: &str) -> String {
let spaced = s.replace('_', " ");
let mut c = spaced.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
pub(crate) fn normalize_prefix(raw: &str) -> String {
let trimmed = raw.trim().trim_matches('/');
if trimmed.is_empty() {
"/admin".to_string()
} else {
format!("/{trimmed}")
}
}
pub(crate) fn path_prefix(path: &str) -> String {
path.split('/')
.nth(1)
.filter(|segment| !segment.is_empty())
.map_or_else(|| "/admin".to_string(), |segment| format!("/{segment}"))
}
pub(crate) fn route_path(path: &str) -> topcoat::router::PathBuf {
topcoat::router::Path::from_str(path)
.expect("panel route paths are well-formed")
.to_owned()
}
#[cfg(test)]
mod tests;