Skip to main content

gix_error/concrete/
metadata.rs

1use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
2
3use bstr::BString;
4
5use crate::{Class, ResourceExhaustionKind};
6
7/// An ordered dictionary of named diagnostic values belonging to a single error context.
8///
9/// Functions returning metadata document the keys and their meaning. [`crate::Exn::metadata()`] and
10/// [`crate::Error::metadata()`] yield non-empty dictionaries separately; dictionaries from independent causes
11/// are never merged.
12pub type Metadata = BTreeMap<Cow<'static, str>, MetadataValue>;
13
14/// A diagnostic message with an optional semantic class and named diagnostic values.
15///
16/// Use this instead of chaining message, classification, and scalar-context errors when they describe a single
17/// failure. [`Self::new()`] starts without a class or values; [`Self::with_class()`] and [`Self::with()`] add them.
18/// Class-based constructors such as [`crate::not_found()`] combine the message and class in one step.
19///
20/// Unlike [`ClassificationMarker`](crate::ClassificationMarker), this is a visible diagnostic: it participates in
21/// error iteration, downcasting, reports, and cause selection. A marker only adds a classification to an existing
22/// error without a diagnostic of its own, preserving that error's concrete type. Both are inspected by [`crate::classify()`].
23/// The class itself isn't displayed, and [`crate::types::Classification::error()`] refers to this error, not a synthetic source.
24///
25/// Preserve real callee errors with [`ResultExt::or_raise()`](crate::ResultExt::or_raise) or
26/// [`Exn::raise()`](crate::Exn::raise). Keep concrete error types when recovery requires their specific payloads;
27/// use classification predicates to recognize categories, and document diagnostic keys on the function returning them.
28/// [`Exn::metadata()`](crate::Exn::metadata) and [`crate::Error::metadata()`] yield each message's non-empty value dictionary.
29/// Dictionaries from separate contexts aren't merged. To identify a specific failure without inspecting its values,
30/// use [`Class::Tagged`]; see [matching a specific failure](crate#matching-a-specific-failure).
31///
32/// Debug formatting omits absent classes and empty values. Present classes omit their `Some` wrapper,
33/// and the class and values stay on single lines, even in pretty output.
34pub struct Message {
35    /// The operation or situation described by these values.
36    pub message: Cow<'static, str>,
37    /// The semantic class of this diagnostic, if known.
38    pub class: Option<Class>,
39    /// Diagnostic values, ordered by key. Functions returning metadata document their keys.
40    pub values: Metadata,
41}
42
43impl Message {
44    /// Create a diagnostic with `message`, no classification, and no values.
45    pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
46        Self {
47            message: message.into(),
48            class: None,
49            values: Metadata::new(),
50        }
51    }
52
53    /// Set `class`, replacing any previous classification without adding a cause.
54    pub fn with_class(mut self, class: Class) -> Self {
55        self.class = Some(class);
56        self
57    }
58
59    /// Add `value` under `key`, replacing any previous value in this context.
60    /// Inspect values through [`crate::Exn::metadata()`] after raising, or [`crate::Error::metadata()`] after wrapping.
61    pub fn with(mut self, key: impl Into<Cow<'static, str>>, value: impl Into<MetadataValue>) -> Self {
62        self.values.insert(key.into(), value.into());
63        self
64    }
65}
66
67impl fmt::Debug for Message {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        let mut debug = f.debug_struct("Message");
70        debug.field("message", &self.message);
71        if let Some(class) = self.class {
72            debug.field("class", &format_args!("{class:?}"));
73        }
74        if !self.values.is_empty() {
75            debug.field("values", &format_args!("{:?}", self.values));
76        }
77        debug.finish()
78    }
79}
80
81impl fmt::Display for Message {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.write_str(&self.message)?;
84        for (key, value) in &self.values {
85            write!(f, ", {key:?}={value}")?;
86        }
87        Ok(())
88    }
89}
90
91impl std::error::Error for Message {}
92
93impl From<Cow<'static, str>> for Message {
94    fn from(message: Cow<'static, str>) -> Self {
95        Self::new(message)
96    }
97}
98
99impl From<String> for Message {
100    fn from(message: String) -> Self {
101        Self::new(message)
102    }
103}
104
105impl From<&'static str> for Message {
106    fn from(message: &'static str) -> Self {
107        Self::new(message)
108    }
109}
110
111/// Create a diagnostic for invalid function or method input, classified as [`Class::Validation`].
112pub fn validation(message: impl Into<Cow<'static, str>>) -> Message {
113    Message::new(message).with_class(Class::Validation)
114}
115
116/// Create a diagnostic for malformed or internally inconsistent data, classified as [`Class::Corruption`].
117pub fn corruption(message: impl Into<Cow<'static, str>>) -> Message {
118    Message::new(message).with_class(Class::Corruption)
119}
120
121/// Create a diagnostic for a missing resource, classified as [`Class::NotFound`].
122pub fn not_found(message: impl Into<Cow<'static, str>>) -> Message {
123    Message::new(message).with_class(Class::NotFound)
124}
125
126/// Create a diagnostic for an operation that may succeed when retried, classified as [`Class::Retryable`].
127pub fn retryable(message: impl Into<Cow<'static, str>>) -> Message {
128    Message::new(message).with_class(Class::Retryable)
129}
130
131/// Create a diagnostic for an exhausted resource, classified as [`Class::ResourceExhaustion`] of `kind`.
132pub fn resource_exhaustion(kind: ResourceExhaustionKind, message: impl Into<Cow<'static, str>>) -> Message {
133    Message::new(message).with_class(Class::ResourceExhaustion(kind))
134}
135
136/// Create a diagnostic for an exceeded application-configured allocation limit.
137pub fn allocation_limit(message: impl Into<Cow<'static, str>>) -> Message {
138    resource_exhaustion(ResourceExhaustionKind::AllocationLimit, message)
139}
140
141/// Create a diagnostic for an unrepresentable allocation size or memory that could not be reserved.
142pub fn allocation_failure(message: impl Into<Cow<'static, str>>) -> Message {
143    resource_exhaustion(ResourceExhaustionKind::AllocationFailure, message)
144}
145
146/// Create a diagnostic classified as [`Class::Io`] of `kind`, without an original [`std::io::Error`].
147///
148/// This does not supply an I/O origin for [`crate::types::Classification::io_kind()`]. When an actual I/O error is
149/// available, preserve it as a cause with [`ResultExt::or_raise()`](crate::ResultExt::or_raise) instead.
150pub fn io(kind: std::io::ErrorKind, message: impl Into<Cow<'static, str>>) -> Message {
151    Message::new(message).with_class(Class::Io(kind))
152}
153
154/// An owned scalar value in a [`Metadata`] dictionary. Bytes and native paths retain their original representation.
155///
156/// Debug formatting keeps the variant and its value on a single line, even in pretty output.
157#[derive(Clone, PartialEq)]
158#[non_exhaustive]
159pub enum MetadataValue {
160    /// A boolean.
161    Bool(bool),
162    /// A signed integer.
163    I64(i64),
164    /// An unsigned integer.
165    U64(u64),
166    /// A floating-point number.
167    F64(f64),
168    /// UTF-8 text.
169    String(String),
170    /// An arbitrary byte string.
171    Bytes(BString),
172    /// A native filesystem path.
173    Path(PathBuf),
174}
175
176impl fmt::Debug for MetadataValue {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            MetadataValue::Bool(value) => write!(f, "Bool({value:?})"),
180            MetadataValue::I64(value) => write!(f, "I64({value:?})"),
181            MetadataValue::U64(value) => write!(f, "U64({value:?})"),
182            MetadataValue::F64(value) => write!(f, "F64({value:?})"),
183            MetadataValue::String(value) => write!(f, "String({value:?})"),
184            MetadataValue::Bytes(value) => write!(f, "Bytes({value:?})"),
185            MetadataValue::Path(value) => write!(f, "Path({value:?})"),
186        }
187    }
188}
189
190impl fmt::Display for MetadataValue {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        match self {
193            MetadataValue::Bool(value) => fmt::Display::fmt(value, f),
194            MetadataValue::I64(value) => fmt::Display::fmt(value, f),
195            MetadataValue::U64(value) => fmt::Display::fmt(value, f),
196            MetadataValue::F64(value) => fmt::Display::fmt(value, f),
197            MetadataValue::String(value) => fmt::Debug::fmt(value, f),
198            MetadataValue::Bytes(value) => fmt::Debug::fmt(value, f),
199            MetadataValue::Path(value) => fmt::Debug::fmt(value, f),
200        }
201    }
202}
203
204macro_rules! from {
205    ($variant:ident: $($ty:ty),+ $(,)?) => {
206        $(impl From<$ty> for MetadataValue {
207            fn from(value: $ty) -> Self {
208                Self::$variant(value.into())
209            }
210        })+
211    };
212}
213
214from!(Bool: bool);
215from!(I64: i8, i16, i32, i64);
216from!(U64: u8, u16, u32, u64);
217from!(F64: f32, f64);
218from!(String: String, &str);
219from!(Bytes: BString, &bstr::BStr, Vec<u8>, &[u8]);
220from!(Path: PathBuf, &std::path::Path);
221
222impl From<usize> for MetadataValue {
223    fn from(value: usize) -> Self {
224        Self::U64(value as u64)
225    }
226}
227
228impl From<isize> for MetadataValue {
229    fn from(value: isize) -> Self {
230        Self::I64(value as i64)
231    }
232}