semdiff-differ-json 0.4.2

JSON diff calculator and reporters for semdiff.
Documentation
use nom::error::ErrorKind;
use nom::{Err, Mode, Offset, OutputMode, PResult, Parser};
use std::marker::PhantomData;
use std::str::FromStr;

pub(super) struct NumberLiteral<E>(PhantomData<E>);

impl<E> NumberLiteral<E> {
    pub(super) fn new() -> Self {
        NumberLiteral(PhantomData)
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Number {
    Int(i64),
    Float(f64),
}

impl<'a, E> Parser<&'a str> for NumberLiteral<E>
where
    E: nom::error::ParseError<&'a str>,
{
    type Output = Number;
    type Error = E;

    fn process<OM: OutputMode>(&mut self, input: &'a str) -> PResult<OM, &'a str, Self::Output, Self::Error> {
        let first = input;
        let input = input.strip_prefix("-").unwrap_or(input);
        let input = if let Some(tail) = input.strip_prefix("0") {
            tail
        } else {
            let mut input = input
                .strip_prefix(|c: char| c.is_ascii_digit())
                .ok_or_else(|| Err::Error(OM::Error::bind(|| E::from_error_kind(input, ErrorKind::Digit))))?;
            while let Some(tail) = input.strip_prefix(|c: char| c.is_ascii_digit()) {
                input = tail;
            }
            input
        };

        if !input.starts_with(['.', 'e']) {
            let number = &first[..first.offset(input)];
            return i64::from_str(&first[..first.offset(input)])
                .map(|n| (input, OM::Output::bind(|| Number::Int(n))))
                .map_err(|_| Err::Error(OM::Error::bind(|| E::from_error_kind(number, ErrorKind::Digit))));
        }

        let input = if let Some(input) = input.strip_prefix(".") {
            let mut input = input
                .strip_prefix(|c: char| c.is_ascii_digit())
                .ok_or_else(|| Err::Error(OM::Error::bind(|| E::from_error_kind(input, ErrorKind::Digit))))?;
            while let Some(tail) = input.strip_prefix(|c: char| c.is_ascii_digit()) {
                input = tail;
            }
            input
        } else {
            input
        };

        let input = if let Some(input) = input.strip_prefix("e") {
            let input = input.strip_prefix(|c: char| c == '+' || c == '-').unwrap_or(input);
            let mut input = input
                .strip_prefix(|c: char| c.is_ascii_digit())
                .ok_or_else(|| Err::Error(OM::Error::bind(|| E::from_error_kind(input, ErrorKind::Digit))))?;
            while let Some(tail) = input.strip_prefix(|c: char| c.is_ascii_digit()) {
                input = tail;
            }
            input
        } else {
            input
        };

        let number = &first[..first.offset(input)];
        f64::from_str(&first[..first.offset(input)])
            .map(|n| (input, OM::Output::bind(|| Number::Float(n))))
            .map_err(|_| Err::Error(OM::Error::bind(|| E::from_error_kind(number, ErrorKind::Digit))))
    }
}

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

    #[test]
    fn number_literal_parser_accepts_int_alternatives() {
        assert_eq!(NumberLiteral::<Error<&str>>::new().parse("0"), Ok(("", Number::Int(0))));
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-0"),
            Ok(("", Number::Int(0)))
        );
        assert_eq!(NumberLiteral::<Error<&str>>::new().parse("1"), Ok(("", Number::Int(1))));
        assert_eq!(NumberLiteral::<Error<&str>>::new().parse("9"), Ok(("", Number::Int(9))));
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("10"),
            Ok(("", Number::Int(10)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-1"),
            Ok(("", Number::Int(-1)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-9876543210"),
            Ok(("", Number::Int(-9876543210)))
        );
    }

    #[test]
    fn number_literal_parser_accepts_fraction_parts() {
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("0.0"),
            Ok(("", Number::Float(0.0)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-0.5"),
            Ok(("", Number::Float(-0.5)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("1.25"),
            Ok(("", Number::Float(1.25)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-12.75"),
            Ok(("", Number::Float(-12.75)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("123.456789"),
            Ok(("", Number::Float(123.456789)))
        );
    }

    #[test]
    fn number_literal_parser_accepts_exponent_parts() {
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("1e2"),
            Ok(("", Number::Float(100.0)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("1e+2"),
            Ok(("", Number::Float(100.0)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("1e-2"),
            Ok(("", Number::Float(0.01)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("1e02"),
            Ok(("", Number::Float(100.0)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-0e0"),
            Ok(("", Number::Float(-0.0)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("1.25e2"),
            Ok(("", Number::Float(125.0)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-12.5e+1"),
            Ok(("", Number::Float(-125.0)))
        );
    }

    #[test]
    fn number_literal_parser_returns_remaining_input() {
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("123]"),
            Ok(("]", Number::Int(123)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-0, next"),
            Ok((", next", Number::Int(0)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("1.25e2 rest"),
            Ok((" rest", Number::Float(125.0)))
        );
    }

    #[test]
    fn number_literal_parser_leaves_non_bnf_suffixes_unconsumed() {
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("1E2"),
            Ok(("E2", Number::Int(1)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("01"),
            Ok(("1", Number::Int(0)))
        );
        assert_eq!(
            NumberLiteral::<Error<&str>>::new().parse("-01"),
            Ok(("1", Number::Int(0)))
        );
    }

    #[test]
    fn number_literal_parser_rejects_inputs_without_a_number_prefix() {
        assert!(NumberLiteral::<Error<&str>>::new().parse("").is_err());
        assert!(NumberLiteral::<Error<&str>>::new().parse("-").is_err());
        assert!(NumberLiteral::<Error<&str>>::new().parse("+1").is_err());
        assert!(NumberLiteral::<Error<&str>>::new().parse(".1").is_err());
        assert!(NumberLiteral::<Error<&str>>::new().parse("e1").is_err());
        assert!(NumberLiteral::<Error<&str>>::new().parse("abc").is_err());
    }
}