kevy_store/error.rs
1//! [`KevyError`] — the single error type of the embeddable stack.
2//!
3//! Defined here because `kevy-store` sits at the bottom of every
4//! consumer's dependency graph (kevy-embedded, kevy-client, kevy-persist,
5//! kevy-rt and the server all already depend on it) and is the source of
6//! the dominant structured error, [`StoreError`]. Hosting the unified
7//! type here adds no dependency edge anywhere; the protocol crate
8//! (`kevy-resp`) hosting it would require a protocol → store edge.
9
10use crate::StoreError;
11#[cfg(not(feature = "std"))]
12use crate::nostd_prelude::*;
13use core::fmt;
14#[cfg(feature = "std")]
15use std::io;
16
17/// Unified result alias over [`KevyError`].
18///
19/// ```
20/// use kevy_store::{KevyResult, Store};
21/// fn counter(s: &mut Store) -> KevyResult<i64> {
22/// Ok(s.incr_by(b"hits", 1)?)
23/// }
24/// let mut s = Store::new();
25/// assert_eq!(counter(&mut s)?, 1);
26/// # Ok::<(), kevy_store::KevyError>(())
27/// ```
28pub type KevyResult<T> = core::result::Result<T, KevyError>;
29
30/// The error type of the embeddable stack: `kevy_embedded::Store` and
31/// `kevy_client::Connection` surfaces return `Result<_, KevyError>`.
32///
33/// Structured errors stay structured — a wrong-type write arrives as
34/// `KevyError::Store(StoreError::WrongType)`, not as stringly `io::Error`
35/// text. `From<io::Error>` / `From<StoreError>` keep `?` ergonomic at
36/// both the OS boundary and the store boundary.
37///
38/// ```
39/// use kevy_store::{KevyError, StoreError};
40/// let e = KevyError::from(StoreError::WrongType);
41/// assert_eq!(e.to_string(), "store error: wrong type for this operation");
42/// assert!(std::error::Error::source(&e).is_some());
43/// ```
44#[derive(Debug)]
45#[non_exhaustive]
46pub enum KevyError {
47 /// Structured store-semantic error (wrong type, non-integer,
48 /// overflow, out-of-memory, …).
49 ///
50 /// ```
51 /// use kevy_store::{KevyError, SetCondition, Store, StoreError};
52 /// let mut s = Store::new();
53 /// s.set(b"k", b"text".to_vec(), None, SetCondition::Always);
54 /// let e = KevyError::from(s.incr_by(b"k", 1).unwrap_err());
55 /// assert!(matches!(e, KevyError::Store(StoreError::NotInteger)));
56 /// ```
57 Store(StoreError),
58 /// Operating-system / transport failure (file, socket, AOF).
59 ///
60 /// ```
61 /// use kevy_store::KevyError;
62 /// let e: KevyError = std::fs::read("/nonexistent/kevy/file").unwrap_err().into();
63 /// let KevyError::Io(io) = &e else { panic!("expected Io") };
64 /// assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
65 /// ```
66 #[cfg(feature = "std")]
67 Io(io::Error),
68 /// RESP-level failure on a client link: a server error reply
69 /// (`-ERR …` text preserved verbatim) or a malformed / unexpected
70 /// reply shape.
71 ///
72 /// ```
73 /// use kevy_store::KevyError;
74 /// let e = KevyError::Protocol("ERR unknown command".into());
75 /// assert_eq!(e.to_string(), "protocol error: ERR unknown command");
76 /// ```
77 Protocol(String),
78 /// Write rejected: the target is a read-only replica.
79 ///
80 /// ```
81 /// use kevy_store::KevyError;
82 /// let io: std::io::Error = KevyError::ReadOnly.into();
83 /// assert_eq!(io.kind(), std::io::ErrorKind::PermissionDenied);
84 /// ```
85 ReadOnly,
86 /// Invalid argument to a typed API (bad flag combination, empty
87 /// prefix, malformed URL, …). Rejected before touching any state.
88 ///
89 /// ```
90 /// use kevy_store::KevyError;
91 /// let e = KevyError::InvalidInput("empty prefix".into());
92 /// assert_eq!(e.to_string(), "invalid input: empty prefix");
93 /// ```
94 InvalidInput(String),
95 /// A named object (index, view, required key) doesn't exist.
96 ///
97 /// ```
98 /// use kevy_store::KevyError;
99 /// let io: std::io::Error = KevyError::NotFound("idx:users".into()).into();
100 /// assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
101 /// ```
102 NotFound(String),
103 /// The operation isn't available on this backend or build.
104 ///
105 /// ```
106 /// use kevy_store::KevyError;
107 /// let e = KevyError::Unsupported("tiering on wasm".into());
108 /// assert_eq!(e.to_string(), "unsupported: tiering on wasm");
109 /// ```
110 Unsupported(String),
111 /// A bounded blocking call ran out its timeout.
112 ///
113 /// ```
114 /// use kevy_store::KevyError;
115 /// let io: std::io::Error = KevyError::TimedOut.into();
116 /// assert_eq!(io.kind(), std::io::ErrorKind::TimedOut);
117 /// ```
118 TimedOut,
119 /// The connection / in-process bus is gone (EOF).
120 ///
121 /// ```
122 /// use kevy_store::KevyError;
123 /// assert_eq!(KevyError::Closed.to_string(), "connection closed");
124 /// ```
125 Closed,
126}
127
128impl fmt::Display for KevyError {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::Store(e) => write!(f, "store error: {e}"),
132 #[cfg(feature = "std")]
133 Self::Io(e) => write!(f, "io error: {e}"),
134 Self::Protocol(msg) => write!(f, "protocol error: {msg}"),
135 Self::ReadOnly => f.write_str("write refused: this is a read-only replica"),
136 Self::InvalidInput(msg) => write!(f, "invalid input: {msg}"),
137 Self::NotFound(what) => write!(f, "not found: {what}"),
138 Self::Unsupported(msg) => write!(f, "unsupported: {msg}"),
139 Self::TimedOut => write!(f, "timed out"),
140 Self::Closed => write!(f, "connection closed"),
141 }
142 }
143}
144
145impl core::error::Error for KevyError {
146 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
147 match self {
148 Self::Store(e) => Some(e),
149 #[cfg(feature = "std")]
150 Self::Io(e) => Some(e),
151 _ => None,
152 }
153 }
154}
155
156impl From<StoreError> for KevyError {
157 fn from(e: StoreError) -> Self {
158 Self::Store(e)
159 }
160}
161
162#[cfg(feature = "std")]
163impl From<io::Error> for KevyError {
164 fn from(e: io::Error) -> Self {
165 Self::Io(e)
166 }
167}
168
169/// The interop a consumer inside an `io::Result` world needs (
170/// dogfood F2): kevy provides the conversion — the orphan rule means
171/// nobody else can — so `?` works without 280 hand-rolled
172/// `io::Error::other` wrappers. Kind-mapped, and **source-preserving**:
173/// except for the `Io` passthrough, the original [`KevyError`] rides as
174/// the error's source, so a caller that later cares can downcast it
175/// back out instead of losing the type at the boundary.
176#[cfg(feature = "std")]
177impl From<KevyError> for io::Error {
178 fn from(e: KevyError) -> Self {
179 use io::ErrorKind as K;
180 match e {
181 KevyError::Io(inner) => inner,
182 other => {
183 let kind = match &other {
184 KevyError::Io(_) => unreachable!("moved out above"),
185 KevyError::TimedOut => K::TimedOut,
186 KevyError::Closed => K::ConnectionAborted,
187 KevyError::InvalidInput(_) => K::InvalidInput,
188 KevyError::NotFound(_) => K::NotFound,
189 KevyError::Unsupported(_) => K::Unsupported,
190 KevyError::ReadOnly => K::PermissionDenied,
191 KevyError::Protocol(_) => K::InvalidData,
192 KevyError::Store(StoreError::OutOfMemory) => K::OutOfMemory,
193 KevyError::Store(_) => K::InvalidData,
194 };
195 io::Error::new(kind, other)
196 }
197 }
198 }
199}
200
201#[cfg(test)]
202#[cfg(feature = "std")]
203mod io_interop_tests {
204 use super::*;
205
206 /// The kind survives the boundary and the typed
207 /// error rides as source — strictly better than the
208 /// `io::Error::other` wrapping it replaces.
209 #[test]
210 fn kinds_map_and_the_source_is_the_typed_error() {
211 let cases: [(KevyError, io::ErrorKind); 6] = [
212 (KevyError::TimedOut, io::ErrorKind::TimedOut),
213 (KevyError::Closed, io::ErrorKind::ConnectionAborted),
214 (KevyError::InvalidInput("x".into()), io::ErrorKind::InvalidInput),
215 (KevyError::NotFound("idx".into()), io::ErrorKind::NotFound),
216 (KevyError::Store(StoreError::WrongType), io::ErrorKind::InvalidData),
217 (KevyError::Store(StoreError::OutOfMemory), io::ErrorKind::OutOfMemory),
218 ];
219 for (kevy, kind) in cases {
220 let msg = kevy.to_string();
221 let io: io::Error = kevy.into();
222 assert_eq!(io.kind(), kind, "{msg}");
223 let src = io.get_ref().expect("source preserved");
224 assert!(src.is::<KevyError>(), "downcastable back to KevyError");
225 assert_eq!(src.to_string(), msg, "message survives");
226 }
227 }
228
229 /// An `Io` variant passes through untouched — no double wrap.
230 #[test]
231 fn io_passes_through_without_wrapping() {
232 let inner = io::Error::new(io::ErrorKind::BrokenPipe, "pipe");
233 let io: io::Error = KevyError::Io(inner).into();
234 assert_eq!(io.kind(), io::ErrorKind::BrokenPipe);
235 assert!(io.get_ref().is_some_and(|s| !s.is::<KevyError>()), "not re-wrapped");
236 }
237}