splutter 0.1.0

A library to create/generate text efficiently.
Documentation
use crate::output::Output;
use std::str::pattern::Pattern;

pub trait StrValidationExt<'a> {
    fn as_integer(&self) -> Option<StrRefInteger<'a>>;
    fn as_decimal(&self) -> Option<StrRefDecimal<'a>>;
    fn as_identifier(&self) -> Option<StrRefIdentifier<'a>>;
}

impl<'a> StrValidationExt<'a> for &'a str {
    fn as_integer(&self) -> Option<StrRefInteger<'a>> {
        StrRefInteger::new(self)
    }

    fn as_decimal(&self) -> Option<StrRefDecimal<'a>> {
        StrRefDecimal::new(self)
    }

    fn as_identifier(&self) -> Option<StrRefIdentifier<'a>> {
        StrRefIdentifier::new(self)
    }
}

pub trait Swap {
    type Output;
    fn swap(self) -> Self::Output;
}

impl<T1, T2> Swap for (T1, T2) {
    type Output = (T2, T1);

    fn swap(self) -> Self::Output {
        (self.1, self.0)
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StrRefInteger<'a>(&'a str);

impl<'a> StrRefInteger<'a> {
    pub fn new(value: &'a str) -> Option<Self> {
        let is_not_empty = !value.is_empty();
        let is_digit = value.chars().all(|char| char.is_ascii_digit());
        (is_not_empty && is_digit).then_some(Self(value))
    }
}

impl<'a> Output for StrRefInteger<'a> {
    fn output(self, output: &mut String) {
        self.0.output(output)
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StrRefDecimal<'a>(&'a str);

impl<'a> StrRefDecimal<'a> {
    pub fn new(value: &'a str) -> Option<Self> {
        let initial = value;
        let (value, before) = take_integer_or_nothing(value)?;
        let value = take_tag('.', value)?;
        let (value, after) = take_integer_or_nothing(value)?;
        (value.is_empty() && (!before.is_empty() || !after.is_empty())).then_some(Self(initial))
    }
}

impl<'a> Output for StrRefDecimal<'a> {
    fn output(self, output: &mut String) {
        self.0.output(output)
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StrRefIdentifier<'a>(&'a str);

impl<'a> StrRefIdentifier<'a> {
    pub fn new(value: &'a str) -> Option<Self> {
        let mut chars = value.chars();
        let first_is_letter = chars.next()?.is_ascii_alphabetic();
        let remaining_are_identifier = chars.all(is_ascii_identifier);
        (remaining_are_identifier && first_is_letter).then_some(Self(value))
    }
}

impl<'a> Output for StrRefIdentifier<'a> {
    fn output(self, output: &mut String) {
        self.0.output(output)
    }
}

#[inline]
fn is_ascii_identifier(char: char) -> bool {
    matches!(char, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_')
}

fn take_tag<'a, P>(tag: P, input: &'a str) -> Option<&'a str>
where
    P: Pattern<'a>,
{
    tag.strip_prefix_of(input)
}

fn take_integer(input: &str) -> Option<(&str, &str)> {
    let index = input
        .find(|char: char| !char.is_ascii_digit())
        .unwrap_or(input.len());
    (index != 0)
        .then_some(index)
        .map(|index| input.split_at(index).swap())
}

fn take_integer_or_nothing(input: &str) -> Option<(&str, &str)> {
    let index = input
        .find(|char: char| !char.is_ascii_digit())
        .unwrap_or(input.len());
    Some(input.split_at(index).swap())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(test)]
    mod integer {
        use super::*;

        #[test]
        fn can_parse() {
            let integer = "12".as_integer();
            assert_eq!(integer, Some(StrRefInteger("12")));
        }

        #[test]
        fn fail_on_empty() {
            let integer = "".as_integer();
            assert_eq!(integer, None);
        }

        #[test]
        fn fail_on_text() {
            let integer = "123abc".as_integer();
            assert_eq!(integer, None);
        }
    }

    #[cfg(test)]
    mod decimal {
        use super::*;

        #[test]
        fn can_parse_with_both_sides() {
            let decimal = "12.34".as_decimal();
            assert_eq!(decimal, Some(StrRefDecimal("12.34")))
        }

        #[test]
        fn can_parse_before_dot() {
            let decimal = "12.".as_decimal();
            assert_eq!(decimal, Some(StrRefDecimal("12.")));
        }

        #[test]
        fn can_parse_after_dot() {
            let decimal = ".34".as_decimal();
            assert_eq!(decimal, Some(StrRefDecimal(".34")));
        }

        #[test]
        fn fail_on_empty() {
            let decimal = "".as_decimal();
            assert_eq!(decimal, None);
        }

        #[test]
        fn fail_on_comma() {
            let decimal = "12,34".as_decimal();
            assert_eq!(decimal, None);
        }

        #[test]
        fn fail_on_text() {
            let decimal = "12.34abc".as_decimal();
            assert_eq!(decimal, None)
        }
    }

    #[cfg(test)]
    mod identifier {}

    #[cfg(test)]
    mod take_integer {
        use super::*;

        #[test]
        fn can_take_integer() {
            let result = take_integer("123abc");
            assert_eq!(result, Some(("abc", "123")));
        }

        #[test]
        fn can_take_integer_only() {
            let result = take_integer("123");
            assert_eq!(result, Some(("", "123")))
        }

        #[test]
        fn fail_for_no_integer() {
            let result = take_integer("abc");
            assert_eq!(result, None);
        }

        #[test]
        fn fail_if_not_starting_with_integer() {
            let result = take_integer("abc123");
            assert_eq!(result, None);
        }
    }

    #[cfg(test)]
    mod take_integer_or_nothing {
        use crate::validators::string::take_integer_or_nothing;

        #[test]
        fn can_take_integer() {
            let result = take_integer_or_nothing("123abc");
            assert_eq!(result, Some(("abc", "123")))
        }

        #[test]
        fn can_take_integer_only() {
            let result = take_integer_or_nothing("123");
            assert_eq!(result, Some(("", "123")))
        }

        #[test]
        fn can_take_nothing() {
            let result = take_integer_or_nothing("abc");
            assert_eq!(result, Some(("abc", "")))
        }
    }
}