#![cfg_attr(not(test), no_std)]
extern crate alloc;
use alloc::string::String;
mod boundary;
mod case;
mod converter;
mod pattern;
pub use boundary::{split, Boundary};
pub use case::Case;
pub use converter::Converter;
pub use pattern::Pattern;
pub trait Casing<T: AsRef<str>> {
fn to_case(&self, case: Case) -> String;
#[allow(clippy::wrong_self_convention)]
fn from_case(&self, case: Case) -> StateConverter<T>;
fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
fn is_case(&self, case: Case) -> bool;
}
impl<T: AsRef<str>> Casing<T> for T {
fn to_case(&self, case: Case) -> String {
StateConverter::new(self).to_case(case)
}
fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
StateConverter::new(self).set_boundaries(bs)
}
fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
StateConverter::new(self).remove_boundaries(bs)
}
fn from_case(&self, case: Case) -> StateConverter<T> {
StateConverter::new(self).from_case(case)
}
fn is_case(&self, case: Case) -> bool {
self.as_ref() == self.to_case(case).as_str()
}
}
pub struct StateConverter<'a, T: AsRef<str>> {
s: &'a T,
conv: Converter,
}
impl<'a, T: AsRef<str>> StateConverter<'a, T> {
fn new(s: &'a T) -> Self {
Self {
s,
conv: Converter::new(),
}
}
pub fn from_case(self, case: Case) -> Self {
Self {
conv: self.conv.from_case(case),
..self
}
}
pub fn set_boundaries(self, bs: &[Boundary]) -> Self {
Self {
s: self.s,
conv: self.conv.set_boundaries(bs),
}
}
pub fn remove_boundaries(self, bs: &[Boundary]) -> Self {
Self {
s: self.s,
conv: self.conv.remove_boundaries(bs),
}
}
pub fn to_case(self, case: Case) -> String {
self.conv.to_case(case).convert(self.s)
}
}
#[macro_export]
macro_rules! case {
(snake) => {
convert_case::Case::Snake
};
(constant) => {
convert_case::Case::Constant
};
(upper_snake) => {
convert_case::Case::UpperSnake
};
(ada) => {
convert_case::Case::Ada;
};
(kebab) => {
convert_case::Case::Kebab
};
(cobol) => {
convert_case::Case::Cobol
};
(upper_kebab) => {
convert_case::Case::UpperKebab
};
(train) => {
convert_case::Case::Train
};
(flat) => {
convert_case::Case::Flat
};
(upper_flat) => {
convert_case::Case::UpperFlat
};
(pascal) => {
convert_case::Case::Pascal
};
(upper_camel) => {
convert_case::Case::UpperCamel
};
(camel) => {
convert_case::Case::Camel
};
(lower) => {
convert_case::Case::Lower
};
(upper) => {
convert_case::Case::Upper
};
(title) => {
convert_case::Case::Title
};
(sentence) => {
convert_case::Case::Sentence
};
}
#[macro_export]
macro_rules! ccase {
($case:ident, $e:expr) => {
convert_case::Converter::new()
.to_case(convert_case::case!($case))
.convert($e)
};
($from:ident -> $to:ident, $e:expr) => {
convert_case::Converter::new()
.from_case(convert_case::case!($from))
.to_case(convert_case::case!($to))
.convert($e)
};
}
#[cfg(test)]
mod test {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
fn possible_cases(s: &str) -> Vec<Case> {
Case::all_cases()
.iter()
.filter(|&case| s.from_case(*case).to_case(*case) == s)
.map(|c| *c)
.collect()
}
#[test]
fn lossless_against_lossless() {
let examples = vec![
(Case::Snake, "my_variable_22_name"),
(Case::Constant, "MY_VARIABLE_22_NAME"),
(Case::Ada, "My_Variable_22_Name"),
(Case::Kebab, "my-variable-22-name"),
(Case::Cobol, "MY-VARIABLE-22-NAME"),
(Case::Train, "My-Variable-22-Name"),
(Case::Pascal, "MyVariable22Name"),
(Case::Camel, "myVariable22Name"),
(Case::Lower, "my variable 22 name"),
(Case::Upper, "MY VARIABLE 22 NAME"),
(Case::Title, "My Variable 22 Name"),
(Case::Sentence, "My variable 22 name"),
];
for (case_a, str_a) in &examples {
for (case_b, str_b) in &examples {
assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a))
}
}
}
#[test]
fn obvious_default_parsing() {
let examples = vec![
"SuperMario64Game",
"super-mario64-game",
"superMario64 game",
"Super Mario 64_game",
"SUPERMario 64-game",
"super_mario-64 game",
];
for example in examples {
assert_eq!("super_mario_64_game", example.to_case(Case::Snake));
}
}
#[test]
fn multiline_strings() {
assert_eq!("One\ntwo\nthree", "one\ntwo\nthree".to_case(Case::Title));
}
#[test]
fn camel_case_acroynms() {
assert_eq!(
"xml_http_request",
"XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake)
);
assert_eq!(
"xml_http_request",
"XMLHttpRequest"
.from_case(Case::UpperCamel)
.to_case(Case::Snake)
);
assert_eq!(
"xml_http_request",
"XMLHttpRequest"
.from_case(Case::Pascal)
.to_case(Case::Snake)
);
}
#[test]
fn leading_tailing_delimeters() {
assert_eq!(
"_leading_underscore",
"_leading_underscore"
.from_case(Case::Snake)
.to_case(Case::Snake)
);
assert_eq!(
"tailing_underscore_",
"tailing_underscore_"
.from_case(Case::Snake)
.to_case(Case::Snake)
);
assert_eq!(
"_leading_hyphen",
"-leading-hyphen"
.from_case(Case::Kebab)
.to_case(Case::Snake)
);
assert_eq!(
"tailing_hyphen_",
"tailing-hyphen-"
.from_case(Case::Kebab)
.to_case(Case::Snake)
);
assert_eq!(
"tailing_hyphens_____",
"tailing-hyphens-----"
.from_case(Case::Kebab)
.to_case(Case::Snake)
);
assert_eq!(
"tailingHyphens",
"tailing-hyphens-----"
.from_case(Case::Kebab)
.to_case(Case::Camel)
);
}
#[test]
fn double_delimeters() {
assert_eq!(
"many___underscores",
"many___underscores"
.from_case(Case::Snake)
.to_case(Case::Snake)
);
assert_eq!(
"many---underscores",
"many---underscores"
.from_case(Case::Kebab)
.to_case(Case::Kebab)
);
}
#[test]
fn early_word_boundaries() {
assert_eq!(
"a_bagel",
"aBagel".from_case(Case::Camel).to_case(Case::Snake)
);
}
#[test]
fn late_word_boundaries() {
assert_eq!(
"team_a",
"teamA".from_case(Case::Camel).to_case(Case::Snake)
);
}
#[test]
fn empty_string() {
for (case_a, case_b) in Case::all_cases()
.into_iter()
.zip(Case::all_cases().into_iter())
{
assert_eq!("", "".from_case(*case_a).to_case(*case_b));
}
}
#[test]
fn default_all_boundaries() {
assert_eq!(
"abc_abc_abc_abc_abc_abc",
"ABC-abc_abcAbc ABCAbc".to_case(Case::Snake)
);
assert_eq!("8_a_8_a_8", "8a8A8".to_case(Case::Snake));
}
mod is_case {
use super::*;
#[test]
fn snake() {
assert!("im_snake_case".is_case(Case::Snake));
assert!(!"im_NOTsnake_case".is_case(Case::Snake));
}
#[test]
fn kebab() {
assert!("im-kebab-case".is_case(Case::Kebab));
assert!(!"im_not_kebab".is_case(Case::Kebab));
}
#[test]
fn lowercase_word() {
for lower_case in [
Case::Snake,
Case::Kebab,
Case::Flat,
Case::Lower,
Case::Camel,
] {
assert!("lowercase".is_case(lower_case));
}
}
#[test]
fn uppercase_word() {
for upper_case in [Case::Constant, Case::Cobol, Case::UpperFlat, Case::Upper] {
assert!("UPPERCASE".is_case(upper_case));
}
}
#[test]
fn capital_word() {
for capital_case in [
Case::Ada,
Case::Train,
Case::Pascal,
Case::Title,
Case::Sentence,
] {
assert!("Capitalcase".is_case(capital_case));
}
}
#[test]
fn underscores_not_kebab() {
assert!(!"kebab-case".is_case(Case::Snake));
}
#[test]
fn multiple_delimiters() {
assert!(!"kebab-snake_case".is_case(Case::Snake));
assert!(!"kebab-snake_case".is_case(Case::Kebab));
assert!(!"kebab-snake_case".is_case(Case::Lower));
}
#[test]
fn not_a_case() {
for c in Case::all_cases() {
assert!(!"hyphen-and_underscore".is_case(*c));
assert!(!"Sentence-with-hyphens".is_case(*c));
assert!(!"Sentence_with_underscores".is_case(*c));
}
}
}
#[test]
fn remove_boundaries() {
assert_eq!(
"m02_s05_binary_trees.pdf",
"M02S05BinaryTrees.pdf"
.from_case(Case::Pascal)
.remove_boundaries(&[Boundary::UpperDigit])
.to_case(Case::Snake)
);
}
#[test]
fn with_boundaries() {
assert_eq!(
"my-dumb-file-name",
"my_dumbFileName"
.set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
.to_case(Case::Kebab)
);
}
#[test]
fn multiple_from_case() {
assert_eq!(
"longtime_nosee",
"LongTime NoSee"
.from_case(Case::Camel)
.from_case(Case::Title)
.to_case(Case::Snake),
)
}
use std::collections::HashSet;
use std::iter::FromIterator;
#[test]
fn detect_many_cases() {
let lower_cases_vec = possible_cases(&"asef");
let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
let mut actual = HashSet::new();
actual.insert(Case::Lower);
actual.insert(Case::Camel);
actual.insert(Case::Snake);
actual.insert(Case::Kebab);
actual.insert(Case::Flat);
assert_eq!(lower_cases_set, actual);
let lower_cases_vec = possible_cases(&"asefCase");
let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
let mut actual = HashSet::new();
actual.insert(Case::Camel);
assert_eq!(lower_cases_set, actual);
}
#[test]
fn detect_each_case() {
let s = "My String Identifier".to_string();
for &case in Case::all_cases() {
let new_s = s.from_case(case).to_case(case);
let possible = possible_cases(&new_s);
assert!(possible.iter().any(|c| c == &case));
}
}
#[test]
fn accent_mark() {
let s = "música moderna".to_string();
assert_eq!("MúsicaModerna", s.to_case(Case::Pascal));
}
#[test]
fn russian() {
let s = "ПЕРСПЕКТИВА24".to_string();
let _n = s.to_case(Case::Title);
}
#[test]
fn appropriate_associated_boundaries() {
let word_groups = &[
vec!["my", "var", "name"],
vec!["MY", "var", "Name"],
vec!["another", "vAR"],
vec!["XML", "HTTP", "Request"],
];
for words in word_groups {
for case in Case::all_cases() {
if case == &Case::Flat || case == &Case::UpperFlat {
continue;
}
assert_eq!(
case.pattern().mutate(&split(
&case.pattern().mutate(words).join(case.delim()),
case.boundaries()
)),
case.pattern().mutate(words),
"Test boundaries on Case::{:?} with {:?}",
case,
words,
);
}
}
}
}