unwrap-safe 0.1.0

Safe unwrap replacements that log instead of panic — drop-in macros for Option and Result
Documentation
// Copyright (c) Ted Kaplan. All Rights Reserved.
// SPDX-License-Identifier: MIT

//! Safe unwrap replacements that log instead of panic.
//!
//! `.unwrap()` and `.expect()` are useful during development but cause
//! production outages when invariants break. This crate provides drop-in
//! macro replacements that degrade gracefully: return a default value and
//! emit a structured log instead of panicking.
//!
//! # Quick Reference
//!
//! | Before | After | For |
//! |--------|-------|-----|
//! | `opt.unwrap()` | `unwrap_or_warn!(opt, default, "ctx")` | `Option<T>` |
//! | `res.unwrap()` | `result_or_warn!(res, default, "ctx")` | `Result<T, E>` |
//! | `s.parse::<T>().unwrap()` | `parse_or_warn!(s, T, default, "ctx")` | String parsing |
//! | `opt.expect("msg")` | `expect_or_error!(opt, default, "msg")` | Option soft-expect |
//! | `res.expect("msg")` | `expect_result_or_error!(res, default, "msg")` | Result soft-expect |
//!
//! All macros include `file!()` and `line!()` in the log output.
//!
//! # Example
//!
//! ```rust
//! use unwrap_safe::{unwrap_or_warn, result_or_warn, parse_or_warn};
//!
//! // Option: return default on None
//! let name: Option<String> = None;
//! let display = unwrap_or_warn!(name, String::from("anonymous"), "user display name");
//! assert_eq!(display, "anonymous");
//!
//! // Result: return default on Err
//! let val: Result<i32, String> = Err("timeout".into());
//! let count = result_or_warn!(val, 0, "request count");
//! assert_eq!(count, 0);
//!
//! // Parse: return default on bad input
//! let port = parse_or_warn!("not_a_port", u16, 8080, "PORT env var");
//! assert_eq!(port, 8080);
//! ```

/// Unwrap an `Option<T>`, returning a default and logging a warning if `None`.
///
/// Drop-in replacement for `.unwrap()` and `.expect()` on `Option` values
/// where panicking is unacceptable in production.
///
/// For `Result` types, use [`result_or_warn!`] instead — it captures the
/// error value in the log.
///
/// # Examples
///
/// ```rust
/// use unwrap_safe::unwrap_or_warn;
///
/// let val: Option<i32> = None;
/// let result = unwrap_or_warn!(val, 42, "loading user preference");
/// assert_eq!(result, 42);
/// ```
#[macro_export]
macro_rules! unwrap_or_warn {
    ($opt:expr, $default:expr, $context:expr) => {
        match $opt {
            Some(v) => v,
            None => {
                ::tracing::warn!(
                    context = $context,
                    location = concat!(file!(), ":", line!()),
                    "Unexpected None — using default (was unwrap)"
                );
                $default
            }
        }
    };
}

/// Unwrap a `Result<T, E>`, returning a default and logging a warning on `Err`.
///
/// Captures the error value in the log, unlike [`unwrap_or_warn!`] which
/// is for `Option` types.
///
/// # Examples
///
/// ```rust
/// use unwrap_safe::result_or_warn;
///
/// let val: Result<i32, String> = Err("timeout".into());
/// let result = result_or_warn!(val, -1, "db query");
/// assert_eq!(result, -1);
/// ```
#[macro_export]
macro_rules! result_or_warn {
    ($result:expr, $default:expr, $context:expr) => {
        match $result {
            Ok(v) => v,
            Err(e) => {
                ::tracing::warn!(
                    error = %e,
                    context = $context,
                    location = concat!(file!(), ":", line!()),
                    "Unexpected error — using default (was unwrap)"
                );
                $default
            }
        }
    };
}

/// Parse a string into a type, returning a default and logging on failure.
///
/// Drop-in replacement for `str.parse::<T>().unwrap()`.
///
/// # Examples
///
/// ```rust
/// use unwrap_safe::parse_or_warn;
///
/// let port = parse_or_warn!("not_a_number", u16, 8080, "PORT config");
/// assert_eq!(port, 8080);
///
/// let port = parse_or_warn!("3000", u16, 8080, "PORT config");
/// assert_eq!(port, 3000);
/// ```
#[macro_export]
macro_rules! parse_or_warn {
    ($s:expr, $ty:ty, $default:expr, $context:expr) => {
        match $s.parse::<$ty>() {
            Ok(v) => v,
            Err(e) => {
                ::tracing::warn!(
                    input = %$s,
                    error = %e,
                    context = $context,
                    location = concat!(file!(), ":", line!()),
                    "Parse failed — using default (was unwrap)"
                );
                $default
            }
        }
    };
}

/// Soft expect for `Option`: logs an error and returns a default instead of panicking.
///
/// For cases where the code currently uses `.expect("message")` but a panic
/// would be worse than a degraded response.
///
/// # Examples
///
/// ```rust
/// use unwrap_safe::expect_or_error;
///
/// let val: Option<&str> = None;
/// let result = expect_or_error!(val, "fallback", "config key must exist");
/// assert_eq!(result, "fallback");
/// ```
#[macro_export]
macro_rules! expect_or_error {
    ($opt:expr, $default:expr, $msg:expr) => {
        match $opt {
            Some(v) => v,
            None => {
                ::tracing::error!(
                    expectation = $msg,
                    location = concat!(file!(), ":", line!()),
                    "EXPECT FAILED — invariant violated, using fallback (was expect/unwrap)"
                );
                $default
            }
        }
    };
}

/// Soft expect for `Result`: logs an error and returns a default instead of panicking.
///
/// # Examples
///
/// ```rust
/// use unwrap_safe::expect_result_or_error;
///
/// let val: Result<i32, String> = Err("connection refused".into());
/// let result = expect_result_or_error!(val, 0, "db connection must succeed");
/// assert_eq!(result, 0);
/// ```
#[macro_export]
macro_rules! expect_result_or_error {
    ($result:expr, $default:expr, $msg:expr) => {
        match $result {
            Ok(v) => v,
            Err(e) => {
                ::tracing::error!(
                    error = %e,
                    expectation = $msg,
                    location = concat!(file!(), ":", line!()),
                    "EXPECT FAILED — invariant violated, using fallback (was expect/unwrap)"
                );
                $default
            }
        }
    };
}

#[cfg(test)]
mod tests {
    #[test]
    fn unwrap_or_warn_some() {
        let val: Option<i32> = Some(42);
        let result = unwrap_or_warn!(val, 0, "test");
        assert_eq!(result, 42);
    }

    #[test]
    fn unwrap_or_warn_none() {
        let val: Option<i32> = None;
        let result = unwrap_or_warn!(val, -1, "test fallback");
        assert_eq!(result, -1);
    }

    #[test]
    fn unwrap_or_warn_string_option() {
        let val: Option<String> = None;
        let result = unwrap_or_warn!(val, String::from("default"), "username lookup");
        assert_eq!(result, "default");
    }

    #[test]
    fn result_or_warn_ok() {
        let val: Result<i32, String> = Ok(42);
        let result = result_or_warn!(val, 0, "test");
        assert_eq!(result, 42);
    }

    #[test]
    fn result_or_warn_err() {
        let val: Result<i32, String> = Err("broken".into());
        let result = result_or_warn!(val, -1, "test fallback");
        assert_eq!(result, -1);
    }

    #[test]
    fn result_or_warn_io_error() {
        let val: Result<String, std::io::Error> =
            Err(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
        let result = result_or_warn!(val, String::from("fallback"), "file read");
        assert_eq!(result, "fallback");
    }

    #[test]
    fn parse_or_warn_success() {
        let port = parse_or_warn!("3000", u16, 8080, "port config");
        assert_eq!(port, 3000);
    }

    #[test]
    fn parse_or_warn_failure() {
        let port = parse_or_warn!("not_a_number", u16, 8080, "port config");
        assert_eq!(port, 8080);
    }

    #[test]
    fn parse_or_warn_float() {
        let val = parse_or_warn!("3.14", f64, 0.0, "threshold");
        assert!((val - 3.14).abs() < f64::EPSILON);
    }

    #[test]
    fn parse_or_warn_empty_string() {
        let val = parse_or_warn!("", i32, 0, "empty input");
        assert_eq!(val, 0);
    }

    #[test]
    fn expect_or_error_some() {
        let val: Option<&str> = Some("hello");
        let result = expect_or_error!(val, "fallback", "must exist");
        assert_eq!(result, "hello");
    }

    #[test]
    fn expect_or_error_none() {
        let val: Option<&str> = None;
        let result = expect_or_error!(val, "fallback", "config key required");
        assert_eq!(result, "fallback");
    }

    #[test]
    fn expect_result_or_error_ok() {
        let val: Result<i32, String> = Ok(100);
        let result = expect_result_or_error!(val, 0, "must succeed");
        assert_eq!(result, 100);
    }

    #[test]
    fn expect_result_or_error_err() {
        let val: Result<i32, String> = Err("db crashed".into());
        let result = expect_result_or_error!(val, 0, "db must be available");
        assert_eq!(result, 0);
    }

    #[test]
    fn real_pattern_option_chain() {
        let config: Option<Vec<String>> = Some(vec!["a".into(), "b".into()]);
        let first = unwrap_or_warn!(
            config.as_ref().and_then(|v| v.first().cloned()),
            String::from("default"),
            "first config value"
        );
        assert_eq!(first, "a");
    }

    #[test]
    fn macros_compile_in_if_block() {
        if true {
            let _ = unwrap_or_warn!(Some(1), 0, "test");
            let _ = result_or_warn!(Ok::<i32, String>(1), 0, "test");
            let _ = parse_or_warn!("1", i32, 0, "test");
            let _ = expect_or_error!(Some(1), 0, "test");
            let _ = expect_result_or_error!(Ok::<i32, String>(1), 0, "test");
        }
    }

    #[test]
    fn macros_with_mut_binding() {
        let mut count = unwrap_or_warn!(Some(0i32), -1, "initial count");
        count += 1;
        assert_eq!(count, 1);
    }
}