Skip to main content

drasi_lib/
error.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Error types for drasi-lib operations.
16//!
17//! This module provides structured error types using `thiserror` for idiomatic Rust error handling.
18//! The pattern follows major Rust libraries like `tokio`, `reqwest`, and `sqlx`:
19//! - Public API returns `crate::error::Result<T>` with structured `DrasiError` variants
20//! - Internal code uses `anyhow::Result<T>` for flexibility
21//! - Error chains are preserved via `#[error(transparent)]` for debugging
22//!
23//! # Example
24//!
25//! ```ignore
26//! use drasi_lib::error::{DrasiError, Result};
27//!
28//! fn example() -> Result<()> {
29//!     // Pattern match on specific error variants
30//!     match some_operation() {
31//!         Err(DrasiError::ComponentNotFound { component_type, component_id }) => {
32//!             println!("{} '{}' not found", component_type, component_id);
33//!         }
34//!         Err(DrasiError::InvalidState { message }) => {
35//!             println!("Invalid state: {}", message);
36//!         }
37//!         Err(e) => return Err(e),
38//!         Ok(v) => { /* ... */ }
39//!     }
40//!     Ok(())
41//! }
42//! ```
43
44use thiserror::Error;
45
46/// Main error type for drasi-lib operations.
47///
48/// This enum provides structured error variants that enable type-safe pattern matching
49/// by callers. Each variant contains contextual information about the error.
50#[derive(Error, Debug)]
51pub enum DrasiError {
52    /// Component (source, query, or reaction) was not found.
53    #[error("{component_type} '{component_id}' not found")]
54    ComponentNotFound {
55        /// The type of component (e.g., "source", "query", "reaction")
56        component_type: String,
57        /// The ID of the component that was not found
58        component_id: String,
59    },
60
61    /// Component already exists with the given ID.
62    #[error("{component_type} '{component_id}' already exists")]
63    AlreadyExists {
64        /// The type of component
65        component_type: String,
66        /// The ID that already exists
67        component_id: String,
68    },
69
70    /// Invalid configuration provided.
71    #[error("Invalid configuration: {message}")]
72    InvalidConfig {
73        /// Description of the configuration error
74        message: String,
75    },
76
77    /// Operation is not valid in the current state.
78    #[error("Invalid state: {message}")]
79    InvalidState {
80        /// Description of the state error
81        message: String,
82    },
83
84    /// Validation failed (e.g., builder validation, input validation).
85    #[error("Validation failed: {message}")]
86    Validation {
87        /// Description of the validation error
88        message: String,
89    },
90
91    /// A component operation (start, stop, delete, etc.) failed.
92    #[error("Failed to {operation} {component_type} '{component_id}': {reason}")]
93    OperationFailed {
94        /// The type of component
95        component_type: String,
96        /// The ID of the component
97        component_id: String,
98        /// The operation that failed (e.g., "start", "stop", "delete")
99        operation: String,
100        /// The reason for the failure
101        reason: String,
102    },
103
104    /// Internal error - wraps underlying errors while preserving the error chain.
105    /// Use `.source()` to access the underlying error chain.
106    #[error(transparent)]
107    Internal(#[from] anyhow::Error),
108}
109
110// ============================================================================
111// Constructor helpers for common error patterns
112// ============================================================================
113
114impl DrasiError {
115    /// Create a component not found error.
116    ///
117    /// # Example
118    /// ```ignore
119    /// DrasiError::component_not_found("source", "my-source-id")
120    /// ```
121    pub fn component_not_found(
122        component_type: impl Into<String>,
123        component_id: impl Into<String>,
124    ) -> Self {
125        DrasiError::ComponentNotFound {
126            component_type: component_type.into(),
127            component_id: component_id.into(),
128        }
129    }
130
131    /// Create an already exists error.
132    ///
133    /// # Example
134    /// ```ignore
135    /// DrasiError::already_exists("query", "my-query-id")
136    /// ```
137    pub fn already_exists(
138        component_type: impl Into<String>,
139        component_id: impl Into<String>,
140    ) -> Self {
141        DrasiError::AlreadyExists {
142            component_type: component_type.into(),
143            component_id: component_id.into(),
144        }
145    }
146
147    /// Create an invalid configuration error.
148    ///
149    /// # Example
150    /// ```ignore
151    /// DrasiError::invalid_config("Missing required field 'query'")
152    /// ```
153    pub fn invalid_config(message: impl Into<String>) -> Self {
154        DrasiError::InvalidConfig {
155            message: message.into(),
156        }
157    }
158
159    /// Create an invalid state error.
160    ///
161    /// # Example
162    /// ```ignore
163    /// DrasiError::invalid_state("Server must be initialized before starting")
164    /// ```
165    pub fn invalid_state(message: impl Into<String>) -> Self {
166        DrasiError::InvalidState {
167            message: message.into(),
168        }
169    }
170
171    /// Create a validation error.
172    ///
173    /// # Example
174    /// ```ignore
175    /// DrasiError::validation("Query string cannot be empty")
176    /// ```
177    pub fn validation(message: impl Into<String>) -> Self {
178        DrasiError::Validation {
179            message: message.into(),
180        }
181    }
182
183    /// Create an operation failed error.
184    ///
185    /// # Example
186    /// ```ignore
187    /// DrasiError::operation_failed("source", "my-source", "start", "Connection refused")
188    /// ```
189    pub fn operation_failed(
190        component_type: impl Into<String>,
191        component_id: impl Into<String>,
192        operation: impl Into<String>,
193        reason: impl Into<String>,
194    ) -> Self {
195        DrasiError::OperationFailed {
196            component_type: component_type.into(),
197            component_id: component_id.into(),
198            operation: operation.into(),
199            reason: reason.into(),
200        }
201    }
202
203    // ========================================================================
204    // Backward compatibility helpers (deprecated, use structured variants)
205    // ========================================================================
206
207    /// Create a provisioning error.
208    ///
209    /// # Deprecated
210    /// Consider using `operation_failed` with specific component context instead.
211    pub fn provisioning(msg: impl Into<String>) -> Self {
212        DrasiError::InvalidConfig {
213            message: format!("Provisioning error: {}", msg.into()),
214        }
215    }
216
217    /// Create a component error (generic operation failure).
218    ///
219    /// # Deprecated
220    /// Consider using `operation_failed` with specific component context instead.
221    pub fn component_error(msg: impl Into<String>) -> Self {
222        DrasiError::InvalidState {
223            message: format!("Component error: {}", msg.into()),
224        }
225    }
226
227    /// Create a startup validation error.
228    ///
229    /// # Deprecated
230    /// Consider using `validation` or `invalid_config` instead.
231    pub fn startup_validation(msg: impl Into<String>) -> Self {
232        DrasiError::Validation {
233            message: format!("Startup validation failed: {}", msg.into()),
234        }
235    }
236}
237
238/// Result type for drasi-lib operations.
239///
240/// This is the standard result type for all public API methods in drasi-lib.
241/// It uses `DrasiError` which supports pattern matching on specific error variants.
242pub type Result<T> = std::result::Result<T, DrasiError>;
243
244// ============================================================================
245// Tests
246// ============================================================================
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_component_not_found_display() {
254        let err = DrasiError::component_not_found("source", "my-source");
255        assert_eq!(err.to_string(), "source 'my-source' not found");
256    }
257
258    #[test]
259    fn test_already_exists_display() {
260        let err = DrasiError::already_exists("query", "my-query");
261        assert_eq!(err.to_string(), "query 'my-query' already exists");
262    }
263
264    #[test]
265    fn test_invalid_config_display() {
266        let err = DrasiError::invalid_config("Missing field");
267        assert_eq!(err.to_string(), "Invalid configuration: Missing field");
268    }
269
270    #[test]
271    fn test_invalid_state_display() {
272        let err = DrasiError::invalid_state("Not initialized");
273        assert_eq!(err.to_string(), "Invalid state: Not initialized");
274    }
275
276    #[test]
277    fn test_validation_display() {
278        let err = DrasiError::validation("Empty query string");
279        assert_eq!(err.to_string(), "Validation failed: Empty query string");
280    }
281
282    #[test]
283    fn test_operation_failed_display() {
284        let err =
285            DrasiError::operation_failed("source", "my-source", "start", "Connection refused");
286        assert_eq!(
287            err.to_string(),
288            "Failed to start source 'my-source': Connection refused"
289        );
290    }
291
292    #[test]
293    fn test_internal_error_from_anyhow() {
294        let anyhow_err = anyhow::anyhow!("Something went wrong");
295        let drasi_err: DrasiError = anyhow_err.into();
296        assert!(matches!(drasi_err, DrasiError::Internal(_)));
297        assert!(drasi_err.to_string().contains("Something went wrong"));
298    }
299
300    #[test]
301    fn test_error_pattern_matching() {
302        let err = DrasiError::component_not_found("source", "test-source");
303
304        match err {
305            DrasiError::ComponentNotFound {
306                component_type,
307                component_id,
308            } => {
309                assert_eq!(component_type, "source");
310                assert_eq!(component_id, "test-source");
311            }
312            _ => panic!("Expected ComponentNotFound variant"),
313        }
314    }
315
316    #[test]
317    fn test_internal_error_transparent() {
318        // Create an anyhow error with a source chain
319        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
320        let anyhow_err = anyhow::Error::new(io_error).context("Failed to read config");
321        let drasi_err: DrasiError = anyhow_err.into();
322
323        // The error should be Internal variant
324        assert!(matches!(drasi_err, DrasiError::Internal(_)));
325
326        // The display should show the full chain due to #[error(transparent)]
327        let display = drasi_err.to_string();
328        assert!(display.contains("Failed to read config"));
329
330        // source() returns the underlying anyhow error's source
331        // Note: anyhow wraps errors, so source behavior depends on the chain
332        if let DrasiError::Internal(ref anyhow_err) = drasi_err {
333            // We can access the anyhow error and its chain
334            assert!(anyhow_err.to_string().contains("Failed to read config"));
335        }
336    }
337
338    #[test]
339    fn test_backward_compat_provisioning() {
340        let err = DrasiError::provisioning("Failed to provision");
341        assert!(err.to_string().contains("Provisioning error"));
342    }
343
344    #[test]
345    fn test_backward_compat_component_error() {
346        let err = DrasiError::component_error("Component failed");
347        assert!(err.to_string().contains("Component error"));
348    }
349
350    #[test]
351    fn test_backward_compat_startup_validation() {
352        let err = DrasiError::startup_validation("Validation failed");
353        assert!(err.to_string().contains("Startup validation failed"));
354    }
355}