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}