use crate::CaseStyleError;
use std::str::FromStr;
pub const LOWER_HYPHEN: CaseStyle = CaseStyle::LowerHyphen;
pub const LOWER_UNDERSCORE: CaseStyle = CaseStyle::LowerUnderscore;
pub const LOWER_CAMEL: CaseStyle = CaseStyle::LowerCamel;
pub const UPPER_CAMEL: CaseStyle = CaseStyle::UpperCamel;
pub const UPPER_UNDERSCORE: CaseStyle = CaseStyle::UpperUnderscore;
const VALUES: [CaseStyle; 5] = [
LOWER_HYPHEN,
LOWER_UNDERSCORE,
LOWER_CAMEL,
UPPER_CAMEL,
UPPER_UNDERSCORE,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CaseStyle {
LowerHyphen,
LowerUnderscore,
LowerCamel,
UpperCamel,
UpperUnderscore,
}
impl CaseStyle {
pub const LOWER_HYPHEN: Self = Self::LowerHyphen;
pub const LOWER_UNDERSCORE: Self = Self::LowerUnderscore;
pub const LOWER_CAMEL: Self = Self::LowerCamel;
pub const UPPER_CAMEL: Self = Self::UpperCamel;
pub const UPPER_UNDERSCORE: Self = Self::UpperUnderscore;
#[inline]
pub const fn values() -> &'static [Self; 5] {
&VALUES
}
pub fn of(name: &str) -> Result<Self, CaseStyleError> {
let normalized_name = name.replace('_', "-").to_ascii_lowercase();
for style in Self::values() {
if style.name() == normalized_name {
return Ok(*style);
}
}
Err(CaseStyleError::new(name))
}
#[inline]
pub const fn name(self) -> &'static str {
match self {
Self::LowerHyphen => "lower-hyphen",
Self::LowerUnderscore => "lower-underscore",
Self::LowerCamel => "lower-camel",
Self::UpperCamel => "upper-camel",
Self::UpperUnderscore => "upper-underscore",
}
}
#[inline]
pub const fn word_separator(self) -> &'static str {
match self {
Self::LowerHyphen => "-",
Self::LowerUnderscore | Self::UpperUnderscore => "_",
Self::LowerCamel | Self::UpperCamel => "",
}
}
pub fn to(self, target: Self, value: &str) -> String {
if value.is_empty() || target == self {
return value.to_string();
}
if let Some(converted) = self.quick_convert(target, value) {
return converted;
}
self.convert_by_words(target, value)
}
pub fn matches(self, value: &str) -> bool {
if value.is_empty() {
return false;
}
match self {
Self::LowerHyphen => {
matches_separated(value, b'-', is_ascii_lower_or_digit)
}
Self::LowerUnderscore => {
matches_separated(value, b'_', is_ascii_lower_or_digit)
}
Self::LowerCamel => matches_camel(value, is_ascii_lower),
Self::UpperCamel => matches_camel(value, is_ascii_upper),
Self::UpperUnderscore => {
matches_separated(value, b'_', is_ascii_upper_or_digit)
}
}
}
fn quick_convert(self, target: Self, value: &str) -> Option<String> {
match (self, target) {
(Self::LowerHyphen, Self::LowerUnderscore) => {
Some(value.replace('-', "_"))
}
(Self::LowerHyphen, Self::UpperUnderscore) => {
Some(value.replace('-', "_").to_ascii_uppercase())
}
(Self::LowerUnderscore, Self::LowerHyphen) => {
Some(value.replace('_', "-"))
}
(Self::LowerUnderscore, Self::UpperUnderscore) => {
Some(value.to_ascii_uppercase())
}
(Self::UpperUnderscore, Self::LowerHyphen) => {
Some(value.replace('_', "-").to_ascii_lowercase())
}
(Self::UpperUnderscore, Self::LowerUnderscore) => {
Some(value.to_ascii_lowercase())
}
_ => None,
}
}
fn convert_by_words(self, target: Self, value: &str) -> String {
let mut out = String::with_capacity(
value.len() + 4 * target.word_separator().len(),
);
let mut word_start = 0;
let mut search_start = 0;
let mut has_boundary = false;
while let Some(boundary) = self.find_boundary(value, search_start) {
if word_start == boundary {
search_start = boundary + self.word_separator().len().max(1);
word_start = search_start.min(value.len());
continue;
}
let word = &value[word_start..boundary];
if word_start == 0 {
out.push_str(&target.normalize_first_word(word));
} else {
out.push_str(&target.normalize_word(word));
}
out.push_str(target.word_separator());
has_boundary = true;
word_start = boundary + self.word_separator().len();
search_start = boundary + self.word_separator().len().max(1);
}
if !has_boundary {
target.normalize_first_word(value)
} else {
let word = &value[word_start..];
out.push_str(&target.normalize_word(word));
out
}
}
fn find_boundary(self, value: &str, start: usize) -> Option<usize> {
match self {
Self::LowerHyphen => find_first_byte(value, start, b'-'),
Self::LowerUnderscore | Self::UpperUnderscore => {
find_first_byte(value, start, b'_')
}
Self::LowerCamel | Self::UpperCamel => {
find_first_camel_case_boundary(value, start)
}
}
}
fn normalize_word(self, word: &str) -> String {
match self {
Self::LowerHyphen | Self::LowerUnderscore => {
word.to_ascii_lowercase()
}
Self::LowerCamel | Self::UpperCamel => {
first_char_only_to_upper(word)
}
Self::UpperUnderscore => word.to_ascii_uppercase(),
}
}
fn normalize_first_word(self, word: &str) -> String {
match self {
Self::LowerCamel => word.to_ascii_lowercase(),
_ => self.normalize_word(word),
}
}
}
impl FromStr for CaseStyle {
type Err = CaseStyleError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::of(s)
}
}
fn find_first_byte(value: &str, start: usize, needle: u8) -> Option<usize> {
value
.as_bytes()
.iter()
.enumerate()
.skip(start)
.find_map(|(index, byte)| (*byte == needle).then_some(index))
}
fn find_first_camel_case_boundary(value: &str, start: usize) -> Option<usize> {
let start = start.max(1);
value
.as_bytes()
.iter()
.enumerate()
.skip(start)
.find_map(|(index, _)| {
is_camel_case_word_boundary(value, index).then_some(index)
})
}
fn is_camel_case_word_boundary(value: &str, index: usize) -> bool {
let bytes = value.as_bytes();
if index == 0 || index >= bytes.len() {
return false;
}
let current_type = char_type(bytes[index]);
if current_type == CharType::Other {
return false;
}
let previous_type = char_type(bytes[index - 1]);
match previous_type {
CharType::Lower => {
current_type == CharType::Upper || current_type == CharType::Digit
}
CharType::Upper => {
if current_type == CharType::Lower {
false
} else if current_type == CharType::Digit {
true
} else if current_type == CharType::Upper {
let next = bytes
.get(index + 1)
.copied()
.map(char_type)
.unwrap_or(CharType::Other);
next == CharType::Lower
} else {
false
}
}
CharType::Digit => current_type == CharType::Upper,
CharType::Other => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CharType {
Upper,
Lower,
Digit,
Other,
}
fn char_type(byte: u8) -> CharType {
if is_ascii_upper(byte) {
CharType::Upper
} else if is_ascii_lower(byte) {
CharType::Lower
} else if is_ascii_digit(byte) {
CharType::Digit
} else {
CharType::Other
}
}
fn first_char_only_to_upper(word: &str) -> String {
let mut chars = word.chars();
let Some(first) = chars.next() else {
return String::new();
};
let mut out = String::with_capacity(word.len());
out.push(first.to_ascii_uppercase());
out.push_str(&chars.as_str().to_ascii_lowercase());
out
}
fn matches_separated(
value: &str,
separator: u8,
is_word_byte: fn(u8) -> bool,
) -> bool {
let bytes = value.as_bytes();
if bytes.first() == Some(&separator) || bytes.last() == Some(&separator) {
return false;
}
let mut last_is_separator = false;
for byte in bytes {
if *byte == separator {
if last_is_separator {
return false;
}
last_is_separator = true;
} else if is_word_byte(*byte) {
last_is_separator = false;
} else {
return false;
}
}
true
}
fn matches_camel(value: &str, is_valid_first: fn(u8) -> bool) -> bool {
let bytes = value.as_bytes();
if !is_valid_first(bytes[0]) {
return false;
}
bytes.iter().skip(1).all(|byte| is_ascii_alnum(*byte))
}
#[inline]
fn is_ascii_lower(byte: u8) -> bool {
byte.is_ascii_lowercase()
}
#[inline]
fn is_ascii_upper(byte: u8) -> bool {
byte.is_ascii_uppercase()
}
#[inline]
fn is_ascii_digit(byte: u8) -> bool {
byte.is_ascii_digit()
}
#[inline]
fn is_ascii_alnum(byte: u8) -> bool {
byte.is_ascii_alphanumeric()
}
#[inline]
fn is_ascii_lower_or_digit(byte: u8) -> bool {
is_ascii_lower(byte) || is_ascii_digit(byte)
}
#[inline]
fn is_ascii_upper_or_digit(byte: u8) -> bool {
is_ascii_upper(byte) || is_ascii_digit(byte)
}