use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Parse(#[from] ParseError),
#[error("{0}")]
Invalid(#[from] ValidationReport),
#[error(transparent)]
Path(#[from] PathError),
#[error("unsupported: {what} (see {spec_ref})")]
Unsupported {
what: &'static str,
spec_ref: &'static str,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub kind: &'static str,
pub reason: &'static str,
pub input: String,
}
impl ParseError {
pub const MAX_ECHO: usize = 96;
#[must_use]
pub fn new(kind: &'static str, reason: &'static str, input: &str) -> Self {
let input = if input.chars().count() > Self::MAX_ECHO {
let cut: String = input.chars().take(Self::MAX_ECHO).collect();
format!("{cut}…")
} else {
input.to_owned()
};
Self {
kind,
reason,
input,
}
}
#[must_use]
pub fn invariant(class: &'static str, invariant: &'static str) -> Self {
Self {
kind: class,
reason: invariant,
input: String::new(),
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid {}: {} (got {:?})",
self.kind, self.reason, self.input
)
}
}
impl core::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum PathError {
#[error("malformed path at character {offset}: {reason}")]
Malformed {
offset: usize,
reason: &'static str,
},
#[error("path matched no node: {path}")]
NoMatch {
path: String,
},
#[error("path matched {count} nodes, expected exactly one: {path}")]
NotUnique {
path: String,
count: usize,
},
#[error("no attribute `{attribute}` on {class} (at {path})")]
UnknownAttribute {
class: &'static str,
attribute: String,
path: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Violation {
pub path: String,
pub class: &'static str,
pub invariant: &'static str,
pub detail: &'static str,
}
impl fmt::Display for Violation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {}.{} — {}",
if self.path.is_empty() {
"/"
} else {
&self.path
},
self.class,
self.invariant,
self.detail
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ValidationReport {
violations: Vec<Violation>,
}
impl ValidationReport {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, violation: Violation) {
self.violations.push(violation);
}
#[must_use]
pub fn violations(&self) -> &[Violation] {
&self.violations
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.violations.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.violations.len()
}
pub fn into_result(self) -> Result<(), Self> {
if self.is_empty() { Ok(()) } else { Err(self) }
}
}
impl fmt::Display for ValidationReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} invariant(s) violated", self.violations.len())?;
for v in &self.violations {
write!(f, "\n {v}")?;
}
Ok(())
}
}
impl core::error::Error for ValidationReport {}
pub type Result<T, E = Error> = core::result::Result<T, E>;