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}