ffx_command_error/
context.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::Error;
6use anyhow::Context;
7use errors::{FfxError, IntoExitCode, ResultExt};
8use std::fmt::Display;
9
10/// Adds helpers to result types to produce useful error messages to the user from
11/// the ffx frontend (through [`crate::Error`])
12pub trait FfxContext<T, E> {
13    /// Make this error into a BUG check that will display to the user as an error that
14    /// shouldn't happen.
15    fn bug(self) -> Result<T, Error>;
16
17    /// Make this error into a BUG check that will display to the user as an error that
18    /// shouldn't happen, with the added context.
19    fn bug_context<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error>;
20
21    /// Make this error into a BUG check that will display to the user as an error that
22    /// shouldn't happen, with the added context returned by the closure `f`.
23    fn with_bug_context<C: Display + Send + Sync + 'static>(
24        self,
25        f: impl FnOnce() -> C,
26    ) -> Result<T, Error>;
27
28    /// Make this error into a displayed user error, with the added context for display to the user.
29    /// Use this for errors that happen in the normal course of execution, like files not being found.
30    fn user_message<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error>;
31
32    /// Make this error into a displayed user error, with the added context for display to the user.
33    /// Use this for errors that happen in the normal course of execution, like files not being found.
34    fn with_user_message<C: Display + Send + Sync + 'static>(
35        self,
36        f: impl FnOnce() -> C,
37    ) -> Result<T, Error>;
38}
39
40impl<T, E> FfxContext<T, E> for Result<T, E>
41where
42    Self: anyhow::Context<T, E>,
43    E: Into<anyhow::Error>,
44{
45    fn bug(self) -> Result<T, Error> {
46        self.map_err(|e| Error::Unexpected(e.into()))
47    }
48
49    fn bug_context<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
50        self.context(context).map_err(Error::Unexpected)
51    }
52
53    fn with_bug_context<C: Display + Send + Sync + 'static>(
54        self,
55        f: impl FnOnce() -> C,
56    ) -> Result<T, Error> {
57        self.with_context(f).map_err(Error::Unexpected)
58    }
59
60    fn user_message<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
61        self.context(context).map_err(Error::User)
62    }
63
64    fn with_user_message<C: Display + Send + Sync + 'static>(
65        self,
66        f: impl FnOnce() -> C,
67    ) -> Result<T, Error> {
68        self.with_context(f).map_err(Error::User)
69    }
70}
71
72impl<T> FfxContext<T, core::convert::Infallible> for Option<T>
73where
74    Self: anyhow::Context<T, core::convert::Infallible>,
75{
76    fn bug(self) -> Result<T, Error> {
77        self.ok_or_else(|| Error::Unexpected(anyhow::anyhow!("Option is None")))
78    }
79
80    fn bug_context<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
81        self.context(context).map_err(Error::Unexpected)
82    }
83
84    fn with_bug_context<C: Display + Send + Sync + 'static>(
85        self,
86        f: impl FnOnce() -> C,
87    ) -> Result<T, Error> {
88        self.with_context(f).map_err(Error::Unexpected)
89    }
90
91    fn user_message<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
92        self.context(context).map_err(Error::User)
93    }
94
95    fn with_user_message<C: Display + Send + Sync + 'static>(
96        self,
97        f: impl FnOnce() -> C,
98    ) -> Result<T, Error> {
99        self.with_context(f).map_err(Error::User)
100    }
101}
102
103impl ResultExt for Error {
104    fn ffx_error<'a>(&'a self) -> Option<&'a FfxError> {
105        match self {
106            Error::User(err) => err.downcast_ref(),
107            _ => None,
108        }
109    }
110}
111impl IntoExitCode for Error {
112    fn exit_code(&self) -> i32 {
113        use Error::*;
114        match self {
115            Help { code, .. } | ExitWithCode(code) => *code,
116            Unexpected(err) | User(err) | Config(err) => {
117                err.ffx_error().map(FfxError::exit_code).unwrap_or(1)
118            }
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::tests::*;
127    use anyhow::anyhow;
128    use assert_matches::assert_matches;
129
130    #[test]
131    fn error_context_helpers() {
132        assert_matches!(
133            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).bug(),
134            Err(Error::Unexpected(_)),
135            "anyhow.bug() should be a bugcheck error"
136        );
137        assert_matches!(
138            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).bug_context("boom"),
139            Err(Error::Unexpected(_)),
140            "anyhow.bug_context() should be a bugcheck error"
141        );
142        assert_matches!(
143            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).with_bug_context(|| "boom"),
144            Err(Error::Unexpected(_)),
145            "anyhow.bug_context() should be a bugcheck error"
146        );
147        assert_matches!(anyhow::Result::<()>::Err(anyhow!(ERR_STR)).bug_context(FfxError::TestingError), Err(Error::Unexpected(_)), "anyhow.bug_context() should create a bugcheck error even if given an ffx error (magic reduction)");
148        assert_matches!(
149            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).user_message("boom"),
150            Err(Error::User(_)),
151            "anyhow.user_message() should be a user error"
152        );
153        assert_matches!(
154            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).with_user_message(|| "boom"),
155            Err(Error::User(_)),
156            "anyhow.with_user_message() should be a user error"
157        );
158        assert_matches!(anyhow::Result::<()>::Err(anyhow!(ERR_STR)).with_user_message(|| FfxError::TestingError).ffx_error(), Some(FfxError::TestingError), "anyhow.with_user_message should be a user error that properly extracts to the ffx error.");
159    }
160}