Skip to main content

unwrap_safe/
lib.rs

1// Copyright (c) Ted Kaplan. All Rights Reserved.
2// SPDX-License-Identifier: MIT
3
4//! Safe unwrap replacements that log instead of panic.
5//!
6//! `.unwrap()` and `.expect()` are useful during development but cause
7//! production outages when invariants break. This crate provides drop-in
8//! macro replacements that degrade gracefully: return a default value and
9//! emit a structured log instead of panicking.
10//!
11//! # Quick Reference
12//!
13//! | Before | After | For |
14//! |--------|-------|-----|
15//! | `opt.unwrap()` | `unwrap_or_warn!(opt, default, "ctx")` | `Option<T>` |
16//! | `res.unwrap()` | `result_or_warn!(res, default, "ctx")` | `Result<T, E>` |
17//! | `s.parse::<T>().unwrap()` | `parse_or_warn!(s, T, default, "ctx")` | String parsing |
18//! | `opt.expect("msg")` | `expect_or_error!(opt, default, "msg")` | Option soft-expect |
19//! | `res.expect("msg")` | `expect_result_or_error!(res, default, "msg")` | Result soft-expect |
20//!
21//! All macros include `file!()` and `line!()` in the log output.
22//!
23//! # Example
24//!
25//! ```rust
26//! use unwrap_safe::{unwrap_or_warn, result_or_warn, parse_or_warn};
27//!
28//! // Option: return default on None
29//! let name: Option<String> = None;
30//! let display = unwrap_or_warn!(name, String::from("anonymous"), "user display name");
31//! assert_eq!(display, "anonymous");
32//!
33//! // Result: return default on Err
34//! let val: Result<i32, String> = Err("timeout".into());
35//! let count = result_or_warn!(val, 0, "request count");
36//! assert_eq!(count, 0);
37//!
38//! // Parse: return default on bad input
39//! let port = parse_or_warn!("not_a_port", u16, 8080, "PORT env var");
40//! assert_eq!(port, 8080);
41//! ```
42
43/// Unwrap an `Option<T>`, returning a default and logging a warning if `None`.
44///
45/// Drop-in replacement for `.unwrap()` and `.expect()` on `Option` values
46/// where panicking is unacceptable in production.
47///
48/// For `Result` types, use [`result_or_warn!`] instead — it captures the
49/// error value in the log.
50///
51/// # Examples
52///
53/// ```rust
54/// use unwrap_safe::unwrap_or_warn;
55///
56/// let val: Option<i32> = None;
57/// let result = unwrap_or_warn!(val, 42, "loading user preference");
58/// assert_eq!(result, 42);
59/// ```
60#[macro_export]
61macro_rules! unwrap_or_warn {
62    ($opt:expr, $default:expr, $context:expr) => {
63        match $opt {
64            Some(v) => v,
65            None => {
66                ::tracing::warn!(
67                    context = $context,
68                    location = concat!(file!(), ":", line!()),
69                    "Unexpected None — using default (was unwrap)"
70                );
71                $default
72            }
73        }
74    };
75}
76
77/// Unwrap a `Result<T, E>`, returning a default and logging a warning on `Err`.
78///
79/// Captures the error value in the log, unlike [`unwrap_or_warn!`] which
80/// is for `Option` types.
81///
82/// # Examples
83///
84/// ```rust
85/// use unwrap_safe::result_or_warn;
86///
87/// let val: Result<i32, String> = Err("timeout".into());
88/// let result = result_or_warn!(val, -1, "db query");
89/// assert_eq!(result, -1);
90/// ```
91#[macro_export]
92macro_rules! result_or_warn {
93    ($result:expr, $default:expr, $context:expr) => {
94        match $result {
95            Ok(v) => v,
96            Err(e) => {
97                ::tracing::warn!(
98                    error = %e,
99                    context = $context,
100                    location = concat!(file!(), ":", line!()),
101                    "Unexpected error — using default (was unwrap)"
102                );
103                $default
104            }
105        }
106    };
107}
108
109/// Parse a string into a type, returning a default and logging on failure.
110///
111/// Drop-in replacement for `str.parse::<T>().unwrap()`.
112///
113/// # Examples
114///
115/// ```rust
116/// use unwrap_safe::parse_or_warn;
117///
118/// let port = parse_or_warn!("not_a_number", u16, 8080, "PORT config");
119/// assert_eq!(port, 8080);
120///
121/// let port = parse_or_warn!("3000", u16, 8080, "PORT config");
122/// assert_eq!(port, 3000);
123/// ```
124#[macro_export]
125macro_rules! parse_or_warn {
126    ($s:expr, $ty:ty, $default:expr, $context:expr) => {
127        match $s.parse::<$ty>() {
128            Ok(v) => v,
129            Err(e) => {
130                ::tracing::warn!(
131                    input = %$s,
132                    error = %e,
133                    context = $context,
134                    location = concat!(file!(), ":", line!()),
135                    "Parse failed — using default (was unwrap)"
136                );
137                $default
138            }
139        }
140    };
141}
142
143/// Soft expect for `Option`: logs an error and returns a default instead of panicking.
144///
145/// For cases where the code currently uses `.expect("message")` but a panic
146/// would be worse than a degraded response.
147///
148/// # Examples
149///
150/// ```rust
151/// use unwrap_safe::expect_or_error;
152///
153/// let val: Option<&str> = None;
154/// let result = expect_or_error!(val, "fallback", "config key must exist");
155/// assert_eq!(result, "fallback");
156/// ```
157#[macro_export]
158macro_rules! expect_or_error {
159    ($opt:expr, $default:expr, $msg:expr) => {
160        match $opt {
161            Some(v) => v,
162            None => {
163                ::tracing::error!(
164                    expectation = $msg,
165                    location = concat!(file!(), ":", line!()),
166                    "EXPECT FAILED — invariant violated, using fallback (was expect/unwrap)"
167                );
168                $default
169            }
170        }
171    };
172}
173
174/// Soft expect for `Result`: logs an error and returns a default instead of panicking.
175///
176/// # Examples
177///
178/// ```rust
179/// use unwrap_safe::expect_result_or_error;
180///
181/// let val: Result<i32, String> = Err("connection refused".into());
182/// let result = expect_result_or_error!(val, 0, "db connection must succeed");
183/// assert_eq!(result, 0);
184/// ```
185#[macro_export]
186macro_rules! expect_result_or_error {
187    ($result:expr, $default:expr, $msg:expr) => {
188        match $result {
189            Ok(v) => v,
190            Err(e) => {
191                ::tracing::error!(
192                    error = %e,
193                    expectation = $msg,
194                    location = concat!(file!(), ":", line!()),
195                    "EXPECT FAILED — invariant violated, using fallback (was expect/unwrap)"
196                );
197                $default
198            }
199        }
200    };
201}
202
203#[cfg(test)]
204mod tests {
205    #[test]
206    fn unwrap_or_warn_some() {
207        let val: Option<i32> = Some(42);
208        let result = unwrap_or_warn!(val, 0, "test");
209        assert_eq!(result, 42);
210    }
211
212    #[test]
213    fn unwrap_or_warn_none() {
214        let val: Option<i32> = None;
215        let result = unwrap_or_warn!(val, -1, "test fallback");
216        assert_eq!(result, -1);
217    }
218
219    #[test]
220    fn unwrap_or_warn_string_option() {
221        let val: Option<String> = None;
222        let result = unwrap_or_warn!(val, String::from("default"), "username lookup");
223        assert_eq!(result, "default");
224    }
225
226    #[test]
227    fn result_or_warn_ok() {
228        let val: Result<i32, String> = Ok(42);
229        let result = result_or_warn!(val, 0, "test");
230        assert_eq!(result, 42);
231    }
232
233    #[test]
234    fn result_or_warn_err() {
235        let val: Result<i32, String> = Err("broken".into());
236        let result = result_or_warn!(val, -1, "test fallback");
237        assert_eq!(result, -1);
238    }
239
240    #[test]
241    fn result_or_warn_io_error() {
242        let val: Result<String, std::io::Error> =
243            Err(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
244        let result = result_or_warn!(val, String::from("fallback"), "file read");
245        assert_eq!(result, "fallback");
246    }
247
248    #[test]
249    fn parse_or_warn_success() {
250        let port = parse_or_warn!("3000", u16, 8080, "port config");
251        assert_eq!(port, 3000);
252    }
253
254    #[test]
255    fn parse_or_warn_failure() {
256        let port = parse_or_warn!("not_a_number", u16, 8080, "port config");
257        assert_eq!(port, 8080);
258    }
259
260    #[test]
261    fn parse_or_warn_float() {
262        let val = parse_or_warn!("3.14", f64, 0.0, "threshold");
263        assert!((val - 3.14).abs() < f64::EPSILON);
264    }
265
266    #[test]
267    fn parse_or_warn_empty_string() {
268        let val = parse_or_warn!("", i32, 0, "empty input");
269        assert_eq!(val, 0);
270    }
271
272    #[test]
273    fn expect_or_error_some() {
274        let val: Option<&str> = Some("hello");
275        let result = expect_or_error!(val, "fallback", "must exist");
276        assert_eq!(result, "hello");
277    }
278
279    #[test]
280    fn expect_or_error_none() {
281        let val: Option<&str> = None;
282        let result = expect_or_error!(val, "fallback", "config key required");
283        assert_eq!(result, "fallback");
284    }
285
286    #[test]
287    fn expect_result_or_error_ok() {
288        let val: Result<i32, String> = Ok(100);
289        let result = expect_result_or_error!(val, 0, "must succeed");
290        assert_eq!(result, 100);
291    }
292
293    #[test]
294    fn expect_result_or_error_err() {
295        let val: Result<i32, String> = Err("db crashed".into());
296        let result = expect_result_or_error!(val, 0, "db must be available");
297        assert_eq!(result, 0);
298    }
299
300    #[test]
301    fn real_pattern_option_chain() {
302        let config: Option<Vec<String>> = Some(vec!["a".into(), "b".into()]);
303        let first = unwrap_or_warn!(
304            config.as_ref().and_then(|v| v.first().cloned()),
305            String::from("default"),
306            "first config value"
307        );
308        assert_eq!(first, "a");
309    }
310
311    #[test]
312    fn macros_compile_in_if_block() {
313        if true {
314            let _ = unwrap_or_warn!(Some(1), 0, "test");
315            let _ = result_or_warn!(Ok::<i32, String>(1), 0, "test");
316            let _ = parse_or_warn!("1", i32, 0, "test");
317            let _ = expect_or_error!(Some(1), 0, "test");
318            let _ = expect_result_or_error!(Ok::<i32, String>(1), 0, "test");
319        }
320    }
321
322    #[test]
323    fn macros_with_mut_binding() {
324        let mut count = unwrap_or_warn!(Some(0i32), -1, "initial count");
325        count += 1;
326        assert_eq!(count, 1);
327    }
328}