1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! Typed failures carrying the stable reason codes the wire contract names.
use std::fmt;
/// A stable, machine-readable refusal reason.
///
/// The consumer matches on these strings, so a variant's wire spelling is part
/// of the contract and never changes to suit a message. The
/// `unsupported_*`, `projection_profile_mismatch` and platform variants are the
/// closed set the provider kit manifest declares; the rest describe local
/// failures that occur before or during a mutation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasonCode {
/// The requested operation is not declared by this provider.
UnsupportedOperation,
/// A component kind outside the declared vocabulary.
UnsupportedComponentKind,
/// A native surface this provider does not own.
UnsupportedNativeSurface,
/// A bundle format this provider cannot read.
UnsupportedBundleFormat,
/// A protocol version this provider does not implement.
UnsupportedProtocolVersion,
/// The projection profile does not match the one the bundle was built for.
ProjectionProfileMismatch,
/// A permission profile this provider does not offer.
UnsupportedPermissionProfile,
/// The running operating system is outside the declared support matrix.
UnsupportedPlatform,
/// The running architecture is outside the declared support matrix.
UnsupportedArchitecture,
/// A journal, transaction directory or partial backup slot is present.
///
/// Planning refuses cleanly rather than guessing; only recovery may resolve
/// this state.
RecoveryRequired,
/// The target changed after the lock was taken.
Stale,
/// The target path is not a usable canonical directory.
InvalidTarget,
/// The target lock could not be acquired.
LockUnavailable,
/// Durable state could not be read, written or promoted.
StateUnavailable,
/// A digest, size or echo did not match its expected value.
IntegrityMismatch,
}
impl ReasonCode {
/// The exact string the wire surface emits.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::UnsupportedOperation => "unsupported_operation",
Self::UnsupportedComponentKind => "unsupported_component_kind",
Self::UnsupportedNativeSurface => "unsupported_native_surface",
Self::UnsupportedBundleFormat => "unsupported_bundle_format",
Self::UnsupportedProtocolVersion => "unsupported_protocol_version",
Self::ProjectionProfileMismatch => "projection_profile_mismatch",
Self::UnsupportedPermissionProfile => "unsupported_permission_profile",
Self::UnsupportedPlatform => "unsupported_platform",
Self::UnsupportedArchitecture => "unsupported_architecture",
Self::RecoveryRequired => "recovery_required",
Self::Stale => "stale",
Self::InvalidTarget => "invalid_target",
Self::LockUnavailable => "lock_unavailable",
Self::StateUnavailable => "state_unavailable",
Self::IntegrityMismatch => "integrity_mismatch",
}
}
}
impl fmt::Display for ReasonCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// A refusal: a stable reason plus a human-readable detail.
#[derive(Debug)]
pub struct Error {
reason: ReasonCode,
detail: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl Error {
/// Build a refusal from a reason and a detail.
pub fn new(reason: ReasonCode, detail: impl Into<String>) -> Self {
Self {
reason,
detail: detail.into(),
source: None,
}
}
/// Attach the underlying cause.
#[must_use]
pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(source));
self
}
/// Whether the cause underneath was "that path is not there".
///
/// Asked by the tree walk, which meets three separate gaps between listing
/// an entry and reading it and must tell a racing writer from a real
/// failure. The source is kept as a boxed `dyn Error`, so the io kind is
/// recovered by downcast rather than by matching on the message -- a guard
/// that matched prose would measure how a failure was described.
#[must_use]
pub fn is_missing_path(&self) -> bool {
self.source
.as_ref()
.and_then(|source| source.downcast_ref::<std::io::Error>())
.is_some_and(|source| source.kind() == std::io::ErrorKind::NotFound)
}
/// The stable reason code.
#[must_use]
pub const fn reason(&self) -> ReasonCode {
self.reason
}
/// The human-readable detail.
#[must_use]
pub fn detail(&self) -> &str {
&self.detail
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.reason, self.detail)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|boxed| &**boxed as &(dyn std::error::Error + 'static))
}
}
/// Result carrying a typed refusal.
pub type Result<T> = std::result::Result<T, Error>;