Skip to main content

oopsie_core/
extras.rs

1//! Ready-made [`Capturable`] types for ambient process and environment state.
2//!
3//! Each type snapshots a piece of context — the working directory, home directory, command-line
4//! arguments, or a named environment variable — into an error through a `#[oopsie(capture)]`
5//! field. Capturing never fails: an unavailable directory or variable is recorded as `None`.
6//!
7//! Requires the `extras` feature.
8
9use std::{env, ffi::OsStr, ops::Deref};
10
11use crate::Capturable;
12
13/// The current working directory at the time the error was built, or `None` if it could not
14/// be read.
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[cfg_attr(
17    feature = "serde",
18    derive(serde::Serialize, serde::Deserialize),
19    serde(transparent)
20)]
21pub struct CurrentDirOpt(Option<std::path::PathBuf>);
22
23impl std::fmt::Display for CurrentDirOpt {
24    #[inline]
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match &self.0 {
27            Some(path) => path.display().fmt(f),
28            None => write!(f, "<current dir error>"),
29        }
30    }
31}
32
33impl Deref for CurrentDirOpt {
34    type Target = Option<std::path::PathBuf>;
35
36    #[inline]
37    fn deref(&self) -> &Self::Target {
38        &self.0
39    }
40}
41
42impl CurrentDirOpt {
43    /// Consumes this value, yielding the inner `Option`.
44    #[inline]
45    #[must_use]
46    pub fn into_inner(self) -> Option<std::path::PathBuf> {
47        self.0
48    }
49}
50
51impl Capturable for CurrentDirOpt {
52    #[inline]
53    fn capture() -> Self {
54        Self(env::current_dir().ok())
55    }
56}
57
58/// The user's home directory at the time the error was built, or `None` if it could not be
59/// determined.
60#[derive(Debug, Clone, PartialEq, Eq)]
61#[cfg_attr(
62    feature = "serde",
63    derive(serde::Serialize, serde::Deserialize),
64    serde(transparent)
65)]
66pub struct HomeDirOpt(Option<std::path::PathBuf>);
67
68impl std::fmt::Display for HomeDirOpt {
69    #[inline]
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match &self.0 {
72            Some(path) => path.display().fmt(f),
73            None => write!(f, "<home dir error>"),
74        }
75    }
76}
77
78impl Deref for HomeDirOpt {
79    type Target = Option<std::path::PathBuf>;
80
81    #[inline]
82    fn deref(&self) -> &Self::Target {
83        &self.0
84    }
85}
86
87impl HomeDirOpt {
88    /// Consumes this value, yielding the inner `Option`.
89    #[inline]
90    #[must_use]
91    pub fn into_inner(self) -> Option<std::path::PathBuf> {
92        self.0
93    }
94}
95
96impl Capturable for HomeDirOpt {
97    #[inline]
98    fn capture() -> Self {
99        Self(env::home_dir())
100    }
101}
102
103/// The process's command-line arguments at the time the error was built.
104#[derive(Debug, Clone, PartialEq, Eq)]
105#[cfg_attr(
106    feature = "serde",
107    derive(serde::Serialize, serde::Deserialize),
108    serde(transparent)
109)]
110pub struct EnvArgs(Box<[Box<OsStr>]>);
111
112impl std::fmt::Display for EnvArgs {
113    #[inline]
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "{:?}", self.0)
116    }
117}
118
119impl Deref for EnvArgs {
120    type Target = [Box<OsStr>];
121
122    #[inline]
123    fn deref(&self) -> &Self::Target {
124        &self.0
125    }
126}
127
128impl EnvArgs {
129    /// Consumes this value, yielding the inner `Box<[Box<OsStr>]>`.
130    #[inline]
131    #[must_use]
132    pub fn into_inner(self) -> Box<[Box<OsStr>]> {
133        self.0
134    }
135}
136
137impl Capturable for EnvArgs {
138    #[inline]
139    fn capture() -> Self {
140        Self(
141            env::args_os()
142                .map(Into::into)
143                .collect::<Box<[Box<OsStr>]>>(),
144        )
145    }
146}
147
148/// The value of the environment variable named by `E` at the time the error was built, or
149/// `None` if it was unset.
150#[derive(Debug, Clone, PartialEq, Eq)]
151#[cfg_attr(
152    feature = "serde",
153    derive(serde::Serialize, serde::Deserialize),
154    serde(transparent)
155)]
156pub struct EnvVarOpt<E: EnvVarName>(Option<Box<str>>, std::marker::PhantomData<E>);
157
158/// Names the environment variable that an [`EnvVarOpt`] reads.
159///
160/// Declare implementors with [`make_env_var!`](env_vars::make_env_var) rather than by hand.
161pub trait EnvVarName {
162    /// The environment variable name.
163    const NAME: &'static str;
164}
165
166impl<E: EnvVarName> Capturable for EnvVarOpt<E> {
167    #[inline]
168    fn capture() -> Self {
169        Self(
170            env::var(E::NAME).ok().map(Into::into),
171            std::marker::PhantomData,
172        )
173    }
174}
175
176impl<E: EnvVarName> std::fmt::Display for EnvVarOpt<E> {
177    #[inline]
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        match &self.0 {
180            Some(value) => value.fmt(f),
181            None => write!(f, "<env var {} unset>", E::NAME),
182        }
183    }
184}
185
186impl<E: EnvVarName> Deref for EnvVarOpt<E> {
187    type Target = Option<Box<str>>;
188
189    #[inline]
190    fn deref(&self) -> &Self::Target {
191        &self.0
192    }
193}
194
195impl<E: EnvVarName> EnvVarOpt<E> {
196    /// Consumes this value, yielding the inner `Option`.
197    #[inline]
198    #[must_use]
199    pub fn into_inner(self) -> Option<Box<str>> {
200        self.0
201    }
202}
203
204/// Ready-made [`EnvVarOpt`] aliases for common variables, plus the
205/// [`make_env_var!`](env_vars::make_env_var) macro for declaring your own.
206pub mod env_vars {
207    #[doc(hidden)]
208    pub mod __private {
209        pub use pastey;
210    }
211
212    #[doc(hidden)]
213    #[macro_export]
214    macro_rules! __make_env_var {
215        ($($name:ident),+ $(,)?) => { $crate::extras::env_vars::__private::pastey::paste! { $(
216            #[doc = "Marker type for the `" $name "` environment variable."]
217            #[derive(Debug, Clone, Copy, PartialEq, Eq)]
218            pub struct [< $name:camel VarName>];
219
220            impl $crate::extras::EnvVarName for [< $name:camel VarName>] {
221                const NAME: &'static str = stringify!($name);
222            }
223
224            #[doc = "Captures the `" $name "` environment variable."]
225            pub type [< $name:camel Opt >] = $crate::extras::EnvVarOpt<[< $name:camel VarName>]>;
226        )+ }};
227    }
228    /// Declare [`EnvVarName`](super::EnvVarName) markers and [`EnvVarOpt`](super::EnvVarOpt)
229    /// aliases for named variables.
230    ///
231    /// `make_env_var!(PATH, RUST_LOG)` generates `PathOpt` and `RustLogOpt` capturable types;
232    /// each name becomes an `UpperCamelCase` alias with an `Opt` suffix.
233    pub use crate::__make_env_var as make_env_var;
234
235    make_env_var!(PATH, HOME, USER, RUST_BACKTRACE, RUST_LOG,);
236}
237
238#[cfg(test)]
239mod tests {
240    use super::env_vars::make_env_var;
241
242    use super::*;
243
244    #[test]
245    fn current_dir_opt_captures_current_dir() {
246        let captured = CurrentDirOpt::capture();
247
248        assert_eq!(
249            captured.as_ref().unwrap(),
250            &std::env::current_dir().unwrap(),
251            "captured current dir should match actual current dir"
252        );
253    }
254
255    #[test]
256    fn env_args_opt_captures_args() {
257        let captured = EnvArgs::capture();
258        let actual_args = std::env::args_os()
259            .map(Into::into)
260            .collect::<Vec<Box<OsStr>>>();
261
262        assert_eq!(
263            captured.as_ref(),
264            &actual_args,
265            "captured env args should match actual env args"
266        );
267    }
268
269    #[test]
270    fn env_var_opt_captures_env_var() {
271        #[expect(
272            unsafe_code,
273            reason = "test setup requires setting an env var at runtime"
274        )]
275        unsafe {
276            std::env::set_var("__OOPSIE_TEST_ENV_VAR", std::process::id().to_string());
277        };
278        make_env_var!(__OOPSIE_TEST_ENV_VAR);
279
280        let captured = OopsieTestEnvVarOpt::capture();
281        assert_eq!(
282            captured.as_deref().unwrap(),
283            &std::process::id().to_string(),
284            "captured env var should match actual env var value"
285        );
286    }
287}