scalp 0.4.3

A declarative parsing library for pretty and highly customizable command-line interfaces.
Documentation
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Case {
    #[default]
    Same,
    Lower,
    Upper,
    Pascal,
    Camel,
    Snake {
        upper: bool,
    },
    Kebab {
        upper: bool,
    },
    Separate {
        separator: char,
        upper: bool,
    },
}

impl Case {
    #[inline]
    pub fn convert<'a>(&self, source: &'a str) -> impl Iterator<Item = char> + 'a {
        use orn::or8::*;
        match self {
            Case::Same => Iterator::T0(source.chars()),
            Case::Lower => Iterator::T1(Self::lower(source)),
            Case::Upper => Iterator::T2(Self::upper(source)),
            Case::Pascal => Iterator::T3(Self::pascal(source)),
            Case::Camel => Iterator::T4(Self::camel(source)),
            Case::Snake { upper } => Iterator::T5(Self::snake(source, *upper)),
            Case::Kebab { upper } => Iterator::T6(Self::kebab(source, *upper)),
            Case::Separate { separator, upper } => {
                Iterator::T7(separated(source, *separator, !*upper))
            }
        }
        .map(Or::into)
    }

    #[inline]
    pub fn upper(source: &str) -> impl Iterator<Item = char> + '_ {
        source.chars().filter_map(|letter| {
            if is_separator(letter) {
                None
            } else {
                Some(letter.to_ascii_uppercase())
            }
        })
    }

    #[inline]
    pub fn lower(source: &str) -> impl Iterator<Item = char> + '_ {
        source.chars().filter_map(|letter| {
            if is_separator(letter) {
                None
            } else {
                Some(letter.to_ascii_lowercase())
            }
        })
    }

    #[inline]
    pub fn pascal(source: &str) -> impl Iterator<Item = char> + '_ {
        continuous(source, true)
    }

    #[inline]
    pub fn camel(source: &str) -> impl Iterator<Item = char> + '_ {
        continuous(source, false)
    }

    #[inline]
    pub fn snake(source: &str, upper: bool) -> impl Iterator<Item = char> + '_ {
        separated(source, '_', !upper)
    }

    #[inline]
    pub fn kebab(source: &str, upper: bool) -> impl Iterator<Item = char> + '_ {
        separated(source, '-', !upper)
    }
}

#[inline]
const fn is_separator(letter: char) -> bool {
    matches!(letter, '_' | '-' | ' ' | '\n')
}

#[inline]
fn continuous(source: &str, mut first: bool) -> impl Iterator<Item = char> + '_ {
    let mut upper = first;
    let mut last = true;
    source.chars().flat_map(move |letter| {
        let mut result = None;
        if letter.is_ascii_alphabetic() {
            if upper {
                result = Some(letter.to_ascii_uppercase());
                upper = false;
            } else if last {
                result = Some(letter.to_ascii_lowercase());
            } else {
                result = Some(letter);
            }
            last = letter.is_ascii_uppercase();
            first = true;
        } else if is_separator(letter) {
            upper = first;
        } else {
            upper = first;
            result = Some(letter);
        }
        result
    })
}

#[inline]
fn separated(source: &str, separator: char, lower: bool) -> impl Iterator<Item = char> + '_ {
    let mut separate = false;
    let mut first = false;
    let mut last = false;
    source.chars().flat_map(move |letter| {
        let mut results = [None, None];
        if letter.is_ascii_uppercase() {
            if separate || last {
                results[0] = Some(separator);
                separate = false;
                last = false;
            }
            first = true;
            results[1] = Some(if lower {
                letter.to_ascii_lowercase()
            } else {
                letter
            });
        } else if letter.is_ascii_lowercase() {
            if separate {
                results[0] = Some(separator);
                separate = false;
            }
            first = true;
            last = true;
            results[1] = Some(if lower {
                letter
            } else {
                letter.to_ascii_uppercase()
            });
        } else if is_separator(letter) {
            separate = first;
            last = false;
        } else {
            results[0] = Some(letter);
            separate = false;
            first = false;
            last = false;
        }
        results.into_iter().flatten()
    })
}

#[cfg(test)]
mod tests {
    use std::fmt;

    use super::*;

    #[test]
    fn pascal() -> Result<(), fmt::Error> {
        let convert = |value| Case::pascal(value).collect::<String>();
        assert_eq!(convert("BobaFett"), "BobaFett");
        assert_eq!(convert("bobaFett"), "BobaFett");
        assert_eq!(convert("boba fett"), "BobaFett");
        assert_eq!(convert("BOBA-FETT"), "BobaFett");
        assert_eq!(convert("BOBA"), "Boba");
        assert_eq!(convert("BOBA_FETT"), "BobaFett");
        assert_eq!(convert("boba-fett"), "BobaFett");
        assert_eq!(convert("_Boba_"), "Boba");
        assert_eq!(convert("_boba_"), "Boba");
        assert_eq!(convert("-Boba-"), "Boba");
        assert_eq!(convert("-boba-"), "Boba");
        assert_eq!(convert("1boba2fett"), "1Boba2Fett");
        assert_eq!(convert("1boBa2FetT"), "1BoBa2FetT");
        assert_eq!(convert("BObaFeTT"), "BobaFeTt");
        Ok(())
    }

    #[test]
    fn camel() -> Result<(), fmt::Error> {
        let convert = |value| Case::camel(value).collect::<String>();
        assert_eq!(convert("BobaFett"), "bobaFett");
        assert_eq!(convert("bobaFett"), "bobaFett");
        assert_eq!(convert("boba fett"), "bobaFett");
        assert_eq!(convert("BOBA-FETT"), "bobaFett");
        assert_eq!(convert("BOBA"), "boba");
        assert_eq!(convert("BOBA_FETT"), "bobaFett");
        assert_eq!(convert("boba-fett"), "bobaFett");
        assert_eq!(convert("_Boba_"), "boba");
        assert_eq!(convert("_boba_"), "boba");
        assert_eq!(convert("-Boba-"), "boba");
        assert_eq!(convert("-boba-"), "boba");
        assert_eq!(convert("1boba2fett"), "1boba2Fett");
        assert_eq!(convert("1boBa2FetT"), "1boBa2FetT");
        assert_eq!(convert("BObaFeTT"), "bobaFeTt");
        Ok(())
    }

    #[test]
    fn snake() -> Result<(), fmt::Error> {
        let convert = |value| Case::snake(value, false).collect::<String>();
        assert_eq!(convert("BobaFett"), "boba_fett");
        assert_eq!(convert("bobaFett"), "boba_fett");
        assert_eq!(convert("boba fett"), "boba_fett");
        assert_eq!(convert("BOBA-FETT"), "boba_fett");
        assert_eq!(convert("BOBA"), "boba");
        assert_eq!(convert("BOBA_FETT"), "boba_fett");
        assert_eq!(convert("boba-fett"), "boba_fett");
        assert_eq!(convert("_Boba_"), "boba");
        assert_eq!(convert("_boba_"), "boba");
        assert_eq!(convert("-Boba-"), "boba");
        assert_eq!(convert("-boba-"), "boba");
        assert_eq!(convert("1boba2fett"), "1boba2fett");
        assert_eq!(convert("1boBa2FetT"), "1bo_ba2fet_t");
        assert_eq!(convert("BObaFeTT"), "boba_fe_tt");
        Ok(())
    }

    #[test]
    fn kebab() -> Result<(), fmt::Error> {
        let convert = |value| Case::kebab(value, false).collect::<String>();
        assert_eq!(convert("BobaFett"), "boba-fett");
        assert_eq!(convert("bobaFett"), "boba-fett");
        assert_eq!(convert("boba fett"), "boba-fett");
        assert_eq!(convert("BOBA-FETT"), "boba-fett");
        assert_eq!(convert("BOBA"), "boba");
        assert_eq!(convert("BOBA_FETT"), "boba-fett");
        assert_eq!(convert("boba-fett"), "boba-fett");
        assert_eq!(convert("_Boba_"), "boba");
        assert_eq!(convert("_boba_"), "boba");
        assert_eq!(convert("-Boba-"), "boba");
        assert_eq!(convert("-boba-"), "boba");
        assert_eq!(convert("1boba2fett"), "1boba2fett");
        assert_eq!(convert("1boBa2FetT"), "1bo-ba2fet-t");
        assert_eq!(convert("BObaFeTT"), "boba-fe-tt");
        Ok(())
    }

    #[test]
    fn upper() -> Result<(), fmt::Error> {
        let convert = |value| Case::upper(value).collect::<String>();
        assert_eq!(convert("BobaFett"), "BOBAFETT");
        assert_eq!(convert("bobaFett"), "BOBAFETT");
        assert_eq!(convert("boba fett"), "BOBAFETT");
        assert_eq!(convert("BOBA-FETT"), "BOBAFETT");
        assert_eq!(convert("BOBA"), "BOBA");
        assert_eq!(convert("BOBA_FETT"), "BOBAFETT");
        assert_eq!(convert("boba-fett"), "BOBAFETT");
        assert_eq!(convert("_Boba_"), "BOBA");
        assert_eq!(convert("_boba_"), "BOBA");
        assert_eq!(convert("-Boba-"), "BOBA");
        assert_eq!(convert("-boba-"), "BOBA");
        assert_eq!(convert("1boba2fett"), "1BOBA2FETT");
        assert_eq!(convert("1boBa2FetT"), "1BOBA2FETT");
        assert_eq!(convert("BObaFeTT"), "BOBAFETT");
        Ok(())
    }

    #[test]
    fn upper_snake() -> Result<(), fmt::Error> {
        let convert = |value| Case::snake(value, true).collect::<String>();
        assert_eq!(convert("BobaFett"), "BOBA_FETT");
        assert_eq!(convert("bobaFett"), "BOBA_FETT");
        assert_eq!(convert("boba fett"), "BOBA_FETT");
        assert_eq!(convert("BOBA-FETT"), "BOBA_FETT");
        assert_eq!(convert("BOBA"), "BOBA");
        assert_eq!(convert("BOBA_FETT"), "BOBA_FETT");
        assert_eq!(convert("boba-fett"), "BOBA_FETT");
        assert_eq!(convert("_Boba_"), "BOBA");
        assert_eq!(convert("_boba_"), "BOBA");
        assert_eq!(convert("-Boba-"), "BOBA");
        assert_eq!(convert("-boba-"), "BOBA");
        assert_eq!(convert("1boba2fett"), "1BOBA2FETT");
        assert_eq!(convert("1boBa2FetT"), "1BO_BA2FET_T");
        assert_eq!(convert("BObaFeTT"), "BOBA_FE_TT");
        Ok(())
    }

    #[test]
    fn upper_kebab() -> Result<(), fmt::Error> {
        let convert = |value| Case::kebab(value, true).collect::<String>();
        assert_eq!(convert("BobaFett"), "BOBA-FETT");
        assert_eq!(convert("bobaFett"), "BOBA-FETT");
        assert_eq!(convert("boba fett"), "BOBA-FETT");
        assert_eq!(convert("BOBA-FETT"), "BOBA-FETT");
        assert_eq!(convert("BOBA"), "BOBA");
        assert_eq!(convert("BOBA_FETT"), "BOBA-FETT");
        assert_eq!(convert("boba-fett"), "BOBA-FETT");
        assert_eq!(convert("_Boba_"), "BOBA");
        assert_eq!(convert("_boba_"), "BOBA");
        assert_eq!(convert("-Boba-"), "BOBA");
        assert_eq!(convert("-boba-"), "BOBA");
        assert_eq!(convert("1boba2fett"), "1BOBA2FETT");
        assert_eq!(convert("1boBa2FetT"), "1BO-BA2FET-T");
        assert_eq!(convert("BObaFeTT"), "BOBA-FE-TT");
        Ok(())
    }
}