feat(handlers): Add JSON handler (#90)

This commit is contained in:
dvermd 2021-11-15 17:49:22 +01:00 committed by GitHub
parent fe77d8c754
commit 53b246829a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 921 additions and 0 deletions

129
src/handlers/json.rs Normal file
View File

@ -0,0 +1,129 @@
use std::fmt;
use crate::{protocol::Diagnostic, ReportHandler, Severity};
/**
[ReportHandler] that renders json output.
It's a machine-readable output.
*/
#[derive(Debug, Clone)]
pub struct JSONReportHandler;
impl JSONReportHandler {
/// Create a new [JSONReportHandler]. There are no customization
/// options.
pub fn new() -> Self {
Self
}
}
impl Default for JSONReportHandler {
fn default() -> Self {
Self::new()
}
}
fn escape(input: &str) -> String {
input
.chars()
.map(|c| match c {
'"' => "\\\\\"".to_string(),
'\'' => "\\\\'".to_string(),
'\r' => "\\\\r".to_string(),
'\n' => "\\\\n".to_string(),
'\t' => "\\\\t".to_string(),
'\u{08}' => "\\\\b".to_string(),
'\u{0c}' => "\\\\f".to_string(),
c => format!("{}", c),
})
.collect()
}
impl JSONReportHandler {
/// Render a [Diagnostic]. This function is mostly internal and meant to
/// be called by the toplevel [ReportHandler] handler, but is
/// made public to make it easier (possible) to test in isolation from
/// global state.
pub fn render_report(
&self,
f: &mut impl fmt::Write,
diagnostic: &(dyn Diagnostic),
) -> fmt::Result {
write!(f, r#"{{"message": "{}","#, escape(&diagnostic.to_string()))?;
if let Some(code) = diagnostic.code() {
write!(f, r#""code": "{}","#, escape(&code.to_string()))?;
}
let severity = match diagnostic.severity() {
Some(Severity::Error) | None => "error",
Some(Severity::Warning) => "warning",
Some(Severity::Advice) => "advice",
};
write!(f, r#""severity": "{:}","#, severity)?;
if let Some(url) = diagnostic.url() {
write!(f, r#""url": "{}","#, &url.to_string())?;
}
if let Some(help) = diagnostic.help() {
write!(f, r#""help": "{}","#, escape(&help.to_string()))?;
}
if diagnostic.source_code().is_some() {
self.render_snippets(f, diagnostic)?;
}
if let Some(labels) = diagnostic.labels() {
write!(f, r#""labels": ["#)?;
let mut add_comma = false;
for label in labels {
if add_comma {
write!(f, ",")?;
} else {
add_comma = true;
}
write!(f, "{{")?;
if let Some(label_name) = label.label() {
write!(f, r#""label": "{}","#, escape(label_name))?;
}
write!(f, r#""span": {{"#)?;
write!(f, r#""offset": {},"#, label.offset())?;
write!(f, r#""length": {}"#, label.len())?;
write!(f, "}}}}")?;
}
write!(f, "],")?;
} else {
write!(f, r#""labels": [],"#)?;
}
if let Some(relateds) = diagnostic.related() {
write!(f, r#""related": ["#)?;
for related in relateds {
self.render_report(f, related)?;
}
write!(f, "]")?;
} else {
write!(f, r#""related": []"#)?;
}
write!(f, "}}")
}
fn render_snippets(
&self,
f: &mut impl fmt::Write,
diagnostic: &(dyn Diagnostic),
) -> fmt::Result {
if let Some(source) = diagnostic.source_code() {
if let Some(mut labels) = diagnostic.labels() {
if let Some(label) = labels.next() {
if let Ok(span_content) = source.read_span(label.inner(), 0, 0) {
let filename = span_content.name().unwrap_or_default();
return write!(f, r#""filename": "{}","#, escape(filename));
}
}
}
}
write!(f, r#""filename": "","#)
}
}
impl ReportHandler for JSONReportHandler {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.render_report(f, diagnostic)
}
}

View File

@ -8,6 +8,8 @@ pub use debug::*;
#[cfg(feature = "fancy")]
pub use graphical::*;
#[allow(unreachable_pub)]
pub use json::*;
#[allow(unreachable_pub)]
pub use narratable::*;
#[allow(unreachable_pub)]
#[cfg(feature = "fancy")]
@ -16,6 +18,7 @@ pub use theme::*;
mod debug;
#[cfg(feature = "fancy")]
mod graphical;
mod json;
mod narratable;
#[cfg(feature = "fancy")]
mod theme;

789
tests/test_json.rs Normal file
View File

@ -0,0 +1,789 @@
mod json_report_handler {
use miette::{Diagnostic, MietteError, NamedSource, Report, SourceSpan};
use miette::JSONReportHandler;
use thiserror::Error;
fn fmt_report(diag: Report) -> String {
let mut out = String::new();
JSONReportHandler::new()
.render_report(&mut out, diag.as_ref())
.unwrap();
out
}
#[test]
fn single_line_with_wide_char() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
let src = "source\n 👼🏼text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 6).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 9,
"length": 6
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn single_line_highlight() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 9,
"length": 4
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn single_line_highlight_offset_zero() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (0, 0).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 0,
"length": 0
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn single_line_highlight_with_empty_span() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 0).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 9,
"length": 0
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn single_line_highlight_no_label() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"span": {
"offset": 9,
"length": 4
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
let src = "source\ntext\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (7, 4).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 7,
"length": 4
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label = "x"]
highlight1: SourceSpan,
#[label = "y"]
highlight2: SourceSpan,
#[label = "z"]
highlight3: SourceSpan,
}
let src = "source\n text text text text text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight1: (9, 4).into(),
highlight2: (14, 4).into(),
highlight3: (24, 4).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "x",
"span": {
"offset": 9,
"length": 4
}
},
{
"label": "y",
"span": {
"offset": 14,
"length": 4
}
},
{
"label": "z",
"span": {
"offset": 24,
"length": 4
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiline_highlight_adjacent() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label = "these two lines"]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 11).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "these two lines",
"span": {
"offset": 9,
"length": 11
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label = "block 1"]
highlight1: SourceSpan,
#[label = "block 2"]
highlight2: SourceSpan,
}
let src = r#"line1
line2
line3
line4
line5
"#
.to_string();
let len = src.len();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight1: (0, len).into(),
highlight2: (10, 9).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "block 1",
"span": {
"offset": 0,
"length": 50
}
},
{
"label": "block 2",
"span": {
"offset": 10,
"length": 9
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiline_highlight_no_label() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("wtf?!\nit broke :(")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source]
source: Inner,
#[source_code]
src: NamedSource,
#[label = "block 1"]
highlight1: SourceSpan,
#[label]
highlight2: SourceSpan,
}
#[derive(Debug, Error)]
#[error("something went wrong\n\nHere's a more detailed explanation of everything that actually went wrong because it's actually important.\n")]
struct Inner(#[source] InnerInner);
#[derive(Debug, Error)]
#[error("very much went wrong")]
struct InnerInner;
let src = r#"line1
line2
line3
line4
line5
"#
.to_string();
let len = src.len();
let err = MyBad {
source: Inner(InnerInner),
src: NamedSource::new("bad_file.rs", src),
highlight1: (0, len).into(),
highlight2: (10, 9).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "wtf?!\\nit broke :(",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "block 1",
"span": {
"offset": 0,
"length": 50
}
},
{
"span": {
"offset": 10,
"length": 9
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiple_multiline_highlights_adjacent() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
highlight2: SourceSpan,
}
let src = "source\n text\n here\nmore here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight1: (0, 10).into(),
highlight2: (20, 6).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 0,
"length": 10
}
},
{
"label": "also this bit",
"span": {
"offset": 20,
"length": 6
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiple_multiline_highlights_overlapping_lines() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
highlight2: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight1: (0, 8).into(),
highlight2: (9, 10).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 0,
"length": 8
}
},
{
"label": "also this bit",
"span": {
"offset": 9,
"length": 10
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiple_multiline_highlights_overlapping_offsets() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
highlight2: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight1: (0, 8).into(),
highlight2: (10, 10).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 0,
"length": 8
}
},
{
"label": "also this bit",
"span": {
"offset": 10,
"length": 10
}
}
],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn url() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(help("try doing it better next time?"), url("https://example.com"))]
struct MyBad;
let err = MyBad;
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"severity": "error",
"url": "https://example.com",
"help": "try doing it better next time?",
"labels": [],
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn related() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
#[related]
related: Vec<MyBad>,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src.clone()),
highlight: (9, 4).into(),
related: vec![MyBad {
src: NamedSource::new("bad_file2.rs", src),
highlight: (0, 6).into(),
related: vec![],
}],
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected: String = r#"
{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 9,
"length": 4
}
}
],
"related": [{
"message": "oops!",
"code": "oops::my::bad",
"severity": "error",
"help": "try doing it better next time?",
"filename": "bad_file2.rs",
"labels": [
{
"label": "this bit here",
"span": {
"offset": 0,
"length": 6
}
}
],
"related": []
}]
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
Ok(())
}
}