1use alloc::{boxed::Box, string::String};
5use core::{fmt, ops::Deref};
6
7pub trait Diagnostic: Sized {
9 type Subject: Deref;
11
12 fn into_report(self, subject: impl Into<Self::Subject>) -> Report<Self> {
14 Report::new(self, subject.into())
15 }
16
17 fn url() -> &'static str;
19
20 fn labels(&self, subject: &Self::Subject) -> Option<Box<dyn Iterator<Item = Label>>>;
22}
23
24#[derive(Debug, PartialEq, Eq, Clone)]
27pub struct Label {
28 text: String,
29 offset: usize,
30 len: usize,
31}
32
33impl Label {
34 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#[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 pub fn subject(&self) -> &<T::Subject as Deref>::Target {
71 &self.subject
72 }
73
74 pub fn original(&self) -> &T {
76 &self.source
77 }
78
79 pub fn decompose(self) -> (T, T::Subject) {
81 (self.source, self.subject)
82 }
83
84 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
163pub trait Diagnose<'s, T> {
167 type Error: Diagnostic;
169
170 #[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 #[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}