toml_edit 0.5.0

Yet another format-preserving TOML parser.
Documentation
use combine::easy::Errors as ParseError;
use combine::stream::easy::Error;
use combine::stream::position::SourcePosition;
use std::error::Error as StdError;
use std::fmt::{Display, Formatter, Result};

/// Type representing a TOML parse error
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct TomlError {
    message: String,
}

impl TomlError {
    pub(crate) fn new(error: ParseError<u8, &[u8], usize>, input: &[u8]) -> Self {
        Self {
            message: format!("{}", FancyError::new(error, input)),
        }
    }

    pub(crate) fn from_unparsed(pos: usize, input: &[u8]) -> Self {
        Self::new(
            ParseError::new(pos, CustomError::UnparsedLine.into()),
            input,
        )
    }

    pub(crate) fn custom(message: String) -> Self {
        Self { message }
    }
}

/// Displays a TOML parse error
///
/// # Example
///
/// TOML parse error at line 1, column 10
///   |
/// 1 | 00:32:00.a999999
///   |          ^
/// Unexpected `a`
/// Expected `digit`
/// While parsing a Time
/// While parsing a Date-Time
impl Display for TomlError {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "{}", self.message)
    }
}

impl StdError for TomlError {
    fn description(&self) -> &'static str {
        "TOML parse error"
    }
}

#[derive(Debug)]
pub(crate) struct FancyError<'a> {
    errors: Vec<Error<char, String>>,
    position: SourcePosition,
    input: &'a [u8],
}

impl<'a> FancyError<'a> {
    pub(crate) fn new(error: ParseError<u8, &'a [u8], usize>, input: &'a [u8]) -> Self {
        let position = translate_position(input, error.position);
        let errors: Vec<_> = error
            .errors
            .into_iter()
            .map(|e| {
                e.map_token(char::from)
                    .map_range(|s| String::from_utf8_lossy(s).into_owned())
            })
            .collect();
        Self {
            errors,
            position,
            input,
        }
    }
}

impl<'a> Display for FancyError<'a> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let SourcePosition { line, column } = self.position;

        let offset = line.to_string().len();
        let content = self
            .input
            .split(|b| *b == b'\n')
            .nth((line - 1) as usize)
            .expect("line");
        let content = String::from_utf8_lossy(content);

        writeln!(f, "TOML parse error at line {}, column {}", line, column)?;

        //   |
        for _ in 0..=offset {
            write!(f, " ")?;
        }
        writeln!(f, "|")?;

        // 1 | 00:32:00.a999999
        write!(f, "{} | ", line)?;
        writeln!(f, "{}", content)?;

        //   |          ^
        for _ in 0..=offset {
            write!(f, " ")?;
        }
        write!(f, "|")?;
        for _ in 0..column {
            write!(f, " ")?;
        }
        writeln!(f, "^")?;

        Error::fmt_errors(self.errors.as_ref(), f)
    }
}

fn translate_position(input: &[u8], index: usize) -> SourcePosition {
    if input.is_empty() {
        return SourcePosition {
            line: 1,
            column: (index + 1) as i32,
        };
    }

    let safe_index = index.min(input.len() - 1);
    let column_offset = index - safe_index;
    let index = safe_index;

    let nl = input[0..index]
        .iter()
        .rev()
        .enumerate()
        .find(|(_, b)| **b == b'\n')
        .map(|(nl, _)| index - nl - 1);
    let line_start = match nl {
        Some(nl) => nl + 1,
        None => 0,
    };
    let line = input[0..line_start].iter().filter(|b| **b == b'\n').count() + 1;
    let line = line as i32;

    let column = std::str::from_utf8(&input[line_start..=index])
        .map(|s| s.chars().count())
        .unwrap_or_else(|_| index - line_start + 1);
    let column = (column + column_offset) as i32;

    SourcePosition { line, column }
}

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

    #[test]
    fn empty() {
        let input = b"";
        let index = 0;
        let position = translate_position(&input[..], index);
        assert_eq!(position, SourcePosition { line: 1, column: 1 });
    }

    #[test]
    fn start() {
        let input = b"Hello";
        let index = 0;
        let position = translate_position(&input[..], index);
        assert_eq!(position, SourcePosition { line: 1, column: 1 });
    }

    #[test]
    fn end() {
        let input = b"Hello";
        let index = input.len() - 1;
        let position = translate_position(&input[..], index);
        assert_eq!(
            position,
            SourcePosition {
                line: 1,
                column: input.len() as i32
            }
        );
    }

    #[test]
    fn after() {
        let input = b"Hello";
        let index = input.len();
        let position = translate_position(&input[..], index);
        assert_eq!(
            position,
            SourcePosition {
                line: 1,
                column: (input.len() + 1) as i32
            }
        );
    }

    #[test]
    fn first_line() {
        let input = b"Hello\nWorld\n";
        let index = 2;
        let position = translate_position(&input[..], index);
        assert_eq!(position, SourcePosition { line: 1, column: 3 });
    }

    #[test]
    fn end_of_line() {
        let input = b"Hello\nWorld\n";
        let index = 5;
        let position = translate_position(&input[..], index);
        assert_eq!(position, SourcePosition { line: 1, column: 6 });
    }

    #[test]
    fn start_of_second_line() {
        let input = b"Hello\nWorld\n";
        let index = 6;
        let position = translate_position(&input[..], index);
        assert_eq!(position, SourcePosition { line: 2, column: 1 });
    }

    #[test]
    fn second_line() {
        let input = b"Hello\nWorld\n";
        let index = 8;
        let position = translate_position(&input[..], index);
        assert_eq!(position, SourcePosition { line: 2, column: 3 });
    }
}

#[derive(Debug, Clone)]
pub(crate) enum CustomError {
    DuplicateKey { key: String, table: String },
    InvalidHexEscape(u32),
    UnparsedLine,
    OutOfRange,
}

impl StdError for CustomError {
    fn description(&self) -> &'static str {
        "TOML parse error"
    }
}

impl Display for CustomError {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        match *self {
            CustomError::DuplicateKey { ref key, ref table } => {
                writeln!(f, "Duplicate key `{}` in `{}` table", key, table)
            }
            CustomError::InvalidHexEscape(ref h) => {
                writeln!(f, "Invalid hex escape code: {:x} ", h)
            }
            CustomError::UnparsedLine => writeln!(f, "Could not parse the line"),
            CustomError::OutOfRange => writeln!(f, "Value is out of range"),
        }
    }
}