json5format 0.1.1

Customizable JSON5 document formatter that preserves comments
Documentation
// Copyright (c) 2020 Google LLC All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#![deny(missing_docs)]

/// A location within a document buffer or document file. This module uses `Location` to identify
/// to refer to locations of JSON5 syntax errors, while parsing) and also to locations in this Rust
/// source file, to improve unit testing output.
pub struct Location {
    /// The name of the JSON5 document file being parsed and formatted (if provided).
    pub file: Option<String>,

    /// A line number within the JSON5 document. (The first line at the top of the document/file is
    /// line 1.)
    pub line: usize,

    /// A character column number within the specified line. (The left-most character of the line is
    /// column 1).
    pub col: usize,
}

impl Location {
    /// Create a new `Location` for the given source document location.
    pub fn new(file: Option<String>, line: usize, col: usize) -> Self {
        Location { file, line, col }
    }
}

impl std::fmt::Display for Location {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(file) = &self.file {
            write!(formatter, "{}:{}:{}", file, self.line, self.col)
        } else {
            write!(formatter, "{}:{}", self.line, self.col)
        }
    }
}

impl std::fmt::Debug for Location {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(formatter, "{}", &self)
    }
}

/// Errors produced by the json5format library.
#[derive(Debug)]
pub enum Error {
    /// A formatter configuration option was invalid.
    Configuration(String),

    /// A syntax error was encountered while parsing a JSON5 document.
    Parse(Option<Location>, String),

    /// The parser or formatter entered an unexpected state. An `Error::Internal` likely indicates
    /// there is a software bug in the json5format library.
    Internal(Option<Location>, String),

    /// This error is only produced by internal test functions to indicate a test result did not
    /// match expectations.
    TestFailure(Option<Location>, String),
}

impl std::error::Error for Error {}

impl Error {
    /// Return a configuration error.
    /// # Arguments
    ///   * err - The error message.
    pub fn configuration(err: impl std::fmt::Display) -> Self {
        Error::Configuration(err.to_string())
    }

    /// Return a parsing error.
    /// # Arguments
    ///   * location - Optional location in the JSON5 document where the error was detected.
    ///   * err - The error message.
    pub fn parse(location: Option<Location>, err: impl std::fmt::Display) -> Self {
        Error::Parse(location, err.to_string())
    }

    /// Return an internal error (indicating an error in the software implementation itself).
    /// # Arguments
    ///   * location - Optional location in the JSON5 document where the error was detected,
    ///     which might be available if the error occurred while parsing the document.
    ///   * err - The error message.
    pub fn internal(location: Option<Location>, err: impl Into<String>) -> Self {
        Error::Internal(location, err.into())
    }

    /// Return a TestFailure error.
    /// # Arguments
    ///   * location - Optional Rust source code location where the test failed.
    ///   * err - The error message.
    pub fn test_failure(location: Option<Location>, err: impl Into<String>) -> Self {
        Error::TestFailure(location, err.into())
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let (prefix, loc, err) = match &self {
            Error::Configuration(err) => ("Configuration error", &None, err.to_string()),
            Error::Parse(loc, err) => ("Parse error", loc, err.to_string()),
            Error::Internal(loc, err) => ("Internal error", loc, err.to_string()),
            Error::TestFailure(loc, err) => ("Test failure", loc, err.to_string()),
        };
        match loc {
            Some(loc) => write!(f, "{}: {}: {}", prefix, loc, err),
            None => write!(f, "{}: {}", prefix, err),
        }
    }
}

/// Create a `TestFailure` error including the source file location of the macro call.
///
/// # Example:
///
/// ```no_run
/// # use json5format::Error;
/// # use json5format::Location;
/// # use json5format::test_error;
/// # fn test() -> std::result::Result<(),Error> {
/// return Err(test_error!("error message"));
/// # }
/// # test();
/// ```
#[macro_export]
macro_rules! test_error {
    ($err:expr) => {
        Error::test_failure(
            Some(Location::new(Some(file!().to_string()), line!() as usize, column!() as usize)),
            $err,
        )
    };
}