jsonptr/
diagnostic.rs

1//! Error reporting data structures and miette integration.
2//!
3
4use alloc::{boxed::Box, string::String};
5use core::{fmt, ops::Deref};
6
7/// Implemented by errors which can be converted into a [`Report`].
8pub trait Diagnostic: Sized {
9    /// The value which caused the error.
10    type Subject: Deref;
11
12    /// Combine the error with its subject to generate a [`Report`].
13    fn into_report(self, subject: impl Into<Self::Subject>) -> Report<Self> {
14        Report::new(self, subject.into())
15    }
16
17    /// The docs.rs URL for this error
18    fn url() -> &'static str;
19
20    /// Returns the label for the given [`Subject`] if applicable.
21    fn labels(&self, subject: &Self::Subject) -> Option<Box<dyn Iterator<Item = Label>>>;
22}
23
24/// A label for a span within a json pointer or malformed string.
25///
26#[derive(Debug, PartialEq, Eq, Clone)]
27pub struct Label {
28    text: String,
29    offset: usize,
30    len: usize,
31}
32
33impl Label {
34    /// Creates a new instance of a [`Label`] from its parts
35    pub fn new(text: String, offset: usize, len: usize) -> Self {
36        Self { text, offset, len }
37    }
38}
39
40#[cfg(feature = "miette")]
41impl From<Label> for miette::LabeledSpan {
42    fn from(value: Label) -> Self {
43        miette::LabeledSpan::new(Some(value.text), value.offset, value.len)
44    }
45}
46
47/// An enriched error wrapper which captures the original error and the subject
48/// (`String` or `PointerBuf`) which caused it, for reporting purposes.
49///
50/// This type serves two roles:
51///
52/// 1. **[`PointerBuf::parse`]**: Captures the [`ParseError`] along with the
53///    input `String`.
54///
55/// 2. **Reporting:** Provides enriched reporting capabilities, including
56///    (optional) `miette` integration, for `ParseError` and associated  errors
57///    of `assign::Assign` and `resolve::Resolve` implementations
58#[derive(Debug, Clone)]
59pub struct Report<T: Diagnostic> {
60    source: T,
61    subject: T::Subject,
62}
63
64impl<T: Diagnostic> Report<T> {
65    fn new(source: T, subject: T::Subject) -> Self {
66        Self { source, subject }
67    }
68
69    /// The value which caused the error.
70    pub fn subject(&self) -> &<T::Subject as Deref>::Target {
71        &self.subject
72    }
73
74    /// The error which occurred.
75    pub fn original(&self) -> &T {
76        &self.source
77    }
78
79    /// The original parts of the [`Report`].
80    pub fn decompose(self) -> (T, T::Subject) {
81        (self.source, self.subject)
82    }
83
84    /// Consumes the [`Report`] and returns the original error `T`.
85    pub fn into_original(self) -> T {
86        self.source
87    }
88}
89
90impl<T: Diagnostic> core::ops::Deref for Report<T> {
91    type Target = T;
92
93    fn deref(&self) -> &Self::Target {
94        &self.source
95    }
96}
97
98impl<T: Diagnostic + fmt::Display> fmt::Display for Report<T> {
99    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
100        fmt::Display::fmt(&self.source, f)
101    }
102}
103
104#[cfg(feature = "std")]
105impl<T> std::error::Error for Report<T>
106where
107    T: Diagnostic + fmt::Debug + std::error::Error + 'static,
108    T::Subject: fmt::Debug,
109{
110    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111        self.source.source()
112    }
113}
114
115#[cfg(feature = "miette")]
116impl<T> miette::Diagnostic for Report<T>
117where
118    T: Diagnostic + fmt::Debug + std::error::Error + 'static,
119    T::Subject: fmt::Debug + miette::SourceCode,
120{
121    fn url<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
122        Some(Box::new(T::url()))
123    }
124
125    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
126        Some(&self.subject)
127    }
128
129    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
130        Some(Box::new(T::labels(self, &self.subject)?.map(Into::into)))
131    }
132}
133
134macro_rules! diagnostic_url {
135    (enum $type:ident) => {
136        $crate::diagnostic::diagnostic_url!("enum", "", $type)
137    };
138    (struct $type:ident) => {
139        $crate::diagnostic::diagnostic_url!("struct", "", $type)
140    };
141    (enum $mod:ident::$type:ident) => {
142        $crate::diagnostic::diagnostic_url!("enum", concat!("/", stringify!($mod)), $type)
143    };
144    (struct $mod:ident::$type:ident) => {
145        $crate::diagnostic::diagnostic_url!("struct", concat!("/", stringify!($mod)), $type)
146    };
147    ($kind:literal, $mod:expr, $type:ident) => {
148        concat!(
149            "https://docs.rs/jsonptr/",
150            env!("CARGO_PKG_VERSION"),
151            "/jsonptr",
152            $mod,
153            "/",
154            $kind,
155            ".",
156            stringify!($type),
157            ".html",
158        )
159    };
160}
161pub(crate) use diagnostic_url;
162
163/// An extension trait for `Result<_, E>`, where `E` is an implementation of
164/// [`Diagnostic`], that converts `E` into [`Report<E>`](`Report`), yielding
165/// `Result<_, Report<E>>`.
166pub trait Diagnose<'s, T> {
167    /// The error type returned from `diagnose` and `diagnose_with`.
168    type Error: Diagnostic;
169
170    /// If the `Result` is an `Err`, converts the error into a [`Report`] with
171    /// the supplied `subject`.
172    ///
173    /// ## Example
174    /// ```
175    /// use core::any::{Any, TypeId};
176    /// use jsonptr::{Pointer, ParseError, Diagnose, Report};
177    /// let subj = "invalid/pointer";
178    /// let err = Pointer::parse(subj).diagnose(subj).unwrap_err();
179    /// assert_eq!(err.type_id(),TypeId::of::<Report<ParseError>>());
180    /// ```
181    #[allow(clippy::missing_errors_doc)]
182    fn diagnose(
183        self,
184        subject: impl Into<<Self::Error as Diagnostic>::Subject>,
185    ) -> Result<T, Report<Self::Error>>;
186
187    /// If the `Result` is an `Err`, converts the error into a [`Report`] with
188    /// the subject returned from `f`
189    ///
190    /// ## Example
191    /// ```
192    /// use core::any::{Any, TypeId};
193    /// use jsonptr::{Pointer, ParseError, Diagnose, Report};
194    /// let subj = "invalid/pointer";
195    /// let err = Pointer::parse(subj).diagnose_with(|| subj).unwrap_err();
196    ///
197    /// assert_eq!(err.type_id(),TypeId::of::<Report<ParseError>>());
198    #[allow(clippy::missing_errors_doc)]
199    fn diagnose_with<F, S>(self, f: F) -> Result<T, Report<Self::Error>>
200    where
201        F: FnOnce() -> S,
202        S: Into<<Self::Error as Diagnostic>::Subject>;
203}
204
205impl<T, E> Diagnose<'_, T> for Result<T, E>
206where
207    E: Diagnostic,
208{
209    type Error = E;
210
211    fn diagnose(
212        self,
213        subject: impl Into<<Self::Error as Diagnostic>::Subject>,
214    ) -> Result<T, Report<Self::Error>> {
215        self.map_err(|error| error.into_report(subject.into()))
216    }
217
218    fn diagnose_with<F, S>(self, f: F) -> Result<T, Report<Self::Error>>
219    where
220        F: FnOnce() -> S,
221        S: Into<<Self::Error as Diagnostic>::Subject>,
222    {
223        self.diagnose(f())
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::{Pointer, PointerBuf};
231    #[test]
232    #[cfg(all(
233        feature = "assign",
234        feature = "miette",
235        feature = "serde",
236        feature = "json"
237    ))]
238    fn assign_error() {
239        let mut v = serde_json::json!({"foo": {"bar": ["0"]}});
240        let ptr = PointerBuf::parse("/foo/bar/invalid/cannot/reach").unwrap();
241        let report = ptr.assign(&mut v, "qux").diagnose(ptr).unwrap_err();
242        println!("{:?}", miette::Report::from(report));
243
244        let ptr = PointerBuf::parse("/foo/bar/3/cannot/reach").unwrap();
245        let report = ptr.assign(&mut v, "qux").diagnose(ptr).unwrap_err();
246        println!("{:?}", miette::Report::from(report));
247    }
248
249    #[test]
250    #[cfg(all(
251        feature = "resolve",
252        feature = "miette",
253        feature = "serde",
254        feature = "json"
255    ))]
256    fn resolve_error() {
257        let v = serde_json::json!({"foo": {"bar": ["0"]}});
258        let ptr = PointerBuf::parse("/foo/bar/invalid/cannot/reach").unwrap();
259        let report = ptr.resolve(&v).diagnose(ptr).unwrap_err();
260        println!("{:?}", miette::Report::from(report));
261
262        let ptr = PointerBuf::parse("/foo/bar/3/cannot/reach").unwrap();
263        let report = ptr.resolve(&v).diagnose(ptr).unwrap_err();
264        println!("{:?}", miette::Report::from(report));
265    }
266
267    #[test]
268    #[cfg(feature = "miette")]
269    fn parse_error() {
270        let invalid = "/foo/bar/invalid~3~encoding/cannot/reach";
271        let report = Pointer::parse(invalid).diagnose(invalid).unwrap_err();
272
273        println!("{:?}", miette::Report::from(report));
274
275        let report = PointerBuf::parse("/foo/bar/invalid~3~encoding/cannot/reach").unwrap_err();
276
277        let report = miette::Report::from(report);
278        println!("{report:?}");
279    }
280}