use alloc::{boxed::Box, string::String};
use core::{fmt, ops::Deref};
pub trait Diagnostic: Sized {
type Subject: Deref;
fn into_report(self, subject: impl Into<Self::Subject>) -> Report<Self> {
Report::new(self, subject.into())
}
fn url() -> &'static str;
fn labels(&self, subject: &Self::Subject) -> Option<Box<dyn Iterator<Item = Label>>>;
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Label {
text: String,
offset: usize,
len: usize,
}
impl Label {
pub fn new(text: String, offset: usize, len: usize) -> Self {
Self { text, offset, len }
}
}
#[cfg(feature = "miette")]
impl From<Label> for miette::LabeledSpan {
fn from(value: Label) -> Self {
miette::LabeledSpan::new(Some(value.text), value.offset, value.len)
}
}
#[derive(Debug, Clone)]
pub struct Report<T: Diagnostic> {
source: T,
subject: T::Subject,
}
impl<T: Diagnostic> Report<T> {
fn new(source: T, subject: T::Subject) -> Self {
Self { source, subject }
}
pub fn subject(&self) -> &<T::Subject as Deref>::Target {
&self.subject
}
pub fn original(&self) -> &T {
&self.source
}
pub fn decompose(self) -> (T, T::Subject) {
(self.source, self.subject)
}
pub fn into_original(self) -> T {
self.source
}
}
impl<T: Diagnostic> core::ops::Deref for Report<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.source
}
}
impl<T: Diagnostic + fmt::Display> fmt::Display for Report<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
fmt::Display::fmt(&self.source, f)
}
}
#[cfg(feature = "std")]
impl<T> std::error::Error for Report<T>
where
T: Diagnostic + fmt::Debug + std::error::Error + 'static,
T::Subject: fmt::Debug,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.source()
}
}
#[cfg(feature = "miette")]
impl<T> miette::Diagnostic for Report<T>
where
T: Diagnostic + fmt::Debug + std::error::Error + 'static,
T::Subject: fmt::Debug + miette::SourceCode,
{
fn url<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
Some(Box::new(T::url()))
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.subject)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
Some(Box::new(T::labels(self, &self.subject)?.map(Into::into)))
}
}
macro_rules! diagnostic_url {
(enum $type:ident) => {
$crate::diagnostic::diagnostic_url!("enum", "", $type)
};
(struct $type:ident) => {
$crate::diagnostic::diagnostic_url!("struct", "", $type)
};
(enum $mod:ident::$type:ident) => {
$crate::diagnostic::diagnostic_url!("enum", concat!("/", stringify!($mod)), $type)
};
(struct $mod:ident::$type:ident) => {
$crate::diagnostic::diagnostic_url!("struct", concat!("/", stringify!($mod)), $type)
};
($kind:literal, $mod:expr, $type:ident) => {
concat!(
"https://docs.rs/jsonptr/",
env!("CARGO_PKG_VERSION"),
"/jsonptr",
$mod,
"/",
$kind,
".",
stringify!($type),
".html",
)
};
}
pub(crate) use diagnostic_url;
pub trait Diagnose<'s, T> {
type Error: Diagnostic;
#[allow(clippy::missing_errors_doc)]
fn diagnose(
self,
subject: impl Into<<Self::Error as Diagnostic>::Subject>,
) -> Result<T, Report<Self::Error>>;
#[allow(clippy::missing_errors_doc)]
fn diagnose_with<F, S>(self, f: F) -> Result<T, Report<Self::Error>>
where
F: FnOnce() -> S,
S: Into<<Self::Error as Diagnostic>::Subject>;
}
impl<T, E> Diagnose<'_, T> for Result<T, E>
where
E: Diagnostic,
{
type Error = E;
fn diagnose(
self,
subject: impl Into<<Self::Error as Diagnostic>::Subject>,
) -> Result<T, Report<Self::Error>> {
self.map_err(|error| error.into_report(subject.into()))
}
fn diagnose_with<F, S>(self, f: F) -> Result<T, Report<Self::Error>>
where
F: FnOnce() -> S,
S: Into<<Self::Error as Diagnostic>::Subject>,
{
self.diagnose(f())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Pointer, PointerBuf};
#[test]
#[cfg(all(
feature = "assign",
feature = "miette",
feature = "serde",
feature = "json"
))]
fn assign_error() {
let mut v = serde_json::json!({"foo": {"bar": ["0"]}});
let ptr = PointerBuf::parse("/foo/bar/invalid/cannot/reach").unwrap();
let report = ptr.assign(&mut v, "qux").diagnose(ptr).unwrap_err();
println!("{:?}", miette::Report::from(report));
let ptr = PointerBuf::parse("/foo/bar/3/cannot/reach").unwrap();
let report = ptr.assign(&mut v, "qux").diagnose(ptr).unwrap_err();
println!("{:?}", miette::Report::from(report));
}
#[test]
#[cfg(all(
feature = "resolve",
feature = "miette",
feature = "serde",
feature = "json"
))]
fn resolve_error() {
let v = serde_json::json!({"foo": {"bar": ["0"]}});
let ptr = PointerBuf::parse("/foo/bar/invalid/cannot/reach").unwrap();
let report = ptr.resolve(&v).diagnose(ptr).unwrap_err();
println!("{:?}", miette::Report::from(report));
let ptr = PointerBuf::parse("/foo/bar/3/cannot/reach").unwrap();
let report = ptr.resolve(&v).diagnose(ptr).unwrap_err();
println!("{:?}", miette::Report::from(report));
}
#[test]
#[cfg(feature = "miette")]
fn parse_error() {
let invalid = "/foo/bar/invalid~3~encoding/cannot/reach";
let report = Pointer::parse(invalid).diagnose(invalid).unwrap_err();
println!("{:?}", miette::Report::from(report));
let report = PointerBuf::parse("/foo/bar/invalid~3~encoding/cannot/reach").unwrap_err();
let report = miette::Report::from(report);
println!("{report:?}");
}
}