use core::fmt;
#[cfg(feature = "std")]
use std::backtrace::{Backtrace, BacktraceStatus};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum ParseErrorKind {
MissingLeadingSlash,
EmptySegment,
UnbalancedBraces,
NestedVariable,
EmptyFieldPath,
InvalidFieldName,
InvalidLiteral,
RestNotLast,
EmptyVerb,
InvalidVerb,
MultipleVerbs,
}
impl ParseErrorKind {
const fn describe(self) -> &'static str {
match self {
Self::MissingLeadingSlash => "template must begin with '/'",
Self::EmptySegment => "template contains an empty path segment",
Self::UnbalancedBraces => "template contains unbalanced '{' or '}'",
Self::NestedVariable => "variable sub-templates may not contain nested variables",
Self::EmptyFieldPath => "variable has an empty field path",
Self::InvalidFieldName => "variable field path contains an invalid identifier",
Self::InvalidLiteral => "literal contains an invalid character or percent escape",
Self::RestNotLast => "'**' may only appear as the final segment",
Self::EmptyVerb => "custom verb after ':' is empty",
Self::InvalidVerb => "custom verb after ':' is not a valid path-template literal",
Self::MultipleVerbs => "template contains more than one ':' verb separator",
}
}
}
#[derive(Debug)]
enum MaybeBacktrace {
#[cfg(feature = "std")]
Captured(#[expect(dead_code, reason = "captured for Debug output and RUST_BACKTRACE diagnostics")] alloc::boxed::Box<Backtrace>),
Disabled,
}
impl MaybeBacktrace {
#[cfg(feature = "std")]
fn capture() -> Self {
Self::from_backtrace(Backtrace::capture())
}
#[cfg(not(feature = "std"))]
#[cfg_attr(coverage_nightly, coverage(off))] #[cfg_attr(test, mutants::skip)] fn capture() -> Self {
Self::Disabled
}
#[cfg(feature = "std")]
fn from_backtrace(backtrace: Backtrace) -> Self {
match backtrace.status() {
BacktraceStatus::Captured => Self::Captured(alloc::boxed::Box::new(backtrace)),
_ => Self::Disabled,
}
}
#[cfg(all(test, feature = "std"))]
fn force_capture() -> Self {
Self::from_backtrace(Backtrace::force_capture())
}
}
#[derive(Debug)]
pub struct ParseError {
kind: ParseErrorKind,
#[expect(dead_code, reason = "captured for Debug output and RUST_BACKTRACE diagnostics")]
backtrace: MaybeBacktrace,
}
impl ParseError {
pub(crate) fn new(kind: ParseErrorKind) -> Self {
Self {
kind,
backtrace: MaybeBacktrace::capture(),
}
}
#[must_use]
pub const fn is_missing_leading_slash(&self) -> bool {
matches!(self.kind, ParseErrorKind::MissingLeadingSlash)
}
#[must_use]
pub const fn is_empty_segment(&self) -> bool {
matches!(self.kind, ParseErrorKind::EmptySegment)
}
#[must_use]
pub const fn is_unbalanced_braces(&self) -> bool {
matches!(self.kind, ParseErrorKind::UnbalancedBraces)
}
#[must_use]
pub const fn is_nested_variable(&self) -> bool {
matches!(self.kind, ParseErrorKind::NestedVariable)
}
#[must_use]
pub const fn is_empty_field_path(&self) -> bool {
matches!(self.kind, ParseErrorKind::EmptyFieldPath)
}
#[must_use]
pub const fn is_invalid_field_name(&self) -> bool {
matches!(self.kind, ParseErrorKind::InvalidFieldName)
}
#[must_use]
pub const fn is_invalid_literal(&self) -> bool {
matches!(self.kind, ParseErrorKind::InvalidLiteral)
}
#[must_use]
pub const fn is_rest_not_last(&self) -> bool {
matches!(self.kind, ParseErrorKind::RestNotLast)
}
#[must_use]
pub const fn is_empty_verb(&self) -> bool {
matches!(self.kind, ParseErrorKind::EmptyVerb)
}
#[must_use]
pub const fn is_invalid_verb(&self) -> bool {
matches!(self.kind, ParseErrorKind::InvalidVerb)
}
#[must_use]
pub const fn is_multiple_verbs(&self) -> bool {
matches!(self.kind, ParseErrorKind::MultipleVerbs)
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.kind.describe())
}
}
impl core::error::Error for ParseError {}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
#[cfg_attr(
miri,
ignore = "Debug-formatting a captured Backtrace symbolicates frames via getcwd, unsupported under Miri isolation"
)]
fn force_capture_produces_a_captured_backtrace() {
use alloc::format;
let backtrace = MaybeBacktrace::force_capture();
assert!(matches!(backtrace, MaybeBacktrace::Captured(_)));
assert!(format!("{backtrace:?}").starts_with("Captured"));
}
#[test]
fn from_backtrace_stores_no_allocation_when_disabled() {
let backtrace = MaybeBacktrace::from_backtrace(Backtrace::disabled());
assert!(matches!(backtrace, MaybeBacktrace::Disabled));
}
}