quack_rs/error.rs
1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! Error types for `DuckDB` extension FFI error propagation.
7//!
8//! [`ExtensionError`] is the primary error type. It implements [`std::error::Error`],
9//! can be constructed from `&str` or `String`, and converts to a `CString` for
10//! passing to `access.set_error`.
11//!
12//! # Example
13//!
14//! ```rust
15//! use quack_rs::error::ExtensionError;
16//!
17//! let err = ExtensionError::from("Failed to register function");
18//! assert_eq!(err.to_string(), "Failed to register function");
19//! ```
20
21use std::ffi::CString;
22use std::fmt;
23
24/// An error that can occur during `DuckDB` extension initialization or registration.
25///
26/// This type is designed for use with the `?` operator inside the extension
27/// entry point. It can be reported back to `DuckDB` via `access.set_error`.
28///
29/// # Construction
30///
31/// ```rust
32/// use quack_rs::error::ExtensionError;
33///
34/// // From a string literal
35/// let e = ExtensionError::from("something went wrong");
36///
37/// // From a String
38/// let msg = format!("failed: {}", 42);
39/// let e = ExtensionError::from(msg);
40///
41/// // Wrapping another error
42/// let parse_err: Result<i32, _> = "not a number".parse();
43/// let e = parse_err.map_err(ExtensionError::from_error);
44/// ```
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct ExtensionError {
47 message: String,
48}
49
50impl ExtensionError {
51 /// Creates a new `ExtensionError` with the given message.
52 ///
53 /// # Example
54 ///
55 /// ```rust
56 /// use quack_rs::error::ExtensionError;
57 ///
58 /// let err = ExtensionError::new("registration failed");
59 /// assert_eq!(err.to_string(), "registration failed");
60 /// ```
61 #[inline]
62 pub fn new(message: impl Into<String>) -> Self {
63 Self {
64 message: message.into(),
65 }
66 }
67
68 /// Wraps any `std::error::Error` into an `ExtensionError`.
69 ///
70 /// # Example
71 ///
72 /// ```rust
73 /// use quack_rs::error::ExtensionError;
74 ///
75 /// let result: Result<i32, _> = "abc".parse::<i32>();
76 /// let err = result.map_err(ExtensionError::from_error);
77 /// assert!(err.is_err());
78 /// ```
79 #[inline]
80 pub fn from_error<E: std::error::Error>(e: E) -> Self {
81 Self {
82 message: e.to_string(),
83 }
84 }
85
86 /// Converts this error into a `CString` suitable for passing to `set_error`.
87 ///
88 /// If the message contains a null byte (which is valid in a Rust `String` but
89 /// not in a C string), the message is truncated at the first null byte.
90 ///
91 /// # Example
92 ///
93 /// ```rust
94 /// use quack_rs::error::ExtensionError;
95 ///
96 /// let err = ExtensionError::new("oops");
97 /// let cstr = err.to_c_string();
98 /// assert_eq!(cstr.to_str().unwrap(), "oops");
99 /// ```
100 #[must_use]
101 pub fn to_c_string(&self) -> CString {
102 CString::new(self.message.as_bytes()).unwrap_or_else(|_| {
103 // Truncate at the first null byte to produce a valid C string.
104 // No panic: if CString::new fails again (logically impossible since
105 // we truncate at the first null byte), fall back to a generic message.
106 let pos = self
107 .message
108 .bytes()
109 .position(|b| b == 0)
110 .unwrap_or(self.message.len());
111 CString::new(&self.message.as_bytes()[..pos]).unwrap_or_else(|_| {
112 // Defensive fallback — should never be reached.
113 CString::new("extension error (message contained null bytes)")
114 .unwrap_or_else(|_| CString::default())
115 })
116 })
117 }
118
119 /// Returns the error message as a string slice.
120 ///
121 /// # Example
122 ///
123 /// ```rust
124 /// use quack_rs::error::ExtensionError;
125 ///
126 /// let err = ExtensionError::new("bad input");
127 /// assert_eq!(err.as_str(), "bad input");
128 /// ```
129 #[must_use]
130 #[inline]
131 pub fn as_str(&self) -> &str {
132 &self.message
133 }
134}
135
136impl fmt::Display for ExtensionError {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 f.write_str(&self.message)
139 }
140}
141
142impl std::error::Error for ExtensionError {}
143
144impl From<&str> for ExtensionError {
145 #[inline]
146 fn from(s: &str) -> Self {
147 Self::new(s)
148 }
149}
150
151impl From<String> for ExtensionError {
152 #[inline]
153 fn from(s: String) -> Self {
154 Self { message: s }
155 }
156}
157
158impl From<Box<dyn std::error::Error>> for ExtensionError {
159 #[inline]
160 fn from(e: Box<dyn std::error::Error>) -> Self {
161 Self {
162 message: e.to_string(),
163 }
164 }
165}
166
167impl From<Box<dyn std::error::Error + Send + Sync>> for ExtensionError {
168 #[inline]
169 fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
170 Self {
171 message: e.to_string(),
172 }
173 }
174}
175
176/// Convenience type alias for `Result<T, ExtensionError>`.
177pub type ExtResult<T> = Result<T, ExtensionError>;
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn new_creates_with_message() {
185 let err = ExtensionError::new("test error");
186 assert_eq!(err.to_string(), "test error");
187 assert_eq!(err.as_str(), "test error");
188 }
189
190 #[test]
191 fn from_str() {
192 let err = ExtensionError::from("from str");
193 assert_eq!(err.message, "from str");
194 }
195
196 #[test]
197 fn from_string() {
198 let s = String::from("from String");
199 let err = ExtensionError::from(s);
200 assert_eq!(err.message, "from String");
201 }
202
203 #[test]
204 fn from_error_wraps_display() {
205 let parse_err = "abc".parse::<i32>().unwrap_err();
206 let err = ExtensionError::from_error(parse_err);
207 assert!(!err.message.is_empty());
208 }
209
210 #[test]
211 fn to_c_string_normal() {
212 let err = ExtensionError::new("hello world");
213 let cstr = err.to_c_string();
214 assert_eq!(cstr.to_str().unwrap(), "hello world");
215 }
216
217 #[test]
218 fn to_c_string_with_null_byte() {
219 // A message with an embedded null byte should be truncated at the null
220 let err = ExtensionError::new("before\0after");
221 let cstr = err.to_c_string();
222 assert_eq!(cstr.to_str().unwrap(), "before");
223 }
224
225 #[test]
226 fn to_c_string_empty() {
227 let err = ExtensionError::new("");
228 let cstr = err.to_c_string();
229 assert_eq!(cstr.to_str().unwrap(), "");
230 }
231
232 #[test]
233 fn display_impl() {
234 let err = ExtensionError::new("display test");
235 let s = format!("{err}");
236 assert_eq!(s, "display test");
237 }
238
239 #[test]
240 fn debug_impl() {
241 let err = ExtensionError::new("debug");
242 let s = format!("{err:?}");
243 assert!(s.contains("debug"));
244 }
245
246 #[test]
247 fn clone_eq() {
248 let err1 = ExtensionError::new("clone test");
249 let err2 = err1.clone();
250 assert_eq!(err1, err2);
251 }
252
253 #[test]
254 fn from_box_dyn_error() {
255 let boxed: Box<dyn std::error::Error> = "abc".parse::<i32>().unwrap_err().into();
256 let err = ExtensionError::from(boxed);
257 assert!(!err.message.is_empty());
258 }
259
260 #[test]
261 fn question_mark_operator_with_str() {
262 fn fails() -> Result<(), ExtensionError> {
263 Err("explicit error")?;
264 Ok(())
265 }
266 assert_eq!(fails().unwrap_err().as_str(), "explicit error");
267 }
268}