From 8bbe9c665c0cacf05eb6d8a2df407682a976b239 Mon Sep 17 00:00:00 2001 From: Mathieu Amiot Date: Thu, 8 Dec 2022 15:56:07 +0100 Subject: [PATCH] feat: Improved error handling by using `thiserror` --- Cargo.lock | 21 +++++ Cargo.toml | 15 ++-- src/error.rs | 240 ++++++++++++++------------------------------------- 3 files changed, 94 insertions(+), 182 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1577470..6aa6e2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,7 @@ dependencies = [ "serde", "serde_json", "serial_test", + "thiserror", "time", "tokio", "url", @@ -819,6 +820,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.14" diff --git a/Cargo.toml b/Cargo.toml index f5a75e7..73e6334 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,20 +24,21 @@ rustls-tls = ["hyper-rustls"] [dependencies] webdriver = { version = "0.46", default-features = false } -url = "2.2.2" -serde = { version = "1.0.103", features = ["derive"] } -serde_json = "1.0.25" +url = "2.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" futures-core = "0.3" futures-util = "0.3" tokio = { version = "1", features = ["sync", "rt", "time"] } hyper = { version = "0.14", features = ["stream", "client", "http1"] } -cookie = { version = "0.16.0", features = ["percent-encode"] } +cookie = { version = "0.16", features = ["percent-encode"] } base64 = "0.13" -hyper-rustls = { version = "0.23.0", optional = true } -hyper-tls = { version = "0.5.0", optional = true } -mime = "0.3.9" +hyper-rustls = { version = "0.23", optional = true } +hyper-tls = { version = "0.5", optional = true } +mime = "0.3" http = "0.2" time = "0.3" +thiserror = "1.0.37" [dev-dependencies] tokio = { version = "1", features = ["full"] } diff --git a/src/error.rs b/src/error.rs index 894e5ff..056a816 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,83 +9,61 @@ use std::str::FromStr; use url::ParseError; /// An error occurred while attempting to establish a session for a new `Client`. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum NewSessionError { /// The given WebDriver URL is invalid. - BadWebdriverUrl(ParseError), + #[error("webdriver url is invalid: {0}")] + BadWebdriverUrl(#[from] ParseError), /// The WebDriver server could not be reached. - Failed(HError), + #[error("webdriver server did not respond: {0}")] + Failed(#[from] HError), /// The connection to the WebDriver server was lost. - Lost(IOError), + #[error("webdriver server disconnected: {0}")] + Lost(#[from] IOError), /// The server did not give a WebDriver-conforming response. + #[error("webdriver server gave non-conformant response")] NotW3C(serde_json::Value), /// The WebDriver server refused to create a new session. - SessionNotCreated(WebDriver), -} - -impl Error for NewSessionError { - fn description(&self) -> &str { - match *self { - NewSessionError::BadWebdriverUrl(..) => "webdriver url is invalid", - NewSessionError::Failed(..) => "webdriver server did not respond", - NewSessionError::Lost(..) => "webdriver server disconnected", - NewSessionError::NotW3C(..) => "webdriver server gave non-conformant response", - NewSessionError::SessionNotCreated(..) => "webdriver did not create session", - } - } - - fn cause(&self) -> Option<&dyn Error> { - match *self { - NewSessionError::BadWebdriverUrl(ref e) => Some(e), - NewSessionError::Failed(ref e) => Some(e), - NewSessionError::Lost(ref e) => Some(e), - NewSessionError::NotW3C(..) => None, - NewSessionError::SessionNotCreated(ref e) => Some(e), - } - } -} - -impl fmt::Display for NewSessionError { - #[allow(deprecated)] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: ", self.description())?; - match *self { - NewSessionError::BadWebdriverUrl(ref e) => write!(f, "{}", e), - NewSessionError::Failed(ref e) => write!(f, "{}", e), - NewSessionError::Lost(ref e) => write!(f, "{}", e), - NewSessionError::NotW3C(ref e) => write!(f, "{:?}", e), - NewSessionError::SessionNotCreated(ref e) => write!(f, "{}", e), - } - } + #[error("webdriver did not create session: {0}")] + SessionNotCreated(#[from] WebDriver), + /// Unexpected and unrecoverable error when creating a session. + #[error("unexpected webdriver error; {0}")] + UnexpectedError(#[from] CmdError), } /// An error occurred while executing some browser action. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum CmdError { /// A standard WebDriver error occurred. /// /// See [the spec] for details about what each of these errors represent. /// /// [the spec]: https://www.w3.org/TR/webdriver/#handling-errors - Standard(WebDriver), + #[error("webdriver returned error: {0}")] + Standard(#[from] WebDriver), /// A bad URL was encountered during parsing. /// /// This normally happens if a link is clicked or the current URL is requested, but the URL in /// question is invalid or otherwise malformed. - BadUrl(ParseError), + #[error("bad url provided: {0}")] + BadUrl(#[from] ParseError), /// A request to the WebDriver server failed. - Failed(HError), + #[error("webdriver could not be reached: {0}")] + Failed(#[from] HError), /// The connection to the WebDriver server was lost. - Lost(IOError), + #[error("webdriver connection lost: {0}")] + Lost(#[from] IOError), /// The WebDriver server responded with a non-standard, non-JSON reply. + #[error("webdriver returned invalid response: {0}")] NotJson(String), /// The WebDriver server responded to a command with an invalid JSON response. - Json(serde_json::Error), + #[error("webdriver returned incoherent response: {0}")] + Json(#[from] serde_json::Error), /// The WebDriver server produced a response that does not conform to the [W3C WebDriver /// specification][spec]. @@ -96,19 +74,23 @@ pub enum CmdError { /// and does not correctly encode and decode `WebElement` references. /// /// [spec]: https://www.w3.org/TR/webdriver/ + #[error("webdriver returned non-conforming response: {0}")] NotW3C(serde_json::Value), /// A function was invoked with an invalid argument. + #[error("Invalid argument `{0}`: {1}")] InvalidArgument(String, String), /// Could not decode a base64 image - ImageDecodeError(base64::DecodeError), + #[error("error decoding image: {0}")] + ImageDecodeError(#[from] base64::DecodeError), /// Timeout of a wait condition. /// /// When waiting for a for a condition using [`Client::wait`](crate::Client::wait), any of the /// consuming methods, waiting on some condition, may return this error, indicating that the /// timeout waiting for the condition occurred. + #[error("timeout waiting on condition")] WaitTimeout, } @@ -163,99 +145,15 @@ impl CmdError { } } -impl Error for CmdError { - fn description(&self) -> &str { - match *self { - CmdError::Standard(..) => "webdriver returned error", - CmdError::BadUrl(..) => "bad url provided", - CmdError::Failed(..) => "webdriver could not be reached", - CmdError::Lost(..) => "webdriver connection lost", - CmdError::NotJson(..) => "webdriver returned invalid response", - CmdError::Json(..) => "webdriver returned incoherent response", - CmdError::NotW3C(..) => "webdriver returned non-conforming response", - CmdError::InvalidArgument(..) => "invalid argument provided", - CmdError::ImageDecodeError(..) => "error decoding image", - CmdError::WaitTimeout => "timeout waiting on condition", - } - } - - fn cause(&self) -> Option<&dyn Error> { - match *self { - CmdError::Standard(ref e) => Some(e), - CmdError::BadUrl(ref e) => Some(e), - CmdError::Failed(ref e) => Some(e), - CmdError::Lost(ref e) => Some(e), - CmdError::Json(ref e) => Some(e), - CmdError::ImageDecodeError(ref e) => Some(e), - CmdError::NotJson(_) - | CmdError::NotW3C(_) - | CmdError::InvalidArgument(..) - | CmdError::WaitTimeout => None, - } - } -} - -impl fmt::Display for CmdError { - #[allow(deprecated)] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: ", self.description())?; - match *self { - CmdError::Standard(ref e) => write!(f, "{}", e), - CmdError::BadUrl(ref e) => write!(f, "{}", e), - CmdError::Failed(ref e) => write!(f, "{}", e), - CmdError::Lost(ref e) => write!(f, "{}", e), - CmdError::NotJson(ref e) => write!(f, "{}", e), - CmdError::Json(ref e) => write!(f, "{}", e), - CmdError::NotW3C(ref e) => write!(f, "{:?}", e), - CmdError::ImageDecodeError(ref e) => write!(f, "{:?}", e), - CmdError::InvalidArgument(ref arg, ref msg) => { - write!(f, "Invalid argument `{}`: {}", arg, msg) - } - CmdError::WaitTimeout => Ok(()), - } - } -} - -impl From for CmdError { - fn from(e: IOError) -> Self { - CmdError::Lost(e) - } -} - -impl From for CmdError { - fn from(e: ParseError) -> Self { - CmdError::BadUrl(e) - } -} - -impl From for CmdError { - fn from(e: HError) -> Self { - CmdError::Failed(e) - } -} - -impl From for CmdError { - fn from(e: serde_json::Error) -> Self { - CmdError::Json(e) - } -} - /// Error of attempting to create an invalid [`WindowHandle`] from a /// [`"current"` string][1]. /// /// [`WindowHandle`]: crate::wd::WindowHandle /// [1]: https://www.w3.org/TR/webdriver/#dfn-window-handles -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, thiserror::Error)] +#[error("Window handle cannot be \"current\"")] pub struct InvalidWindowHandle; -impl fmt::Display for InvalidWindowHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, r#"Window handle cannot be "current""#) - } -} - -impl Error for InvalidWindowHandle {} - impl From for CmdError { fn from(_: InvalidWindowHandle) -> Self { Self::NotW3C(serde_json::Value::String("current".to_string())) @@ -412,39 +310,38 @@ pub enum ErrorStatus { impl ErrorStatus { /// Returns the correct HTTP status code associated with the error type. pub fn http_status(&self) -> StatusCode { - use self::ErrorStatus::*; match *self { - DetachedShadowRoot => StatusCode::NOT_FOUND, - ElementClickIntercepted => StatusCode::BAD_REQUEST, - ElementNotInteractable => StatusCode::BAD_REQUEST, - ElementNotSelectable => StatusCode::BAD_REQUEST, - InsecureCertificate => StatusCode::BAD_REQUEST, - InvalidArgument => StatusCode::BAD_REQUEST, - InvalidCookieDomain => StatusCode::BAD_REQUEST, - InvalidCoordinates => StatusCode::BAD_REQUEST, - InvalidElementState => StatusCode::BAD_REQUEST, - InvalidSelector => StatusCode::BAD_REQUEST, - InvalidSessionId => StatusCode::NOT_FOUND, - JavascriptError => StatusCode::INTERNAL_SERVER_ERROR, - MoveTargetOutOfBounds => StatusCode::INTERNAL_SERVER_ERROR, - NoSuchAlert => StatusCode::NOT_FOUND, - NoSuchCookie => StatusCode::NOT_FOUND, - NoSuchElement => StatusCode::NOT_FOUND, - NoSuchFrame => StatusCode::NOT_FOUND, - NoSuchShadowRoot => StatusCode::NOT_FOUND, - NoSuchWindow => StatusCode::NOT_FOUND, - ScriptTimeout => StatusCode::INTERNAL_SERVER_ERROR, - SessionNotCreated => StatusCode::INTERNAL_SERVER_ERROR, - StaleElementReference => StatusCode::NOT_FOUND, - Timeout => StatusCode::INTERNAL_SERVER_ERROR, - UnableToCaptureScreen => StatusCode::BAD_REQUEST, - UnableToSetCookie => StatusCode::INTERNAL_SERVER_ERROR, - UnexpectedAlertOpen => StatusCode::INTERNAL_SERVER_ERROR, - UnknownCommand => StatusCode::NOT_FOUND, - UnknownError => StatusCode::INTERNAL_SERVER_ERROR, - UnknownMethod => StatusCode::METHOD_NOT_ALLOWED, - UnknownPath => StatusCode::NOT_FOUND, - UnsupportedOperation => StatusCode::INTERNAL_SERVER_ERROR, + Self::ElementClickIntercepted + | Self::ElementNotInteractable + | Self::ElementNotSelectable + | Self::InsecureCertificate + | Self::InvalidArgument + | Self::InvalidCookieDomain + | Self::InvalidCoordinates + | Self::InvalidElementState + | Self::UnableToCaptureScreen + | Self::InvalidSelector => StatusCode::BAD_REQUEST, + Self::DetachedShadowRoot + | Self::InvalidSessionId + | Self::NoSuchAlert + | Self::NoSuchCookie + | Self::NoSuchElement + | Self::NoSuchFrame + | Self::NoSuchShadowRoot + | Self::NoSuchWindow + | Self::UnknownPath + | Self::UnknownCommand + | Self::StaleElementReference => StatusCode::NOT_FOUND, + Self::JavascriptError + | Self::MoveTargetOutOfBounds + | Self::ScriptTimeout + | Self::SessionNotCreated + | Self::Timeout + | Self::UnableToSetCookie + | Self::UnexpectedAlertOpen + | Self::UnknownError + | Self::UnsupportedOperation => StatusCode::INTERNAL_SERVER_ERROR, + Self::UnknownMethod => StatusCode::METHOD_NOT_ALLOWED, } } } @@ -557,7 +454,8 @@ impl TryFrom for ErrorStatus { } /// Error returned by WebDriver. -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, thiserror::Error)] +#[error("{message}")] pub struct WebDriver { /// Code of this error provided by WebDriver. pub error: ErrorStatus, @@ -574,14 +472,6 @@ pub struct WebDriver { pub data: Option, } -impl fmt::Display for WebDriver { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.message) - } -} - -impl Error for WebDriver {} - impl WebDriver { /// Create a new WebDriver error struct. pub fn new(error: ErrorStatus, message: impl Into>) -> Self {