From c25676cb1f4266c2607836e6359f15b9cbd8637e Mon Sep 17 00:00:00 2001 From: Gavrilikhin Daniil Date: Sun, 14 May 2023 16:43:40 +0800 Subject: [PATCH] feat(serde): Add `serde` support (#264) Fixes: https://github.com/zkat/miette/issues/260 --- Cargo.toml | 3 + src/eyreish/macros.rs | 5 +- src/miette_diagnostic.rs | 115 +++++++++++++++++++++++++++++ src/protocol.rs | 151 +++++++++++++++++++++++++++++++++------ 4 files changed, 251 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 61d357a..96b9c9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ supports-unicode = { version = "2.0.0", optional = true } backtrace = { version = "0.3.61", optional = true } terminal_size = { version = "0.1.17", optional = true } backtrace-ext = { version = "0.2.1", optional = true } +serde = { version = "1.0.162", features = ["derive"], optional = true } [dev-dependencies] semver = "1.0.4" @@ -40,6 +41,8 @@ syn = { version = "2.0", features = ["full"] } regex = "1.5" lazy_static = "1.4" +serde_json = "1.0.64" + [features] default = [] no-format-args-capture = [] diff --git a/src/eyreish/macros.rs b/src/eyreish/macros.rs index 50605e7..938ac32 100644 --- a/src/eyreish/macros.rs +++ b/src/eyreish/macros.rs @@ -284,7 +284,10 @@ macro_rules! miette { /// ``` #[macro_export] macro_rules! diagnostic { - ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {{ + ($fmt:literal $($arg:tt)*) => {{ + $crate::MietteDiagnostic::new(format!($fmt $($arg)*)) + }}; + ($($key:ident = $value:expr,)+ $fmt:literal $($arg:tt)*) => {{ let mut diag = $crate::MietteDiagnostic::new(format!($fmt $($arg)*)); $(diag.$key = Some($value.into());)* diag diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs index 29a0c80..67b75d0 100644 --- a/src/miette_diagnostic.rs +++ b/src/miette_diagnostic.rs @@ -3,10 +3,14 @@ use std::{ fmt::{Debug, Display}, }; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + use crate::{Diagnostic, LabeledSpan, Severity}; /// Diagnostic that can be created at runtime. #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct MietteDiagnostic { /// Displayed diagnostic message pub message: String, @@ -15,17 +19,22 @@ pub struct MietteDiagnostic { /// in the toplevel crate's documentation for easy searching. /// Rust path format (`foo::bar::baz`) is recommended, but more classic /// codes like `E0123` will work just fine + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub code: Option, /// [`Diagnostic`] severity. Intended to be used by /// [`ReportHandler`](crate::ReportHandler)s to change the way different /// [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub severity: Option, /// Additional help text related to this Diagnostic + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub help: Option, /// URL to visit for a more detailed explanation/help about this /// [`Diagnostic`]. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub url: Option, /// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub labels: Option>, } @@ -248,3 +257,109 @@ impl MietteDiagnostic { self } } + +#[cfg(feature = "serde")] +#[test] +fn test_serialize_miette_diagnostic() { + use serde_json::json; + + use crate::diagnostic; + + let diag = diagnostic!("message"); + let json = json!({ "message": "message" }); + assert_eq!(json!(diag), json); + + let diag = diagnostic!( + code = "code", + help = "help", + url = "url", + labels = [ + LabeledSpan::at_offset(0, "label1"), + LabeledSpan::at(1..3, "label2") + ], + severity = Severity::Warning, + "message" + ); + let json = json!({ + "message": "message", + "code": "code", + "help": "help", + "url": "url", + "severity": "Warning", + "labels": [ + { + "span": { + "offset": 0, + "length": 0 + }, + "label": "label1" + }, + { + "span": { + "offset": 1, + "length": 2 + }, + "label": "label2" + } + ] + }); + assert_eq!(json!(diag), json); +} + +#[cfg(feature = "serde")] +#[test] +fn test_deserialize_miette_diagnostic() { + use serde_json::json; + + use crate::diagnostic; + + let json = json!({ "message": "message" }); + let diag = diagnostic!("message"); + assert_eq!(diag, serde_json::from_value(json).unwrap()); + + let json = json!({ + "message": "message", + "help": null, + "code": null, + "severity": null, + "url": null, + "labels": null + }); + assert_eq!(diag, serde_json::from_value(json).unwrap()); + + let diag = diagnostic!( + code = "code", + help = "help", + url = "url", + labels = [ + LabeledSpan::at_offset(0, "label1"), + LabeledSpan::at(1..3, "label2") + ], + severity = Severity::Warning, + "message" + ); + let json = json!({ + "message": "message", + "code": "code", + "help": "help", + "url": "url", + "severity": "Warning", + "labels": [ + { + "span": { + "offset": 0, + "length": 0 + }, + "label": "label1" + }, + { + "span": { + "offset": 1, + "length": 2 + }, + "label": "label2" + } + ] + }); + assert_eq!(diag, serde_json::from_value(json).unwrap()); +} diff --git a/src/protocol.rs b/src/protocol.rs index 0f606f6..611b827 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -9,6 +9,9 @@ use std::{ panic::Location, }; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + use crate::MietteError; /// Adds rich metadata to your Error that can be used by @@ -163,6 +166,7 @@ impl From> for Box, span: SourceSpan, } impl LabeledSpan { /// Makes a new labeled span. - pub fn new(label: Option, offset: ByteOffset, len: ByteOffset) -> Self { + pub fn new(label: Option, offset: ByteOffset, len: usize) -> Self { Self { label, span: (offset, len).into(), @@ -299,6 +330,53 @@ impl LabeledSpan { } } +#[cfg(feature = "serde")] +#[test] +fn test_serialize_labeled_span() { + use serde_json::json; + + assert_eq!( + json!(LabeledSpan::new(None, 0, 0)), + json!({ + "span": { "offset": 0, "length": 0 } + }) + ); + + assert_eq!( + json!(LabeledSpan::new(Some("label".to_string()), 0, 0)), + json!({ + "label": "label", + "span": { "offset": 0, "length": 0 } + }) + ) +} + +#[cfg(feature = "serde")] +#[test] +fn test_deserialize_labeled_span() { + use serde_json::json; + + let span: LabeledSpan = serde_json::from_value(json!({ + "label": null, + "span": { "offset": 0, "length": 0 } + })) + .unwrap(); + assert_eq!(span, LabeledSpan::new(None, 0, 0)); + + let span: LabeledSpan = serde_json::from_value(json!({ + "span": { "offset": 0, "length": 0 } + })) + .unwrap(); + assert_eq!(span, LabeledSpan::new(None, 0, 0)); + + let span: LabeledSpan = serde_json::from_value(json!({ + "label": "label", + "span": { "offset": 0, "length": 0 } + })) + .unwrap(); + assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0)) +} + /** Contents of a [`SourceCode`] covered by [`SourceSpan`]. @@ -402,15 +480,14 @@ impl<'a> SpanContents<'a> for MietteSpanContents<'a> { } } -/** -Span within a [`SourceCode`] with an associated message. -*/ +/// Span within a [`SourceCode`] #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SourceSpan { /// The start of the span. offset: SourceOffset, - /// The total length of the span. Think of this as an offset from `start`. - length: SourceOffset, + /// The total length of the span + length: usize, } impl SourceSpan { @@ -418,7 +495,7 @@ impl SourceSpan { pub fn new(start: SourceOffset, length: SourceOffset) -> Self { Self { offset: start, - length, + length: length.offset(), } } @@ -429,31 +506,28 @@ impl SourceSpan { /// Total length of the [`SourceSpan`], in bytes. pub fn len(&self) -> usize { - self.length.offset() + self.length } /// Whether this [`SourceSpan`] has a length of zero. It may still be useful /// to point to a specific point. pub fn is_empty(&self) -> bool { - self.length.offset() == 0 + self.length == 0 } } -impl From<(ByteOffset, ByteOffset)> for SourceSpan { - fn from((start, len): (ByteOffset, ByteOffset)) -> Self { +impl From<(ByteOffset, usize)> for SourceSpan { + fn from((start, len): (ByteOffset, usize)) -> Self { Self { offset: start.into(), - length: len.into(), + length: len, } } } impl From<(SourceOffset, SourceOffset)> for SourceSpan { fn from((start, len): (SourceOffset, SourceOffset)) -> Self { - Self { - offset: start, - length: len, - } + Self::new(start, len) } } @@ -461,17 +535,14 @@ impl From> for SourceSpan { fn from(range: std::ops::Range) -> Self { Self { offset: range.start.into(), - length: range.len().into(), + length: range.len(), } } } impl From for SourceSpan { fn from(offset: SourceOffset) -> Self { - Self { - offset, - length: 0.into(), - } + Self { offset, length: 0 } } } @@ -479,11 +550,31 @@ impl From for SourceSpan { fn from(offset: ByteOffset) -> Self { Self { offset: offset.into(), - length: 0.into(), + length: 0, } } } +#[cfg(feature = "serde")] +#[test] +fn test_serialize_source_span() { + use serde_json::json; + + assert_eq!( + json!(SourceSpan::from(0)), + json!({ "offset": 0, "length": 0}) + ) +} + +#[cfg(feature = "serde")] +#[test] +fn test_deserialize_source_span() { + use serde_json::json; + + let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap(); + assert_eq!(span, SourceSpan::from(0)) +} + /** "Raw" type for the byte offset from the beginning of a [`SourceCode`]. */ @@ -493,6 +584,7 @@ pub type ByteOffset = usize; Newtype that represents the [`ByteOffset`] from the beginning of a [`SourceCode`] */ #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SourceOffset(ByteOffset); impl SourceOffset { @@ -576,3 +668,18 @@ fn test_source_offset_from_location() { source.len() ); } + +#[cfg(feature = "serde")] +#[test] +fn test_serialize_source_offset() { + use serde_json::json; + + assert_eq!(json!(SourceOffset::from(0)), 0) +} + +#[cfg(feature = "serde")] +#[test] +fn test_deserialize_source_offset() { + let offset: SourceOffset = serde_json::from_str("0").unwrap(); + assert_eq!(offset, SourceOffset::from(0)) +}