1use std::{fmt, io};
2
3pub type Result<T> = std::result::Result<T, LuciError>;
5
6#[derive(Debug)]
11pub enum LuciError {
12 Io(io::Error),
14
15 IndexNotFound(String),
17
18 IndexCorrupted(String),
21
22 WriterLocked,
25
26 SchemaConflict {
28 field: String,
29 expected: String,
30 actual: String,
31 },
32
33 UnsupportedQuery(String),
35
36 InvalidQuery(String),
38
39 InvalidValue(String),
44
45 TransactionActive,
48
49 QueryBehaviorDifference { feature: String, difference: String },
52
53 SegmentFormatMigrationRequired(String),
57
58 SegmentFormatUnknown(String),
61}
62
63impl fmt::Display for LuciError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Io(e) => write!(f, "I/O error: {e}"),
67 Self::IndexNotFound(path) => write!(f, "index not found: {path}"),
68 Self::IndexCorrupted(msg) => write!(f, "index corrupted: {msg}"),
69 Self::WriterLocked => write!(f, "another writer holds the lock"),
70 Self::SchemaConflict {
71 field,
72 expected,
73 actual,
74 } => write!(
75 f,
76 "schema conflict on field '{field}': expected {expected}, got {actual}"
77 ),
78 Self::UnsupportedQuery(q) => write!(f, "unsupported query type: {q}"),
79 Self::InvalidQuery(msg) => write!(f, "invalid query: {msg}"),
80 Self::InvalidValue(msg) => write!(f, "invalid value: {msg}"),
81 Self::TransactionActive => write!(
82 f,
83 "cannot use index.add() while a transaction is active on this thread — use txn.add() instead"
84 ),
85 Self::QueryBehaviorDifference {
86 feature,
87 difference,
88 } => write!(f, "{feature}: {difference}"),
89 Self::SegmentFormatMigrationRequired(msg) => {
90 write!(f, "segment format requires migration: {msg}")
91 }
92 Self::SegmentFormatUnknown(msg) => {
93 write!(f, "unknown segment format: {msg}")
94 }
95 }
96 }
97}
98
99impl std::error::Error for LuciError {
100 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
101 match self {
102 Self::Io(e) => Some(e),
103 _ => None,
104 }
105 }
106}
107
108impl From<io::Error> for LuciError {
109 fn from(e: io::Error) -> Self {
110 Self::Io(e)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use std::error::Error;
118
119 #[test]
120 fn io_error_conversion() {
121 let io_err = io::Error::new(io::ErrorKind::NotFound, "file gone");
122 let luci_err = LuciError::from(io_err);
123 assert!(matches!(luci_err, LuciError::Io(_)));
124 assert!(luci_err.source().is_some());
125 }
126
127 #[test]
128 fn display_index_not_found() {
129 let err = LuciError::IndexNotFound("/tmp/test.luci".into());
130 assert!(format!("{err}").contains("/tmp/test.luci"));
131 }
132
133 #[test]
134 fn display_index_corrupted() {
135 let err = LuciError::IndexCorrupted("checksum mismatch".into());
136 assert!(format!("{err}").contains("checksum mismatch"));
137 }
138
139 #[test]
140 fn display_writer_locked() {
141 let err = LuciError::WriterLocked;
142 assert!(format!("{err}").contains("lock"));
143 }
144
145 #[test]
146 fn display_schema_conflict() {
147 let err = LuciError::SchemaConflict {
148 field: "price".into(),
149 expected: "float".into(),
150 actual: "text".into(),
151 };
152 let msg = format!("{err}");
153 assert!(msg.contains("price"));
154 assert!(msg.contains("float"));
155 assert!(msg.contains("text"));
156 }
157
158 #[test]
159 fn display_unsupported_query() {
160 let err = LuciError::UnsupportedQuery("span_near".into());
161 assert!(format!("{err}").contains("span_near"));
162 }
163
164 #[test]
165 fn display_invalid_query() {
166 let err = LuciError::InvalidQuery("unexpected token".into());
167 assert!(format!("{err}").contains("unexpected token"));
168 }
169
170 #[test]
171 fn display_invalid_value() {
172 let err = LuciError::InvalidValue("keyword value exceeds 65535 bytes".into());
173 let msg = format!("{err}");
174 assert!(msg.contains("invalid value"));
175 assert!(msg.contains("65535"));
176 }
177
178 #[test]
179 fn non_io_errors_have_no_source() {
180 let err = LuciError::WriterLocked;
181 assert!(err.source().is_none());
182 }
183}