spacetimedb_data_structures/
error_stream.rs

1//! Types, traits, and macros for working with non-empty streams of errors.
2//!
3//! The `ErrorStream<_>` type provides a collection that stores a non-empty, unordered stream of errors.
4//! This is valuable for collecting as many errors as possible before returning them to the user,
5//! which allows the user to work through the errors in the order of their choosing.
6//! This is particularly useful for CLI tools.
7//!
8//! Example usage:
9//! ```
10//! use spacetimedb_data_structures::error_stream::{
11//!     ErrorStream,
12//!     CombineErrors,
13//!     CollectAllErrors
14//! };
15//! use std::collections::HashSet;
16//!
17//! enum MyError { /* ... */ };
18//! type MyErrors = ErrorStream<MyError>;
19//!
20//! type Name =
21//!     /* ... */
22//! #   String
23//!     ;
24//!
25//! type Age =
26//!     /* ... */
27//! #   i32
28//!     ;
29//!
30//! fn validate_name(name: String) -> Result<Name, MyErrors> {
31//!     // ...
32//! #   Ok(name)
33//! }
34//!
35//! fn validate_age(age: i32) -> Result<Age, MyErrors> {
36//!     // ...
37//! #   Ok(age)
38//! }
39//!
40//! fn validate_person(
41//!     name: String,
42//!     age: i32,
43//!     friends: Vec<String>
44//! ) -> Result<(Name, Age, HashSet<Name>), MyErrors> {
45//!     // First, we perform some validation on various pieces
46//!     // of input data, WITHOUT using `?`.
47//!     let name: Result<Name, MyErrors> = validate_name(name);
48//!     let age: Result<Age, MyErrors> = validate_age(age);
49//!
50//!     // If we have multiple pieces of data, we can use
51//!     // `collect_all_errors` to build an arbitrary collection from them.
52//!     // If there are any errors, all of these errors
53//!     // will be returned in a single ErrorStream.
54//!     let friends: Result<HashSet<Name>, MyErrors> = friends
55//!         .into_iter()
56//!         .map(validate_name)
57//!         .collect_all_errors();
58//!
59//!     // Now, we can combine the results into a single result.
60//!     // If there are any errors, they will be returned in a
61//!     // single ErrorStream.
62//!     let (name, age, friends): (Name, Age, HashSet<Name>) =
63//!         (name, age, friends).combine_errors()?;
64//!
65//!     Ok((name, age, friends))
66//! }
67//! ```
68//!
69//! ## Best practices
70//!
71//! ### Use `ErrorStream` everywhere
72//! It is best to use `ErrorStream` everywhere in a multiple-error module, even
73//! for methods that return only a single error. `CombineAllErrors` and `CollectAllErrors`
74//! can only be implemented for types built from `Result<_, ErrorStream<_>>` due to trait conflicts.
75//! `ErrorStream` uses a `smallvec::SmallVec` internally, so it is efficient for single errors.
76//!
77//! You can convert an `E` to an `ErrorStream<E>` using `.into()`.
78//!
79//! ### Not losing any errors
80//! When using this module, it is best to avoid using `?` until as late as possible,
81//! and to completely avoid using the `Result<Collection<_>, _>::collect` method.
82//! Both of these may result in errors being discarded.
83//!
84//! Prefer using `Result::and_then` for chaining operations that may fail,
85//! and `CollectAllErrors::collect_all_errors` for collecting errors from iterators.
86
87use crate::map::HashSet;
88use std::{fmt, hash::Hash};
89
90/// A non-empty stream of errors.
91///
92/// Logically, this type is unordered, and it is not guaranteed that the errors will be returned in the order they were added.
93/// Attach identifying information to your errors if you want to sort them.
94///
95/// This struct is intended to be used with:
96/// - The [CombineErrors] trait, which allows you to combine a tuples of results.
97/// - The [CollectAllErrors] trait, which allows you to collect errors from an iterator of results.
98///
99/// To create an `ErrorStream` from a single error, you can use `from` or `into`:
100/// ```
101/// use spacetimedb_data_structures::error_stream::ErrorStream;
102///
103/// enum MyError {
104///     A(u32),
105///     B
106/// }
107///
108/// let error: ErrorStream<MyError> = MyError::A(1).into();
109/// // or
110/// let error = ErrorStream::from(MyError::A(1));
111/// ```
112///
113/// This does not allocate (unless your error allocates, of course).
114#[derive(thiserror::Error, Debug, Clone, Default, PartialEq, Eq)]
115pub struct ErrorStream<E>(smallvec::SmallVec<[E; 1]>);
116
117impl<E> ErrorStream<E> {
118    /// Build an error stream from a non-empty collection.
119    /// If the collection is empty, panic.
120    pub fn expect_nonempty<I: IntoIterator<Item = E>>(errors: I) -> Self {
121        let mut errors = errors.into_iter();
122        let first = errors.next().expect("expected at least one error");
123        let mut stream = Self::from(first);
124        stream.extend(errors);
125        stream
126    }
127
128    /// Add some extra errors to a result.
129    ///
130    /// If there are no errors, the result is not modified.
131    /// If there are errors, and the result is `Err`, the errors are added to the stream.
132    /// If there are errors, and the result is `Ok`, the `Ok` value is discarded, and the errors are returned in a stream.
133    pub fn add_extra_errors<T>(
134        result: Result<T, ErrorStream<E>>,
135        extra_errors: impl IntoIterator<Item = E>,
136    ) -> Result<T, ErrorStream<E>> {
137        match result {
138            Ok(value) => {
139                let errors: SmallVec<[E; 1]> = extra_errors.into_iter().collect();
140                if errors.is_empty() {
141                    Ok(value)
142                } else {
143                    Err(ErrorStream(errors))
144                }
145            }
146            Err(mut errors) => {
147                errors.extend(extra_errors);
148                Err(errors)
149            }
150        }
151    }
152
153    /// Returns an iterator over the errors in the stream.
154    pub fn iter(&self) -> impl Iterator<Item = &E> {
155        self.0.iter()
156    }
157
158    /// Returns a mutable iterator over the errors in the stream.
159    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut E> {
160        self.0.iter_mut()
161    }
162
163    /// Returns an iterator over the errors in the stream, consuming the stream.
164    pub fn drain(&mut self) -> impl Iterator<Item = E> + '_ {
165        self.0.drain(..)
166    }
167
168    /// Push an error onto the stream.
169    pub fn push(&mut self, error: E) {
170        self.0.push(error);
171    }
172
173    /// Extend the stream with another stream.
174    pub fn extend(&mut self, other: impl IntoIterator<Item = E>) {
175        self.0.extend(other);
176    }
177
178    /// Unpack an error into `self`, returning `None` if there was an error.
179    /// This is not exposed because `CombineErrors` is more convenient.
180    #[inline(never)] // don't optimize this too much
181    #[cold]
182    fn unpack<T, ES: Into<ErrorStream<E>>>(&mut self, result: Result<T, ES>) -> Option<T> {
183        match result {
184            Ok(value) => Some(value),
185            Err(error) => {
186                self.0.extend(error.into().0);
187                None
188            }
189        }
190    }
191}
192impl<E: fmt::Display> fmt::Display for ErrorStream<E> {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        writeln!(f, "Errors occurred:")?;
195        for error in self.iter() {
196            writeln!(f, "{error}\n")?;
197        }
198        Ok(())
199    }
200}
201
202impl<E: Ord + Eq> ErrorStream<E> {
203    /// Sort and deduplicate the errors in the error stream.
204    pub fn sort_deduplicate(mut self) -> Self {
205        self.0.sort_unstable();
206        self.0.dedup();
207        self
208    }
209}
210impl<E: Eq + Hash> ErrorStream<E> {
211    /// Hash and deduplicate the errors in the error stream.
212    /// The resulting error stream has an arbitrary order.
213    pub fn hash_deduplicate(mut self) -> Self {
214        let set = self.0.drain(..).collect::<HashSet<_>>();
215        self.0.extend(set);
216        self
217    }
218}
219
220impl<E> IntoIterator for ErrorStream<E> {
221    type Item = <smallvec::SmallVec<[E; 1]> as IntoIterator>::Item;
222    type IntoIter = <smallvec::SmallVec<[E; 1]> as IntoIterator>::IntoIter;
223
224    fn into_iter(self) -> Self::IntoIter {
225        self.0.into_iter()
226    }
227}
228
229impl<E> From<E> for ErrorStream<E> {
230    fn from(error: E) -> Self {
231        Self(smallvec::smallvec![error])
232    }
233}
234
235/// A trait for converting a tuple of `Result<_, ErrorStream<_>>`s
236/// into a `Result` of a tuple or combined `ErrorStream`.
237pub trait CombineErrors {
238    /// The type of the output if all results are `Ok`.
239    type Ok;
240    /// The type of the output if any result is `Err`.
241    type Error;
242
243    /// Combine errors from multiple places into one.
244    /// This can be thought of as a kind of parallel `?`.
245    ///
246    /// If your goal is to show the user as many errors as possible, you should
247    /// call this as late as possible, on as wide a tuple as you can.
248    ///
249    /// Example usage:
250    ///
251    /// ```
252    /// use spacetimedb_data_structures::error_stream::{ErrorStream, CombineErrors};
253    ///
254    /// struct MyError { cause: String };
255    ///
256    /// fn age() -> Result<i32, ErrorStream<MyError>> {
257    ///     //...
258    /// # Ok(1)
259    /// }
260    ///
261    /// fn name() -> Result<String, ErrorStream<MyError>> {
262    ///     // ...
263    /// # Ok("hi".into())
264    /// }
265    ///
266    /// fn likes_dogs() -> Result<bool, ErrorStream<MyError>> {
267    ///     // ...
268    /// # Ok(false)
269    /// }
270    ///
271    /// fn description() -> Result<String, ErrorStream<MyError>> {
272    ///     // A typical usage of the API:
273    ///     // Collect multiple `Result`s in parallel, only using
274    ///     // `.combine_errors()?` once no more progress can be made.
275    ///     let (age, name, likes_dogs) =
276    ///         (age(), name(), likes_dogs()).combine_errors()?;
277    ///
278    ///     Ok(format!(
279    ///         "{} is {} years old and {}",
280    ///         name,
281    ///         age,
282    ///         if likes_dogs { "likes dogs" } else { "does not like dogs" }
283    ///     ))
284    /// }
285    /// ```
286    fn combine_errors(self) -> Result<Self::Ok, ErrorStream<Self::Error>>;
287}
288
289macro_rules! tuple_combine_errors {
290    ($($T:ident),*) => {
291        impl<$($T,)* E> CombineErrors for ($(Result<$T, ErrorStream<E>>,)*) {
292            type Ok = ($($T,)*);
293            type Error = E;
294
295            #[allow(non_snake_case)]
296            fn combine_errors(self) -> Result<Self::Ok, ErrorStream<Self::Error>> {
297                let mut errors = ErrorStream(Default::default());
298                let ($($T,)* ) = self;
299                $(
300                    let $T = errors.unpack($T);
301                )*
302                if errors.0.is_empty() {
303                    // correctness: none of these pushed an error to `errors`, so by the contract of `unpack`, they must all be `Some`.
304                    Ok(($($T.unwrap(),)*))
305                } else {
306                    Err(errors)
307                }
308            }
309        }
310    };
311}
312
313tuple_combine_errors!(T1, T2);
314tuple_combine_errors!(T1, T2, T3);
315tuple_combine_errors!(T1, T2, T3, T4);
316tuple_combine_errors!(T1, T2, T3, T4, T5);
317tuple_combine_errors!(T1, T2, T3, T4, T5, T6);
318tuple_combine_errors!(T1, T2, T3, T4, T5, T6, T7);
319tuple_combine_errors!(T1, T2, T3, T4, T5, T6, T7, T8);
320tuple_combine_errors!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
321tuple_combine_errors!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
322tuple_combine_errors!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
323tuple_combine_errors!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
324
325/// A trait for collecting errors from an iterator of results,
326/// returning all errors if anything failed.
327pub trait CollectAllErrors {
328    /// The item type we are aggregating.
329    type Item;
330
331    /// The error type we are aggregating.
332    type Error;
333
334    /// Collect errors from an iterator of results into a single error stream.
335    /// If all results are `Ok`, returns the collected values. Otherwise,
336    /// combine all errors into a single error stream, and return it.
337    ///
338    /// You CANNOT use the standard library function `Result<T, ErrorStream<E>>::collect()` for this,
339    /// as it will return the FIRST error it encounters, rather than collecting all errors!
340    ///
341    /// The collection can be anything that implements `FromIterator`.
342    ///
343    /// Example usage:
344    /// ```
345    /// use spacetimedb_data_structures::error_stream::{
346    ///     ErrorStream,
347    ///     CollectAllErrors
348    /// };
349    /// use std::collections::HashSet;
350    ///
351    /// enum MyError { /* ... */ }
352    ///
353    /// fn operation(
354    ///     data: String,
355    ///     checksum: u32
356    /// ) -> Result<i32, ErrorStream<MyError>> {
357    ///     /* ... */
358    /// #   Ok(1)
359    /// }
360    ///
361    /// fn many_operations(
362    ///     data: Vec<(String, u32)>
363    /// ) -> Result<HashSet<i32>, ErrorStream<MyError>> {
364    ///     data
365    ///         .into_iter()
366    ///         .map(|(data, checksum)| operation(data, checksum))
367    ///         .collect_all_errors::<HashSet<_>>()
368    /// }
369    /// ```
370    fn collect_all_errors<C: FromIterator<Self::Item>>(self) -> Result<C, ErrorStream<Self::Error>>;
371}
372
373impl<T, E, I: Iterator<Item = Result<T, ErrorStream<E>>>> CollectAllErrors for I {
374    type Item = T;
375    type Error = E;
376
377    fn collect_all_errors<Collection: FromIterator<Self::Item>>(self) -> Result<Collection, ErrorStream<Self::Error>> {
378        // not in a valid state: contains no errors!
379        let mut all_errors = ErrorStream(Default::default());
380
381        let collection = self
382            .filter_map(|result| match result {
383                Ok(value) => Some(value),
384                Err(errors) => {
385                    all_errors.extend(errors);
386                    None
387                }
388            })
389            .collect::<Collection>();
390
391        if all_errors.0.is_empty() {
392            // invalid state is not returned.
393            Ok(collection)
394        } else {
395            // not empty, so we're good to return it.
396            Err(all_errors)
397        }
398    }
399}
400
401/// Helper macro to match against an error stream, expecting a specific error.
402/// For use in tests.
403/// Panics if a matching error is not found.
404/// Multiple matches are allowed.
405///
406/// Parameters:
407/// - `$result` must be a `Result<_, ErrorStream<E>>`.
408/// - `$expected` is a pattern to match against the error.
409/// - `$cond` is an optional expression that should evaluate to `true` if the error matches.
410///   Variables from `$expected` are bound in `$cond` behind references.
411///   Do not use any asserts in `$cond` as it may be called against multiple errors.
412///
413/// ```
414/// use spacetimedb_data_structures::error_stream::{
415///     ErrorStream,
416///     CollectAllErrors,
417///     expect_error_matching
418/// };
419///
420/// #[derive(PartialEq, Eq, Clone, Copy, Debug)]
421/// struct CaseRef(u32);
422///
423/// #[derive(Debug)]
424/// enum MyError {
425///     InsufficientSwag { amount: u32 },
426///     TooMuchSwag { reason: String, precedent: CaseRef },
427///     SomethingElse(String)
428/// }
429///
430/// let result: Result<(), ErrorStream<MyError>> = vec![
431///     Err(MyError::TooMuchSwag {
432///         reason: "sunglasses indoors".into(),
433///         precedent: CaseRef(37)
434///     }.into()),
435///     Err(MyError::TooMuchSwag {
436///         reason: "fur coat".into(),
437///         precedent: CaseRef(55)
438///     }.into()),
439///     Err(MyError::SomethingElse(
440///         "non-service animals forbidden".into()
441///     ).into())
442/// ].into_iter().collect_all_errors();
443///
444/// // This will panic if the error stream does not contain
445/// // an error matching `MyError::SomethingElse`.
446/// expect_error_matching!(
447///     result,
448///     MyError::SomethingElse(_)
449/// );
450///
451/// // This will panic if the error stream does not contain
452/// // an error matching `MyError::TooMuchSwag`, plus some
453/// // extra conditions.
454/// expect_error_matching!(
455///     result,
456///     MyError::TooMuchSwag { reason, precedent } =>
457///         precedent == &CaseRef(37) && reason.contains("sunglasses")
458/// );
459/// ```
460#[macro_export]
461macro_rules! expect_error_matching (
462    ($result:expr, $expected:pat => $cond:expr) => {
463        let result: &::std::result::Result<
464            _,
465            $crate::error_stream::ErrorStream<_>
466        > = &$result;
467        match result {
468            Ok(_) => panic!("expected error, but got Ok"),
469            Err(errors) => {
470                let err = errors.iter().find(|error|
471                    if let $expected = error {
472                        $cond
473                    } else {
474                        false
475                    }
476                );
477                if let None = err {
478                    panic!("expected error matching `{}` satisfying `{}`,\n but got {:#?}", stringify!($expected), stringify!($cond), errors);
479                }
480            }
481        }
482    };
483    ($result:expr, $expected:pat) => {
484        let result: &::std::result::Result<
485            _,
486            $crate::error_stream::ErrorStream<_>
487        > = &$result;
488        match result {
489            Ok(_) => panic!("expected error, but got Ok"),
490            Err(errors) => {
491                let err = errors.iter().find(|error| matches!(error, $expected));
492                if let None = err {
493                    panic!("expected error matching `{}`,\n but got {:#?}", stringify!($expected), errors);
494                }
495            }
496        }
497    };
498);
499// Make available in this module as well as crate root.
500pub use expect_error_matching;
501use smallvec::SmallVec;
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    #[derive(Debug, PartialEq)]
508    enum MyError {
509        A(u32),
510        B,
511    }
512
513    type Result<T> = std::result::Result<T, ErrorStream<MyError>>;
514
515    #[test]
516    fn combine_errors() {
517        type ResultTuple = (Result<i32>, Result<String>, Result<u8>);
518        let tuple_1: ResultTuple = (Ok(1), Ok("hi".into()), Ok(3));
519        assert_eq!(tuple_1.combine_errors(), Ok((1, "hi".into(), 3)));
520
521        let tuple_2: ResultTuple = (Err(MyError::A(1).into()), Ok("hi".into()), Ok(3));
522        assert_eq!(tuple_2.combine_errors(), Err(MyError::A(1).into()));
523
524        let tuple_3: ResultTuple = (Err(MyError::A(1).into()), Err(MyError::A(2).into()), Ok(3));
525        assert_eq!(
526            tuple_3.combine_errors(),
527            Err(ErrorStream(smallvec::smallvec![MyError::A(1), MyError::A(2)]))
528        );
529
530        let tuple_4: ResultTuple = (
531            Err(MyError::A(1).into()),
532            Err(MyError::A(2).into()),
533            Err(MyError::A(3).into()),
534        );
535        assert_eq!(
536            tuple_4.combine_errors(),
537            Err(ErrorStream(smallvec::smallvec![
538                MyError::A(1),
539                MyError::A(2),
540                MyError::A(3)
541            ]))
542        );
543    }
544
545    #[test]
546    fn collect_all_errors() {
547        let data: Vec<Result<i32>> = vec![Ok(1), Ok(2), Ok(3)];
548        assert_eq!(data.into_iter().collect_all_errors::<Vec<_>>(), Ok(vec![1, 2, 3]));
549
550        let data = vec![Ok(1), Err(MyError::A(0).into()), Ok(3)];
551        assert_eq!(
552            data.into_iter().collect_all_errors::<Vec<_>>(),
553            Err(ErrorStream([MyError::A(0)].into()))
554        );
555
556        let data: Vec<Result<i32>> = vec![
557            Err(MyError::A(1).into()),
558            Err(MyError::A(2).into()),
559            Err(MyError::A(3).into()),
560        ];
561        assert_eq!(
562            data.into_iter().collect_all_errors::<Vec<_>>(),
563            Err(ErrorStream(smallvec::smallvec![
564                MyError::A(1),
565                MyError::A(2),
566                MyError::A(3)
567            ]))
568        );
569    }
570
571    #[test]
572    #[should_panic]
573    fn expect_error_matching_without_cond_panics() {
574        let data: Result<()> = Err(ErrorStream(vec![MyError::B].into()));
575        expect_error_matching!(data, MyError::A(_));
576    }
577
578    #[test]
579    #[should_panic]
580    fn expect_error_matching_with_cond_panics() {
581        let data: Result<()> = Err(ErrorStream(vec![MyError::A(5), MyError::A(10)].into()));
582        expect_error_matching!(data, MyError::A(n) => n == &12);
583    }
584}