jjpwrgem-parse 0.14.0

JSON parser and formatter with rich errors
Documentation
use core::range::Range;
use std::iter::Peekable;

use crate::{
    Error, ErrorKind, Result,
    tokens::{CharWithContext, JsonChar, Token, TokenWithContext},
};

enum StringState {
    Open,
    CharOrEscapeOrEnd {
        string_range: Range<usize>,
        quote_range: Range<usize>,
    },
    Escape {
        string_range: Range<usize>,
        quote_range: Range<usize>,
        slash_range: Range<usize>,
    },
    UEscape {
        string_range: Range<usize>,
        quote_range: Range<usize>,
        u_range: Range<usize>,
        slash_range: Range<usize>,
        digits_seen: usize,
    },
    End(TokenWithContext),
}

impl StringState {
    fn process(
        self,
        chars: &mut Peekable<impl Iterator<Item = CharWithContext>>,
        input: &str,
    ) -> Result<Self> {
        let res = match self {
            StringState::Open => {
                let Some(CharWithContext(starting_quote, JsonChar('"'))) = chars.next() else {
                    unreachable!("must start with a quote");
                };

                StringState::CharOrEscapeOrEnd {
                    string_range: starting_quote,
                    quote_range: starting_quote,
                }
            }
            StringState::CharOrEscapeOrEnd {
                string_range,
                quote_range,
            } => match chars.next() {
                Some(CharWithContext(r, JsonChar('\\'))) => StringState::Escape {
                    string_range: string_range.start..r.end,
                    quote_range,
                    slash_range: r,
                },
                Some(CharWithContext(r, JsonChar('"'))) => StringState::End(TokenWithContext::new(
                    Token::String,
                    string_range.start..r.end,
                )),
                Some(CharWithContext(r, c)) if c.is_control() => {
                    return Err(Error::new(
                        ErrorKind::UnexpectedControlCharacterInString(c),
                        r,
                        input,
                    ));
                }
                Some(CharWithContext(r, _)) => StringState::CharOrEscapeOrEnd {
                    string_range: string_range.start..r.end,
                    quote_range,
                },
                None => {
                    return Err(Error::from_unterminated(
                        ErrorKind::ExpectedQuote {
                            open_range: quote_range,
                            string_range,
                        },
                        input,
                    ));
                }
            },
            StringState::Escape {
                string_range,
                quote_range,
                slash_range,
            } => match chars.next() {
                Some(CharWithContext(r, c)) if c.can_be_escaped_directly() => {
                    StringState::CharOrEscapeOrEnd {
                        string_range: string_range.start..r.end,
                        quote_range,
                    }
                }
                Some(CharWithContext(r, JsonChar('u'))) => StringState::UEscape {
                    string_range,
                    quote_range,
                    u_range: r,
                    slash_range,
                    digits_seen: 0,
                },
                maybe_c => {
                    return Err(Error::from_maybe_json_char_with_context(
                        |c| ErrorKind::ExpectedEscape {
                            maybe_c: c,
                            slash_range,
                            string_range,
                            quote_range,
                        },
                        maybe_c,
                        input,
                    ));
                }
            },
            StringState::UEscape {
                string_range,
                quote_range,
                u_range,
                slash_range,
                digits_seen,
            } => match chars.next() {
                Some(CharWithContext(r, c)) if c.is_hexdigit() => {
                    let string_range = string_range.start..r.end;
                    let next_digits = digits_seen + 1;
                    if next_digits == 4 {
                        StringState::CharOrEscapeOrEnd {
                            string_range,
                            quote_range,
                        }
                    } else {
                        StringState::UEscape {
                            string_range,
                            quote_range,
                            u_range,
                            slash_range,
                            digits_seen: next_digits,
                        }
                    }
                }
                maybe_c => {
                    return Err(Error::from_maybe_json_char_with_context(
                        |c| ErrorKind::ExpectedHexDigit {
                            quote_range,
                            slash_range,
                            u_range,
                            maybe_c: c,
                            digit_idx: digits_seen + 1,
                        },
                        maybe_c,
                        input,
                    ));
                }
            },
            StringState::End(_) => self,
        };

        Ok(res)
    }
}

pub fn parse_string(input: &str, pos: usize) -> Result<TokenWithContext> {
    let mut chars = input[pos..]
        .char_indices()
        .map(|(i, c)| (i + pos, c).into())
        .peekable();
    let mut state = StringState::Open;

    loop {
        state = state.process(&mut chars, input)?;
        if let StringState::End(tok) = state {
            break Ok(tok);
        }
    }
}