Skip to main content

copybook_utils/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Panic-safe utility functions and extension traits
4//!
5//! This crate provides utilities to eliminate panic conditions in the copybook-rs
6//! codebase, replacing `unwrap()` calls with structured error handling.
7
8use copybook_error::{Error, ErrorCode};
9
10/// Result type alias using copybook-error's Error
11pub type Result<T> = std::result::Result<T, Error>;
12
13/// Extension trait for `Option<T>` providing panic-safe unwrapping with context
14pub trait OptionExt<T> {
15    /// Unwrap an option safely, returning a structured error with context if None
16    ///
17    /// # Errors
18    /// Returns the specified error code and message if the option is `None`.
19    fn ok_or_cbkp_error(self, code: ErrorCode, message: impl Into<String>) -> Result<T>;
20
21    /// Unwrap an option safely with a specific error context
22    ///
23    /// # Errors
24    /// Returns the provided error if the option is `None`.
25    fn ok_or_error(self, error: Error) -> Result<T>;
26}
27
28impl<T> OptionExt<T> for Option<T> {
29    // PERFORMANCE OPTIMIZATION: Aggressive inlining for hot path error handling
30    #[allow(clippy::inline_always)]
31    #[inline]
32    fn ok_or_cbkp_error(self, code: ErrorCode, message: impl Into<String>) -> Result<T> {
33        match self {
34            Some(value) => Ok(value),
35            None => {
36                // Mark error construction as cold path
37                Err(Error::new(code, message.into()))
38            }
39        }
40    }
41
42    #[allow(clippy::inline_always)]
43    #[inline(always)]
44    fn ok_or_error(self, error: Error) -> Result<T> {
45        match self {
46            Some(value) => Ok(value),
47            None => Err(error),
48        }
49    }
50}
51
52/// Extension trait for `Vec<T>` providing panic-safe access operations
53pub trait VecExt<T> {
54    /// Pop from vector safely, returning a structured error if empty
55    ///
56    /// # Errors
57    /// Returns the specified error code and message if the vector is empty.
58    fn pop_or_cbkp_error(&mut self, code: ErrorCode, message: impl Into<String>) -> Result<T>;
59
60    /// Get last element safely, returning a structured error if empty
61    ///
62    /// # Errors
63    /// Returns the specified error code and message if the vector is empty.
64    fn last_or_cbkp_error(&self, code: ErrorCode, message: impl Into<String>) -> Result<&T>;
65
66    /// Get last mutable element safely, returning a structured error if empty
67    ///
68    /// # Errors
69    /// Returns the specified error code and message if the vector is empty.
70    fn last_mut_or_cbkp_error(
71        &mut self,
72        code: ErrorCode,
73        message: impl Into<String>,
74    ) -> Result<&mut T>;
75}
76
77impl<T> VecExt<T> for Vec<T> {
78    // PERFORMANCE OPTIMIZATION: Aggressive inlining for hot path vector operations
79    #[allow(clippy::inline_always)]
80    #[inline(always)]
81    fn pop_or_cbkp_error(&mut self, code: ErrorCode, message: impl Into<String>) -> Result<T> {
82        // Fast path: check length and pop in one operation
83        match self.pop() {
84            Some(value) => Ok(value),
85            None => Err(Error::new(code, message.into())),
86        }
87    }
88
89    #[allow(clippy::inline_always)]
90    #[inline(always)]
91    fn last_or_cbkp_error(&self, code: ErrorCode, message: impl Into<String>) -> Result<&T> {
92        // Fast path: direct slice access when non-empty
93        #[allow(clippy::if_not_else)]
94        if !self.is_empty() {
95            // SAFETY: We just checked that the vector is not empty
96            Ok(&self[self.len() - 1])
97        } else {
98            Err(Error::new(code, message.into()))
99        }
100    }
101
102    #[allow(clippy::inline_always)]
103    #[inline(always)]
104    fn last_mut_or_cbkp_error(
105        &mut self,
106        code: ErrorCode,
107        message: impl Into<String>,
108    ) -> Result<&mut T> {
109        // Fast path: direct slice access when non-empty
110        #[allow(clippy::if_not_else)]
111        if !self.is_empty() {
112            let len = self.len();
113            // SAFETY: We just checked that the vector is not empty
114            Ok(&mut self[len - 1])
115        } else {
116            Err(Error::new(code, message.into()))
117        }
118    }
119}
120
121/// Extension trait for slice indexing providing panic-safe access
122pub trait SliceExt<T> {
123    /// Get element at index safely, returning a structured error if out of bounds
124    ///
125    /// # Errors
126    /// Returns the specified error code and message if the index is out of bounds.
127    fn get_or_cbkp_error(
128        &self,
129        index: usize,
130        code: ErrorCode,
131        message: impl Into<String>,
132    ) -> Result<&T>;
133
134    /// Get mutable element at index safely, returning a structured error if out of bounds
135    ///
136    /// # Errors
137    /// Returns the specified error code and message if the index is out of bounds.
138    fn get_mut_or_cbkp_error(
139        &mut self,
140        index: usize,
141        code: ErrorCode,
142        message: impl Into<String>,
143    ) -> Result<&mut T>;
144}
145
146impl<T> SliceExt<T> for [T] {
147    // PERFORMANCE OPTIMIZATION: Aggressive inlining for hot path slice access
148    #[allow(clippy::inline_always)]
149    #[inline(always)]
150    fn get_or_cbkp_error(
151        &self,
152        index: usize,
153        code: ErrorCode,
154        message: impl Into<String>,
155    ) -> Result<&T> {
156        // Fast path: bounds check with likely hint
157        if index < self.len() {
158            // SAFETY: We just checked the bounds above
159            Ok(&self[index])
160        } else {
161            Err(Error::new(code, message.into()))
162        }
163    }
164
165    #[allow(clippy::inline_always)]
166    #[inline(always)]
167    fn get_mut_or_cbkp_error(
168        &mut self,
169        index: usize,
170        code: ErrorCode,
171        message: impl Into<String>,
172    ) -> Result<&mut T> {
173        // Fast path: bounds check with likely hint
174        if index < self.len() {
175            // SAFETY: We just checked the bounds above
176            Ok(&mut self[index])
177        } else {
178            Err(Error::new(code, message.into()))
179        }
180    }
181}
182
183/// Utility functions for panic-safe operations
184pub mod safe_ops {
185    pub use copybook_safe_ops::*;
186}
187
188#[cfg(test)]
189#[allow(clippy::expect_used)]
190#[allow(clippy::unwrap_used)]
191mod tests {
192    use super::{OptionExt, VecExt, safe_ops};
193    use copybook_error::{ErrorCode, Result};
194
195    #[test]
196    fn test_option_ext_some() -> Result<()> {
197        let opt = Some(42);
198        let value = opt.ok_or_cbkp_error(ErrorCode::CBKP001_SYNTAX, "test")?;
199        assert_eq!(value, 42);
200        Ok(())
201    }
202
203    #[test]
204    fn test_option_ext_none() {
205        let opt: Option<i32> = None;
206        let result = opt.ok_or_cbkp_error(ErrorCode::CBKP001_SYNTAX, "test error");
207        assert!(matches!(
208            result,
209            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
210        ));
211    }
212
213    #[test]
214    fn test_vec_ext_pop() -> Result<()> {
215        let mut vec = vec![1, 2, 3];
216        let value = vec.pop_or_cbkp_error(ErrorCode::CBKP001_SYNTAX, "test")?;
217        assert_eq!(value, 3);
218        Ok(())
219    }
220
221    #[test]
222    fn test_vec_ext_pop_empty() {
223        let mut empty_vec: Vec<i32> = vec![];
224        let result = empty_vec.pop_or_cbkp_error(ErrorCode::CBKP001_SYNTAX, "test error");
225        assert!(matches!(result, Err(error) if error.code == ErrorCode::CBKP001_SYNTAX));
226    }
227
228    #[test]
229    fn test_safe_parse() -> Result<()> {
230        let parsed = safe_ops::parse_usize("123", "test")?;
231        assert_eq!(parsed, 123);
232
233        let result = safe_ops::parse_usize("invalid", "test");
234        assert!(matches!(
235            result,
236            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
237        ));
238
239        Ok(())
240    }
241
242    #[test]
243    fn test_safe_divide() -> Result<()> {
244        let quotient = safe_ops::safe_divide(10, 2, "test")?;
245        assert_eq!(quotient, 5);
246
247        let result = safe_ops::safe_divide(10, 0, "test");
248        assert!(matches!(
249            result,
250            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
251        ));
252
253        Ok(())
254    }
255}