summaryrefslogtreecommitdiff
path: root/src/error/failed.rs
blob: 4d55552a6bc769d5ffdf317451ea08d8aaf85cb2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
use std::{borrow::Cow, error::Error, fmt};

use super::BoxedError;

// Don't bother allocating a String for each error if we have a static error message, which is
// a fairly common use case.
pub type Message = Cow<'static, str>;

// Intended to represent a generic failure caused by some underlying cause, tagged with a
// context-appropriate message to help operators and developers diagnose and repair the issue.
// Unlike `Internal`, this error type has no default HTTP representation - it's not meant for that
// use case: the intended audience for a `Failed` and its underlying cause is the service operator,
// not the conversational users or client developers.
//
// The underlying cause will be used as the `Failed` error's `source()`, and the provided message
// will be used as its `Display` implementation.
//
// Ways to make a `Failed`:
//
// * Call `ResultExt::fail` or `ResultExt::fail_with` on a result to convert its error type (most
//   preferred).
// * Call `ErrorExt::fail` to convert an existing error into `Failed`.
// * Call `Result::map_err` with `Failed::with_message` or `Failed::with_message_from` to convert
//   its error type to `Failed`.
// * Call `Failed::new` with a message and an existing error (least preferred).
//
// Ways to use `Failed`:
//
// * As the error type of a `Result` return value, when all errors that can arise in a function
//   are of interest to the operator only but there are multiple failure types to contend with.
// * As the error type of a `Result` return value, when there's only one error that can arise but
//   you want to annotate that error with a contextual message, like what the function was doing at
//   the time.
// * As a variant in am error enum, when _some_ errors are only of operator interest but other
//   errors may need to be broken out for matching, or returned to clients.
//
// When not to use `Failed`:
//
// * When the caller needs to match on the specific error type. `Failed`, by design, assumes that
//   nothing up the call stack will care about the structure of the error, and while you _can_ get
//   at the underlying error to check what you ahve by going through the `source()` chain, it's
//   tricky to do well. If the specific error is relevant to the caller, surface it through the
//   error type, instead.
#[derive(Debug)]
pub struct Failed {
    source: BoxedError,
    message: Message,
}

impl Failed {
    pub fn new<M, E>(message: M, source: E) -> Self
    where
        M: Into<Message>,
        E: Into<BoxedError>,
    {
        Self {
            source: source.into(),
            message: message.into(),
        }
    }
}

// Adapters for use with `Result::map_err`.
impl Failed {
    // The returned adaptor wraps an error with `Failed`, using the provided message.
    pub fn with_message<M, E>(message: M) -> impl FnOnce(E) -> Self
    where
        M: Into<Message>,
        E: Into<BoxedError>,
    {
        |source| Self::new(message, source)
    }

    // The returned adaptor wraps an error with `Failed`, using a message from the provided message
    // callback.
    pub fn with_message_from<F, M, E>(message_fn: F) -> impl FnOnce(E) -> Self
    where
        F: FnOnce() -> M,
        M: Into<Message>,
        E: Into<BoxedError>,
    {
        |source| Self::new(message_fn(), source)
    }
}

impl Error for Failed {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(self.source.as_ref())
    }
}

impl fmt::Display for Failed {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.message.fmt(f)
    }
}

pub trait ErrorExt {
    // Convenience for `Failed::new(message, err).into()`, when wrapping an existing error into
    // `Failed`.
    fn fail<M, T>(self, message: M) -> T
    where
        M: Into<Message>,
        T: From<Failed>;
}

impl<E> ErrorExt for E
where
    E: Into<BoxedError>,
{
    fn fail<M, T>(self, message: M) -> T
    where
        M: Into<Message>,
        T: From<Failed>,
    {
        Failed::new(message, self).into()
    }
}

pub trait ResultExt {
    type Ok;

    // Convenience for `.map_err(Failed::with_message(message))`, when a result's error type can be
    // collapsed into `Failed`.
    fn fail<M>(self, message: M) -> Result<Self::Ok, Failed>
    where
        M: Into<Message>;

    fn fail_with<F, M>(self, message_fn: F) -> Result<Self::Ok, Failed>
    where
        F: FnOnce() -> M,
        M: Into<Message>;
}

impl<T, E> ResultExt for Result<T, E>
where
    E: Into<BoxedError>,
{
    type Ok = T;

    fn fail<M>(self, message: M) -> Result<T, Failed>
    where
        M: Into<Message>,
    {
        self.map_err(Failed::with_message(message))
    }

    fn fail_with<F, M>(self, message_fn: F) -> Result<T, Failed>
    where
        F: FnOnce() -> M,
        M: Into<Message>,
    {
        self.map_err(Failed::with_message_from(message_fn))
    }
}