diskann_record/load/error.rs
1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Load-side error type and classification.
7//!
8//! The [`Error`] type wraps [`anyhow::Error`] for rich diagnostics and carries a
9//! recoverable / critical bit used by probing call sites. The [`Kind`] enum enumerates
10//! the well-known structural failure modes; [`Kind::is_recoverable`] is the canonical
11//! source of truth for the recoverable / critical classification.
12
13use std::fmt::{Debug, Display};
14
15/// A specialized [`std::result::Result`] for load-side operations.
16pub type Result<T> = ::std::result::Result<T, Error>;
17
18/// Load-side error.
19///
20/// Carries an inner [`anyhow::Error`] for rich diagnostics (chained context,
21/// backtraces) along with a single `recoverable` bit. Recoverable errors are
22/// the contract for probing APIs: a caller that tries multiple load strategies
23/// (e.g. current version, then legacy) can distinguish "this attempt didn't
24/// match, try another" from "the data is broken, stop now".
25///
26/// Most constructors produce *critical* (non-recoverable) errors. Probing
27/// call sites use the explicit `*_recoverable` constructors, or rely on the
28/// [`From<Kind>`] impl which classifies each [`Kind`] variant according to
29/// [`Kind::is_recoverable`].
30#[derive(Debug)]
31pub struct Error {
32 inner: anyhow::Error,
33 recoverable: bool,
34}
35
36impl Error {
37 /// Construct a critical error from an underlying source error.
38 pub fn new<E>(err: E) -> Self
39 where
40 E: std::error::Error + Send + Sync + 'static,
41 {
42 Self {
43 inner: anyhow::Error::new(err),
44 recoverable: false,
45 }
46 }
47
48 /// Construct a critical error from a display message.
49 pub fn message<D>(message: D) -> Self
50 where
51 D: Display + Debug + Send + Sync + 'static,
52 {
53 Self {
54 inner: anyhow::Error::msg(message),
55 recoverable: false,
56 }
57 }
58
59 /// Construct a recoverable error from an underlying source. Suitable for
60 /// probing APIs that may attempt an alternative load strategy.
61 pub fn new_recoverable<E>(err: E) -> Self
62 where
63 E: std::error::Error + Send + Sync + 'static,
64 {
65 Self {
66 inner: anyhow::Error::new(err),
67 recoverable: true,
68 }
69 }
70
71 /// Construct a recoverable error from a display message. Suitable for
72 /// probing APIs that may attempt an alternative load strategy.
73 pub fn message_recoverable<D>(message: D) -> Self
74 where
75 D: Display + Debug + Send + Sync + 'static,
76 {
77 Self {
78 inner: anyhow::Error::msg(message),
79 recoverable: true,
80 }
81 }
82
83 /// Attach additional context. The `recoverable` flag is preserved.
84 pub fn context<D>(self, message: D) -> Self
85 where
86 D: Display + Send + Sync + 'static,
87 {
88 Self {
89 inner: self.inner.context(message),
90 recoverable: self.recoverable,
91 }
92 }
93
94 /// Returns `true` if this error is recoverable. Probing call sites should
95 /// only fall back to alternative load strategies when this is `true`.
96 pub fn is_recoverable(&self) -> bool {
97 self.recoverable
98 }
99}
100
101impl Display for Error {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 write!(f, "Load Error: {:?}", self.inner)
104 }
105}
106
107impl std::error::Error for Error {
108 /// Returns the lower-level source of this error, if it exists.
109 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
110 Some(self.inner.as_ref())
111 }
112}
113
114/// Well-known classes of load-side failure.
115///
116/// Used in two roles:
117///
118/// * As the source of an [`Error`] via `From<Kind>` (and the matching `From<Kind>` for
119/// [`Error`] which classifies recoverable / critical according to
120/// [`Kind::is_recoverable`]).
121/// * As a probe value in error chains — high-level callers can introspect the kind to
122/// decide whether to try a fallback loader.
123#[derive(Debug, Clone, Copy)]
124#[non_exhaustive]
125pub enum Kind {
126 /// The manifest's `$version` does not match the loader's expected
127 /// [`Load::VERSION`](crate::load::Load::VERSION).
128 VersionMismatch,
129 /// A required field is absent from the record.
130 MissingField,
131 /// The shape of the saved value does not match what the loader expected (e.g. found
132 /// an array where an object was needed).
133 TypeMismatch,
134 /// The manifest's version is recognized as not matching the current schema, and the
135 /// type's [`Load::load_legacy`](crate::load::Load::load_legacy) has no upgrade path
136 /// for it.
137 UnknownVersion,
138 /// The variant tag read from the wire format does not match any known
139 /// variant of the target enum.
140 UnknownVariant,
141 /// A numeric value in the manifest does not fit in the requested Rust type
142 /// (either out of range or would lose precision).
143 NumberOutOfRange,
144 /// A `$handle` references a file name that is not registered in the
145 /// manifest's `files` set.
146 MissingFile,
147}
148
149impl Kind {
150 /// Stable, human-readable description of this kind. Used as the default error
151 /// message when constructing an [`Error`] from a `Kind`.
152 pub const fn as_str(self) -> &'static str {
153 match self {
154 Self::VersionMismatch => "version mismatch",
155 Self::MissingField => "missing field",
156 Self::TypeMismatch => "type mismatch",
157 Self::UnknownVersion => "unknown version",
158 Self::UnknownVariant => "unknown variant",
159 Self::NumberOutOfRange => "number out of range for target type",
160 Self::MissingFile => "handle references a file not present in the manifest",
161 }
162 }
163
164 /// Whether an error of this kind should be treated as recoverable by
165 /// probing APIs (i.e., suitable for triggering a fallback to an alternative
166 /// load strategy).
167 ///
168 /// Recoverable kinds describe "the data did not match what this loader
169 /// expected" (a different version or shape might still succeed). Critical
170 /// kinds describe structural or integrity problems where retrying would be
171 /// pointless or unsafe.
172 pub const fn is_recoverable(self) -> bool {
173 match self {
174 // Shape/version probing signals — another loader might succeed.
175 Self::VersionMismatch | Self::MissingField | Self::TypeMismatch => true,
176 // Structural / integrity failures — give up.
177 Self::UnknownVersion
178 | Self::UnknownVariant
179 | Self::NumberOutOfRange
180 | Self::MissingFile => false,
181 }
182 }
183}
184
185impl std::error::Error for Kind {}
186
187impl std::fmt::Display for Kind {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.write_str(self.as_str())
190 }
191}
192
193impl From<Kind> for Error {
194 fn from(kind: Kind) -> Self {
195 Self {
196 inner: anyhow::Error::new(kind),
197 recoverable: kind.is_recoverable(),
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn context_preserves_recoverable_flag() {
208 assert!(
209 Error::from(Kind::TypeMismatch)
210 .context("extra")
211 .is_recoverable()
212 );
213 assert!(
214 !Error::from(Kind::MissingFile)
215 .context("extra")
216 .is_recoverable()
217 );
218 }
219}