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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
//! [`KevyError`] — the single error type of the embeddable stack.
//!
//! Defined here because `kevy-store` sits at the bottom of every
//! consumer's dependency graph (kevy-embedded, kevy-client, kevy-persist,
//! kevy-rt and the server all already depend on it) and is the source of
//! the dominant structured error, [`StoreError`]. Hosting the unified
//! type here adds no dependency edge anywhere; the protocol crate
//! (`kevy-resp`) hosting it would require a protocol → store edge.
use crate::StoreError;
#[cfg(not(feature = "std"))]
use crate::nostd_prelude::*;
use core::fmt;
#[cfg(feature = "std")]
use std::io;
/// Unified result alias over [`KevyError`].
///
/// ```
/// use kevy_store::{KevyResult, Store};
/// fn counter(s: &mut Store) -> KevyResult<i64> {
/// Ok(s.incr_by(b"hits", 1)?)
/// }
/// let mut s = Store::new();
/// assert_eq!(counter(&mut s)?, 1);
/// # Ok::<(), kevy_store::KevyError>(())
/// ```
pub type KevyResult<T> = core::result::Result<T, KevyError>;
/// The error type of the embeddable stack: `kevy_embedded::Store` and
/// `kevy_client::Connection` surfaces return `Result<_, KevyError>`.
///
/// Structured errors stay structured — a wrong-type write arrives as
/// `KevyError::Store(StoreError::WrongType)`, not as stringly `io::Error`
/// text. `From<io::Error>` / `From<StoreError>` keep `?` ergonomic at
/// both the OS boundary and the store boundary.
///
/// ```
/// use kevy_store::{KevyError, StoreError};
/// let e = KevyError::from(StoreError::WrongType);
/// assert_eq!(e.to_string(), "store error: wrong type for this operation");
/// assert!(std::error::Error::source(&e).is_some());
/// ```
#[derive(Debug)]
#[non_exhaustive]
pub enum KevyError {
/// Structured store-semantic error (wrong type, non-integer,
/// overflow, out-of-memory, …).
///
/// ```
/// use kevy_store::{KevyError, SetCondition, Store, StoreError};
/// let mut s = Store::new();
/// s.set(b"k", b"text".to_vec(), None, SetCondition::Always);
/// let e = KevyError::from(s.incr_by(b"k", 1).unwrap_err());
/// assert!(matches!(e, KevyError::Store(StoreError::NotInteger)));
/// ```
Store(StoreError),
/// Operating-system / transport failure (file, socket, AOF).
///
/// ```
/// use kevy_store::KevyError;
/// let e: KevyError = std::fs::read("/nonexistent/kevy/file").unwrap_err().into();
/// let KevyError::Io(io) = &e else { panic!("expected Io") };
/// assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
/// ```
#[cfg(feature = "std")]
Io(io::Error),
/// RESP-level failure on a client link: a server error reply
/// (`-ERR …` text preserved verbatim) or a malformed / unexpected
/// reply shape.
///
/// ```
/// use kevy_store::KevyError;
/// let e = KevyError::Protocol("ERR unknown command".into());
/// assert_eq!(e.to_string(), "protocol error: ERR unknown command");
/// ```
Protocol(String),
/// Write rejected: the target is a read-only replica.
///
/// ```
/// use kevy_store::KevyError;
/// let io: std::io::Error = KevyError::ReadOnly.into();
/// assert_eq!(io.kind(), std::io::ErrorKind::PermissionDenied);
/// ```
ReadOnly,
/// Invalid argument to a typed API (bad flag combination, empty
/// prefix, malformed URL, …). Rejected before touching any state.
///
/// ```
/// use kevy_store::KevyError;
/// let e = KevyError::InvalidInput("empty prefix".into());
/// assert_eq!(e.to_string(), "invalid input: empty prefix");
/// ```
InvalidInput(String),
/// A named object (index, view, required key) doesn't exist.
///
/// ```
/// use kevy_store::KevyError;
/// let io: std::io::Error = KevyError::NotFound("idx:users".into()).into();
/// assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
/// ```
NotFound(String),
/// The operation isn't available on this backend or build.
///
/// ```
/// use kevy_store::KevyError;
/// let e = KevyError::Unsupported("tiering on wasm".into());
/// assert_eq!(e.to_string(), "unsupported: tiering on wasm");
/// ```
Unsupported(String),
/// A bounded blocking call ran out its timeout.
///
/// ```
/// use kevy_store::KevyError;
/// let io: std::io::Error = KevyError::TimedOut.into();
/// assert_eq!(io.kind(), std::io::ErrorKind::TimedOut);
/// ```
TimedOut,
/// The connection / in-process bus is gone (EOF).
///
/// ```
/// use kevy_store::KevyError;
/// assert_eq!(KevyError::Closed.to_string(), "connection closed");
/// ```
Closed,
}
impl fmt::Display for KevyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Store(e) => write!(f, "store error: {e}"),
#[cfg(feature = "std")]
Self::Io(e) => write!(f, "io error: {e}"),
Self::Protocol(msg) => write!(f, "protocol error: {msg}"),
Self::ReadOnly => f.write_str("write refused: this is a read-only replica"),
Self::InvalidInput(msg) => write!(f, "invalid input: {msg}"),
Self::NotFound(what) => write!(f, "not found: {what}"),
Self::Unsupported(msg) => write!(f, "unsupported: {msg}"),
Self::TimedOut => write!(f, "timed out"),
Self::Closed => write!(f, "connection closed"),
}
}
}
impl core::error::Error for KevyError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Store(e) => Some(e),
#[cfg(feature = "std")]
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<StoreError> for KevyError {
fn from(e: StoreError) -> Self {
Self::Store(e)
}
}
#[cfg(feature = "std")]
impl From<io::Error> for KevyError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
/// The interop a consumer inside an `io::Result` world needs (
/// dogfood F2): kevy provides the conversion — the orphan rule means
/// nobody else can — so `?` works without 280 hand-rolled
/// `io::Error::other` wrappers. Kind-mapped, and **source-preserving**:
/// except for the `Io` passthrough, the original [`KevyError`] rides as
/// the error's source, so a caller that later cares can downcast it
/// back out instead of losing the type at the boundary.
#[cfg(feature = "std")]
impl From<KevyError> for io::Error {
fn from(e: KevyError) -> Self {
use io::ErrorKind as K;
match e {
KevyError::Io(inner) => inner,
other => {
let kind = match &other {
KevyError::Io(_) => unreachable!("moved out above"),
KevyError::TimedOut => K::TimedOut,
KevyError::Closed => K::ConnectionAborted,
KevyError::InvalidInput(_) => K::InvalidInput,
KevyError::NotFound(_) => K::NotFound,
KevyError::Unsupported(_) => K::Unsupported,
KevyError::ReadOnly => K::PermissionDenied,
KevyError::Protocol(_) => K::InvalidData,
KevyError::Store(StoreError::OutOfMemory) => K::OutOfMemory,
KevyError::Store(_) => K::InvalidData,
};
io::Error::new(kind, other)
}
}
}
}
#[cfg(test)]
#[cfg(feature = "std")]
mod io_interop_tests {
use super::*;
/// The kind survives the boundary and the typed
/// error rides as source — strictly better than the
/// `io::Error::other` wrapping it replaces.
#[test]
fn kinds_map_and_the_source_is_the_typed_error() {
let cases: [(KevyError, io::ErrorKind); 6] = [
(KevyError::TimedOut, io::ErrorKind::TimedOut),
(KevyError::Closed, io::ErrorKind::ConnectionAborted),
(KevyError::InvalidInput("x".into()), io::ErrorKind::InvalidInput),
(KevyError::NotFound("idx".into()), io::ErrorKind::NotFound),
(KevyError::Store(StoreError::WrongType), io::ErrorKind::InvalidData),
(KevyError::Store(StoreError::OutOfMemory), io::ErrorKind::OutOfMemory),
];
for (kevy, kind) in cases {
let msg = kevy.to_string();
let io: io::Error = kevy.into();
assert_eq!(io.kind(), kind, "{msg}");
let src = io.get_ref().expect("source preserved");
assert!(src.is::<KevyError>(), "downcastable back to KevyError");
assert_eq!(src.to_string(), msg, "message survives");
}
}
/// An `Io` variant passes through untouched — no double wrap.
#[test]
fn io_passes_through_without_wrapping() {
let inner = io::Error::new(io::ErrorKind::BrokenPipe, "pipe");
let io: io::Error = KevyError::Io(inner).into();
assert_eq!(io.kind(), io::ErrorKind::BrokenPipe);
assert!(io.get_ref().is_some_and(|s| !s.is::<KevyError>()), "not re-wrapped");
}
}