Skip to main content

ty_module_resolver/
strategy.rs

1use std::convert::Infallible;
2
3/// Generic handling of two possible approaches to an Error:
4///
5/// * [`FallibleStrategy`]: The code should simply fail
6/// * [`UseDefaultStrategy`]: The code should apply default values and never fail
7///
8/// Any function that wants to be made generic over these approaches should be changed thusly.
9///
10/// Old:
11///
12/// ```ignore
13/// fn do_thing()
14///     -> Result<T, E>
15/// {
16///     let x = something_fallible()?;
17///     Ok(x)
18/// }
19/// ```
20///
21/// New:
22///
23/// ```ignore
24/// fn do_thing<Strategy: MisconfigurationStrategy>(strategy: &Strategy)
25///     -> Result<T, Strategy::Error<E>>
26/// {
27///     let x = strategy.fallback(something_fallible(), |err| {
28///         tracing::debug!("Failed to get value: {err}");
29///         MyType::default()
30///     })?;
31///     Ok(x)
32/// }
33/// ```
34///
35/// The key trick is instead of returning `Result<T, E>` your function should
36/// return `Result<T, Strategy::Error<E>>`. Which simplifies to:
37///
38/// * [`FallibleStrategy`]: `Result<T, E>`
39/// * [`UseDefaultStrategy`]: `Result<T, Infallible>` ~= `T`
40///
41/// Notably, if your function returns `Result<T, Strategy::Error<E>>` you will
42/// be *statically prevented* from returning an `Err` without going through
43/// [`MisconfigurationStrategy::fallback`][] or [`MisconfigurationStrategy::fallback_opt`][]
44/// which ensure you're handling both approaches (or you wrote an `unwrap` but
45/// those standout far more than adding a new `?` to a function that must be able to Not Fail).
46///
47/// Also, for any caller that passes in [`UseDefaultStrategy`], they will be able
48/// to write `let Ok(val) = do_thing(&UseDefaultStrategy);` instead of having to
49/// write an `unwrap()`.
50pub trait MisconfigurationStrategy {
51    /// * [`FallibleStrategy`][]: `E`
52    /// * [`UseDefaultStrategy`][]: [`Infallible`]
53    type Error<E>;
54
55    /// Try to get the value out of a Result that we need to proceed.
56    ///
57    /// If [`UseDefaultStrategy`], on `Err` this will call `fallback_fn` to compute
58    /// a default value and always return `Ok`.
59    ///
60    /// If [`FallibleStrategy`] this is a no-op and will return the Result.
61    fn fallback<T, E>(
62        &self,
63        result: Result<T, E>,
64        fallback_fn: impl FnOnce(E) -> T,
65    ) -> Result<T, Self::Error<E>>;
66
67    /// Try to get the value out of a Result that we can do without.
68    ///
69    /// If [`UseDefaultStrategy`], this will call `fallback_fn` to report an issue
70    /// (i.e. you can invoke `tracing::debug!` or something) and then return `None`.
71    ///
72    /// If [`FallibleStrategy`] this is a no-op and will return the Result (but `Ok` => `Ok(Some)`).
73    fn fallback_opt<T, E>(
74        &self,
75        result: Result<T, E>,
76        fallback_fn: impl FnOnce(E),
77    ) -> Result<Option<T>, Self::Error<E>>;
78
79    /// Convenience to convert the inner `Error` to `anyhow::Error`.
80    fn to_anyhow<T, E>(
81        &self,
82        result: Result<T, Self::Error<E>>,
83    ) -> Result<T, Self::Error<anyhow::Error>>
84    where
85        anyhow::Error: From<E>;
86
87    /// Convenience to map the inner `Error`.
88    fn map_err<T, E1, E2>(
89        &self,
90        result: Result<T, Self::Error<E1>>,
91        map_err: impl FnOnce(E1) -> E2,
92    ) -> Result<T, Self::Error<E2>>;
93}
94
95/// A [`MisconfigurationStrategy`] that refuses to *ever* return an `Err`
96/// and instead substitutes default values or skips functionality.
97#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
98pub struct UseDefaultStrategy;
99
100impl MisconfigurationStrategy for UseDefaultStrategy {
101    type Error<E> = Infallible;
102    fn fallback<T, E>(
103        &self,
104        result: Result<T, E>,
105        fallback_fn: impl FnOnce(E) -> T,
106    ) -> Result<T, Self::Error<E>> {
107        Ok(result.unwrap_or_else(fallback_fn))
108    }
109
110    fn fallback_opt<T, E>(
111        &self,
112        result: Result<T, E>,
113        fallback_fn: impl FnOnce(E),
114    ) -> Result<Option<T>, Self::Error<E>> {
115        match result {
116            Ok(val) => Ok(Some(val)),
117            Err(e) => {
118                fallback_fn(e);
119                Ok(None)
120            }
121        }
122    }
123
124    fn to_anyhow<T, E>(
125        &self,
126        result: Result<T, Self::Error<E>>,
127    ) -> Result<T, Self::Error<anyhow::Error>>
128    where
129        anyhow::Error: From<E>,
130    {
131        let Ok(val) = result;
132        Ok(val)
133    }
134
135    fn map_err<T, E1, E2>(
136        &self,
137        result: Result<T, Self::Error<E1>>,
138        _map_err: impl FnOnce(E1) -> E2,
139    ) -> Result<T, Self::Error<E2>> {
140        let Ok(val) = result;
141        Ok(val)
142    }
143}
144
145/// A [`MisconfigurationStrategy`] that happily fails whenever
146/// an important `Err` is encountered.
147#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
148pub struct FallibleStrategy;
149
150impl MisconfigurationStrategy for FallibleStrategy {
151    type Error<E> = E;
152
153    fn fallback<T, E>(
154        &self,
155        result: Result<T, E>,
156        _fallback_fn: impl FnOnce(E) -> T,
157    ) -> Result<T, Self::Error<E>> {
158        result
159    }
160
161    fn fallback_opt<T, E>(
162        &self,
163        result: Result<T, E>,
164        _fallback_fn: impl FnOnce(E),
165    ) -> Result<Option<T>, Self::Error<E>> {
166        result.map(Some)
167    }
168
169    fn to_anyhow<T, E>(
170        &self,
171        result: Result<T, Self::Error<E>>,
172    ) -> Result<T, Self::Error<anyhow::Error>>
173    where
174        anyhow::Error: From<E>,
175    {
176        Ok(result?)
177    }
178
179    fn map_err<T, E1, E2>(
180        &self,
181        result: Result<T, Self::Error<E1>>,
182        map_err: impl FnOnce(E1) -> E2,
183    ) -> Result<T, Self::Error<E2>> {
184        result.map_err(map_err)
185    }
186}