From 2b71eb9dc2dae9280d14769e33bb253fad775a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1=20de=20Mello?= Date: Mon, 18 Nov 2024 01:34:36 +0000 Subject: [PATCH 1/8] rework Diagnostic trait --- src/assign.rs | 38 ++--- src/pointer.rs | 188 ++-------------------- src/report.rs | 413 ++++++++++++++----------------------------------- 3 files changed, 150 insertions(+), 489 deletions(-) diff --git a/src/assign.rs b/src/assign.rs index dc477f9..2b30439 100644 --- a/src/assign.rs +++ b/src/assign.rs @@ -38,9 +38,10 @@ use crate::{ index::{OutOfBoundsError, ParseIndexError}, - report::{impl_diagnostic_url, Diagnostic, Label, Report, Subject}, - Pointer, PointerBuf, + report::{impl_diagnostic_url, Enrich, Label}, + Pointer, }; +use alloc::borrow::Cow; use core::{ fmt::{self, Debug}, iter::once, @@ -227,40 +228,33 @@ impl fmt::Display for Error { } } -impl_diagnostic_url!(enum assign::Error); - -impl Diagnostic for Error { - type Subject = PointerBuf; - - fn into_report(self, source: Self::Subject) -> Report { - Report::new(self, source) - } +impl<'s> Enrich<'s> for Error { + type Subject = Cow<'s, Pointer>; fn url() -> &'static str { - Self::url() + impl_diagnostic_url!(enum assign::Error) } - fn labels(&self, origin: &Subject) -> Option>> { - let ptr = origin.as_pointer_buf()?; + fn labels(&self, origin: &Self::Subject) -> Option>> { let position = self.position(); - let token = ptr.get(position)?; - let offset = if self.offset() + 1 < ptr.as_str().len() { + let token = origin.get(position)?; + let offset = if self.offset() + 1 < origin.as_str().len() { self.offset() + 1 } else { self.offset() }; let len = token.encoded().len(); Some(match self { - Error::FailedToParseIndex { .. } => Box::new(once(Label { - len, + Error::FailedToParseIndex { .. } => Box::new(once(Label::new( + format!("\"{}\" is an invalid array index", token.decoded()), offset, - text: format!("\"{}\" is an invalid array index", token.decoded()), - })), - Error::OutOfBounds { source, .. } => Box::new(once(Label { + len, + ))), + Error::OutOfBounds { source, .. } => Box::new(once(Label::new( + format!("{} is out of bounds (len: {})", source.index, source.length), offset, len, - text: format!("{} is out of bounds (len: {})", source.index, source.length), - })), + ))), }) } } diff --git a/src/pointer.rs b/src/pointer.rs index 22f0cc0..44f4de5 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -1,10 +1,10 @@ use crate::{ - report::{impl_diagnostic_url, Diagnostic, Label, Report, Subject}, + report::{impl_diagnostic_url, Enrich, Enriched, Label}, token::InvalidEncodingError, Components, Token, Tokens, }; use alloc::{ - borrow::ToOwned, + borrow::{Cow, ToOwned}, boxed::Box, fmt, string::{String, ToString}, @@ -215,6 +215,7 @@ impl Pointer { ) .into() } + /// Splits the `Pointer` at the given index if the character at the index is /// a separator backslash (`'/'`), returning `Some((head, tail))`. Otherwise, /// returns `None`. @@ -910,10 +911,12 @@ impl PointerBuf { /// /// ## Errors /// Returns a [`RichParseError`] if the string is not a valid JSON Pointer. - pub fn parse(s: impl Into) -> Result { + pub fn parse<'s>(s: impl Into>) -> Result> { let s = s.into(); - validate(&s).map_err(|err| err.with_subject(s.clone()))?; - Ok(Self(s)) + match validate(&s) { + Ok(_) => Ok(Self(s.into_owned())), + Err(err) => Err(err.enrich(s)), + } } /// Creates a new `PointerBuf` from a slice of non-encoded strings. @@ -1023,25 +1026,6 @@ impl PointerBuf { pub fn clear(&mut self) { self.0.clear(); } - - pub fn offset_for_position(&self, position: usize) -> Option { - if position == 0 { - return Some(0); - } - let bytes = self.0.as_bytes(); - let mut i = 0; - let mut current = 0; - while i < bytes.len() && current <= position { - if bytes[i] == b'/' { - current += 1; - if current == position { - return Some(i + 1); - } - } - i += 1; - } - None - } } impl FromStr for PointerBuf { @@ -1207,121 +1191,6 @@ const fn validate(value: &str) -> Result<&str, ParseError> { Ok(value) } -/* -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ TopicalParseError ║ -║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -*/ -#[derive(Debug, PartialEq)] -pub struct RichParseError { - subject: String, - source: ParseError, -} - -impl RichParseError { - pub fn new(subject: String, source: ParseError) -> Self { - Self { subject, source } - } - - pub fn subject(&self) -> &str { - &self.subject - } - - pub fn source(&self) -> &ParseError { - &self.source - } - - // TODO: should this be pub? - pub(crate) fn take(self) -> (ParseError, String) { - (self.source, self.subject) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RichParseError { - fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { - Some(&self.source) - } -} - -#[cfg(feature = "miette")] -impl miette::Diagnostic for RichParseError { - fn url<'a>(&'a self) -> Option> { - Some(Box::new(Self::url())) - } - - fn source_code(&self) -> Option<&dyn miette::SourceCode> { - Some(&self.subject) - } - - fn labels(&self) -> Option + '_>> { - Some(Box::new( - self.source.create_labels(&self.subject).map(Into::into), - )) - } -} - -impl core::fmt::Display for RichParseError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - core::fmt::Display::fmt(&self.source, f) - } -} - -impl From for ParseError { - fn from(value: RichParseError) -> Self { - value.source - } -} - -impl From> for RichParseError { - fn from(value: Report) -> Self { - let (err, subj) = value.take(); - Self::new(subj.to_string(), err) - } -} - -impl From> for RichParseError { - fn from(value: Report) -> Self { - let (err, _) = value.take(); - err - } -} -impl From for Report { - fn from(err: RichParseError) -> Self { - let subj = err.subject().to_string(); - Report::new(err, subj) - } -} - -impl From for Report { - fn from(err: RichParseError) -> Self { - let (err, subj) = err.take(); - Report::new(err, subj) - } -} - -impl_diagnostic_url!(struct RichParseError); - -impl Diagnostic for RichParseError { - type Subject = (); - - fn into_report(self, (): Self::Subject) -> Report { - let subject = Subject::String(self.subject.clone()); - Report::new(self, subject) - } - - fn url() -> &'static str { - Self::url() - } - fn labels(&self, subject: &Subject) -> Option>> { - self.source.labels(subject) - } -} - /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ╔══════════════════════════════════════════════════════════════════════════════╗ @@ -1348,6 +1217,7 @@ pub enum ParseError { source: InvalidEncodingError, }, } + impl ParseError { pub fn offset(&self) -> usize { match self { @@ -1356,13 +1226,6 @@ impl ParseError { } } - pub fn with_subject(self, value: String) -> RichParseError { - RichParseError { - source: self, - subject: value, - } - } - pub fn invalid_encoding_len(&self, subject: &str) -> usize { match self { Self::NoLeadingBackslash => 0, @@ -1377,27 +1240,15 @@ impl ParseError { } } -impl_diagnostic_url!(struct ParseError); - -impl Diagnostic for ParseError { - type Subject = String; - - fn into_report(self, subject: Self::Subject) -> Report { - Report::new(self, subject) - } +impl<'s> Enrich<'s> for ParseError { + type Subject = Cow<'s, str>; fn url() -> &'static str { - Self::url() + impl_diagnostic_url!(struct ParseError) } - fn labels(&self, subject: &Subject) -> Option>> { + fn labels(&self, subject: &Self::Subject) -> Option>> { // TODO: considering searching the pointer for all invalid encodings - Some(self.create_labels(subject.as_string()?)) - } -} - -impl ParseError { - fn create_labels(&self, subject: &str) -> Box> { let offset = self.complete_offset(); let len = self.invalid_encoding_len(subject); let text = match self { @@ -1405,9 +1256,10 @@ impl ParseError { ParseError::InvalidEncoding { .. } => "'~' must be followed by '0' or '1'", } .to_string(); - Box::new(once(Label { text, offset, len })) + Some(Box::new(once(Label::new(text, offset, len)))) } } + impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -1541,8 +1393,6 @@ impl std::error::Error for ReplaceError {} mod tests { use std::error::Error; - use crate::report::Diagnose; - use super::*; use quickcheck::TestResult; use quickcheck_macros::quickcheck; @@ -1929,14 +1779,6 @@ mod tests { ); } - #[test] - fn into_report() { - let report = Pointer::parse("invalid~encoding") - .diagnose_with(|| "invalid~encoding") - .unwrap_err(); - assert_eq!(report.subject(), "invalid~encoding"); - } - #[test] fn get() { let ptr = Pointer::from_static("/0/1/2/3/4/5/6/7/8/9"); diff --git a/src/report.rs b/src/report.rs index 2dca0f4..4aa42d7 100644 --- a/src/report.rs +++ b/src/report.rs @@ -1,106 +1,122 @@ //! Error reporting data structures and miette integration. -use crate::{Pointer, PointerBuf, Token}; +use core::fmt; /// Implemented by errors which can be converted into a [`Report`]. -pub trait Diagnostic: Sized { +pub trait Enrich<'s>: Sized + private::Sealed { /// The value which caused the error. type Subject; - /// Convert the error into a [`Report`]. - fn into_report(self, subject: Self::Subject) -> Report; + /// Enrich the error with its subject. + fn enrich(self, subject: Self::Subject) -> Enriched<'s, Self> { + Enriched::new(self, subject) + } /// The docs.rs URL for this error fn url() -> &'static str; /// Returns the label for the given [`Subject`] if applicable. - fn labels(&self, subject: &Subject) -> Option>>; + fn labels(&self, subject: &Self::Subject) -> Option>>; } /// A label for a span within a json pointer or malformed string. #[derive(Debug, PartialEq, Eq, Clone)] pub struct Label { - /// The text for the label. - pub text: String, - /// The offset of the label. - pub offset: usize, - /// The length of the label. - pub len: usize, + text: String, + offset: usize, + len: usize, } impl Label { - #[cfg(feature = "miette")] - pub fn into_miette_labeled_span(self) -> miette::LabeledSpan { - miette::LabeledSpan::new(Some(self.text), self.offset, self.len) + /// Creates a new instance of a [`Label`] from its parts + pub fn new(text: String, offset: usize, len: usize) -> Self { + Self { text, offset, len } } } #[cfg(feature = "miette")] impl From