Skip to main content

logdive_core/
error.rs

1//! Unified error type for `logdive-core`.
2//!
3//! All fallible APIs in this crate return [`Result<T>`], aliased to
4//! `std::result::Result<T, LogdiveError>`. Module-local error types
5//! from earlier milestones (e.g. [`QueryParseError`] from the `query`
6//! module) are preserved as public types and convert into `LogdiveError`
7//! via `From` impls, so `?` works seamlessly at API boundaries while
8//! callers that need structured access (the CLI rendering parse-error
9//! carets, for example) can still match against the richer inner type.
10
11use std::io;
12use std::path::PathBuf;
13
14use thiserror::Error;
15
16use crate::query::QueryParseError;
17
18/// Convenient crate-wide result alias.
19pub type Result<T> = std::result::Result<T, LogdiveError>;
20
21/// Every error the core crate can produce.
22#[derive(Debug, Error)]
23#[non_exhaustive]
24pub enum LogdiveError {
25    /// Wrapper around a [`QueryParseError`], preserved as-is so callers
26    /// that want the structured position/message can match on it.
27    #[error(transparent)]
28    QueryParse(#[from] QueryParseError),
29
30    /// The `since <datetime>` clause contained a string that did not
31    /// parse as any accepted datetime format.
32    #[error("invalid datetime {input:?}: {reason}")]
33    InvalidDatetime { input: String, reason: String },
34
35    /// A field name slipped through the parser's validation. This is a
36    /// defense-in-depth guard at the SQL-generation boundary and should
37    /// be unreachable in practice.
38    #[error("unsafe field name {0:?}")]
39    UnsafeFieldName(String),
40
41    /// A row came back from SQLite with a malformed `fields` JSON column.
42    /// Indicates corruption or an out-of-band write to the database.
43    #[error("corrupt fields JSON in row: {0}")]
44    CorruptFieldsJson(#[source] serde_json::Error),
45
46    /// Underlying SQLite error.
47    #[error("sqlite error: {0}")]
48    Sqlite(#[from] rusqlite::Error),
49
50    /// I/O error while creating the index directory, opening the database
51    /// file, or reading a log file for ingestion.
52    #[error("io error at {path}: {source}")]
53    Io {
54        path: PathBuf,
55        #[source]
56        source: io::Error,
57    },
58
59    /// Generic I/O error without an associated path (for stream-style
60    /// ingestion from stdin, where there isn't a meaningful path).
61    #[error("io error: {0}")]
62    IoBare(#[from] io::Error),
63
64    /// Miscellaneous serde error not covered by a more specific variant.
65    #[error("json error: {0}")]
66    Json(#[from] serde_json::Error),
67}
68
69impl LogdiveError {
70    /// Construct an [`LogdiveError::Io`] with the offending path attached.
71    /// Preferred over the bare `From<io::Error>` conversion when a path
72    /// is known — the error message is markedly more useful.
73    pub fn io_at(path: impl Into<PathBuf>, source: io::Error) -> Self {
74        Self::Io {
75            path: path.into(),
76            source,
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn query_parse_error_converts_via_question_mark() {
87        // Structural test: `QueryParseError` must be `?`-convertible into
88        // LogdiveError so consumer modules can `use crate::Result;` and
89        // propagate parse errors without an explicit `.map_err`.
90        fn try_parse() -> Result<()> {
91            let ast = crate::query::parse("level=")?; // malformed
92            let _ = ast;
93            Ok(())
94        }
95        let err = try_parse().unwrap_err();
96        assert!(matches!(err, LogdiveError::QueryParse(_)));
97    }
98
99    #[test]
100    fn sqlite_error_converts_via_question_mark() {
101        fn do_thing() -> Result<()> {
102            let conn = rusqlite::Connection::open_in_memory()?;
103            conn.execute("this is not valid SQL", [])?;
104            Ok(())
105        }
106        let err = do_thing().unwrap_err();
107        assert!(matches!(err, LogdiveError::Sqlite(_)));
108    }
109
110    #[test]
111    fn json_error_converts_via_question_mark() {
112        fn do_thing() -> Result<serde_json::Value> {
113            let v = serde_json::from_str("not json")?;
114            Ok(v)
115        }
116        let err = do_thing().unwrap_err();
117        assert!(matches!(err, LogdiveError::Json(_)));
118    }
119
120    #[test]
121    fn io_at_attaches_path_to_error_message() {
122        let src = io::Error::new(io::ErrorKind::NotFound, "missing");
123        let err = LogdiveError::io_at("/tmp/never-exists.db", src);
124        let msg = format!("{err}");
125        assert!(msg.contains("/tmp/never-exists.db"));
126        assert!(msg.contains("missing"));
127    }
128
129    #[test]
130    fn invalid_datetime_formats_both_input_and_reason() {
131        let err = LogdiveError::InvalidDatetime {
132            input: "not-a-date".to_string(),
133            reason: "expected RFC3339".to_string(),
134        };
135        let msg = format!("{err}");
136        assert!(msg.contains("not-a-date"));
137        assert!(msg.contains("RFC3339"));
138    }
139}