Skip to main content

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
10#[cfg(not(feature = "std"))]
11use crate::nostd_prelude::*;
12use crate::StoreError;
13use core::fmt;
14#[cfg(feature = "std")]
15use std::io;
16
17/// Unified result alias over [`KevyError`].
18pub type KevyResult<T> = core::result::Result<T, KevyError>;
19
20/// The error type of the embeddable stack: `kevy_embedded::Store` and
21/// `kevy_client::Connection` surfaces return `Result<_, KevyError>`.
22///
23/// Structured errors stay structured — a wrong-type write arrives as
24/// `KevyError::Store(StoreError::WrongType)`, not as stringly `io::Error`
25/// text. `From<io::Error>` / `From<StoreError>` keep `?` ergonomic at
26/// both the OS boundary and the store boundary.
27#[derive(Debug)]
28pub enum KevyError {
29    /// Structured store-semantic error (wrong type, non-integer,
30    /// overflow, out-of-memory, …).
31    Store(StoreError),
32    /// Operating-system / transport failure (file, socket, AOF).
33    #[cfg(feature = "std")]
34    Io(io::Error),
35    /// RESP-level failure on a client link: a server error reply
36    /// (`-ERR …` text preserved verbatim) or a malformed / unexpected
37    /// reply shape.
38    Protocol(String),
39    /// Write rejected: the target is a read-only replica.
40    ReadOnly,
41    /// Invalid argument to a typed API (bad flag combination, empty
42    /// prefix, malformed URL, …). Rejected before touching any state.
43    InvalidInput(String),
44    /// A named object (index, view, required key) doesn't exist.
45    NotFound(String),
46    /// The operation isn't available on this backend or build.
47    Unsupported(String),
48    /// A bounded blocking call ran out its timeout.
49    TimedOut,
50    /// The connection / in-process bus is gone (EOF).
51    Closed,
52}
53
54impl fmt::Display for KevyError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::Store(e) => write!(f, "store error: {e:?}"),
58            #[cfg(feature = "std")]
59            Self::Io(e) => write!(f, "io error: {e}"),
60            Self::Protocol(msg) => write!(f, "protocol error: {msg}"),
61            Self::ReadOnly => {
62                write!(f, "READONLY You can't write against a read only replica")
63            }
64            Self::InvalidInput(msg) => write!(f, "invalid input: {msg}"),
65            Self::NotFound(what) => write!(f, "not found: {what}"),
66            Self::Unsupported(msg) => write!(f, "unsupported: {msg}"),
67            Self::TimedOut => write!(f, "timed out"),
68            Self::Closed => write!(f, "connection closed"),
69        }
70    }
71}
72
73impl core::error::Error for KevyError {
74    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
75        match self {
76            #[cfg(feature = "std")]
77            Self::Io(e) => Some(e),
78            _ => None,
79        }
80    }
81}
82
83impl From<StoreError> for KevyError {
84    fn from(e: StoreError) -> Self {
85        Self::Store(e)
86    }
87}
88
89#[cfg(feature = "std")]
90impl From<io::Error> for KevyError {
91    fn from(e: io::Error) -> Self {
92        Self::Io(e)
93    }
94}
95
96/// The interop a consumer inside an `io::Result` world needs (
97/// dogfood F2): kevy provides the conversion — the orphan rule means
98/// nobody else can — so `?` works without 280 hand-rolled
99/// `io::Error::other` wrappers. Kind-mapped, and **source-preserving**:
100/// except for the `Io` passthrough, the original [`KevyError`] rides as
101/// the error's source, so a caller that later cares can downcast it
102/// back out instead of losing the type at the boundary.
103#[cfg(feature = "std")]
104impl From<KevyError> for io::Error {
105    fn from(e: KevyError) -> Self {
106        use io::ErrorKind as K;
107        match e {
108            KevyError::Io(inner) => inner,
109            other => {
110                let kind = match &other {
111                    KevyError::Io(_) => unreachable!("moved out above"),
112                    KevyError::TimedOut => K::TimedOut,
113                    KevyError::Closed => K::ConnectionAborted,
114                    KevyError::InvalidInput(_) => K::InvalidInput,
115                    KevyError::NotFound(_) => K::NotFound,
116                    KevyError::Unsupported(_) => K::Unsupported,
117                    KevyError::ReadOnly => K::PermissionDenied,
118                    KevyError::Protocol(_) => K::InvalidData,
119                    KevyError::Store(StoreError::OutOfMemory) => K::OutOfMemory,
120                    KevyError::Store(_) => K::InvalidData,
121                };
122                io::Error::new(kind, other)
123            }
124        }
125    }
126}
127
128#[cfg(all(test, feature = "std"))]
129mod io_interop_tests {
130    use super::*;
131
132    /// The kind survives the boundary and the typed
133    /// error rides as source — strictly better than the
134    /// `io::Error::other` wrapping it replaces.
135    #[test]
136    fn kinds_map_and_the_source_is_the_typed_error() {
137        let cases: [(KevyError, io::ErrorKind); 6] = [
138            (KevyError::TimedOut, io::ErrorKind::TimedOut),
139            (KevyError::Closed, io::ErrorKind::ConnectionAborted),
140            (KevyError::InvalidInput("x".into()), io::ErrorKind::InvalidInput),
141            (KevyError::NotFound("idx".into()), io::ErrorKind::NotFound),
142            (KevyError::Store(StoreError::WrongType), io::ErrorKind::InvalidData),
143            (KevyError::Store(StoreError::OutOfMemory), io::ErrorKind::OutOfMemory),
144        ];
145        for (kevy, kind) in cases {
146            let msg = kevy.to_string();
147            let io: io::Error = kevy.into();
148            assert_eq!(io.kind(), kind, "{msg}");
149            let src = io.get_ref().expect("source preserved");
150            assert!(src.is::<KevyError>(), "downcastable back to KevyError");
151            assert_eq!(src.to_string(), msg, "message survives");
152        }
153    }
154
155    /// An `Io` variant passes through untouched — no double wrap.
156    #[test]
157    fn io_passes_through_without_wrapping() {
158        let inner = io::Error::new(io::ErrorKind::BrokenPipe, "pipe");
159        let io: io::Error = KevyError::Io(inner).into();
160        assert_eq!(io.kind(), io::ErrorKind::BrokenPipe);
161        assert!(io.get_ref().is_some_and(|s| !s.is::<KevyError>()), "not re-wrapped");
162    }
163}