perspective_js/utils/
errors.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::fmt::Display;
14use std::rc::Rc;
15use std::string::FromUtf8Error;
16
17use perspective_client::{ClientError, ValidateExpressionsData};
18use thiserror::*;
19use wasm_bindgen::prelude::*;
20
21#[macro_export]
22macro_rules! apierror {
23    ($msg:expr) => {{
24        use $crate::utils::errors::ApiErrorType::*;
25        let js_err_type = $msg;
26        let err = js_sys::Error::new(js_err_type.to_string().as_str());
27        let js_err = $crate::utils::errors::ApiError(
28            js_err_type,
29            $crate::utils::errors::JsBackTrace(std::rc::Rc::new(err.clone())),
30        );
31        js_err
32    }};
33}
34
35fn format_js_error(value: &JsValue) -> String {
36    if let Some(err) = value.dyn_ref::<js_sys::Error>() {
37        let msg = err.message().as_string().unwrap();
38        if let Ok(x) = js_sys::Reflect::get(value, &"stack".into()) {
39            format!("{}\n{}", msg, x.as_string().unwrap())
40        } else {
41            msg
42        }
43    } else {
44        value
45            .as_string()
46            .unwrap_or_else(|| format!("{:?}", value))
47            .to_string()
48    }
49}
50
51fn format_valid_exprs(recs: &ValidateExpressionsData) -> String {
52    recs.errors
53        .iter()
54        .map(|x| format!("\"{}\": {}", x.0, x.1.error_message))
55        .collect::<Vec<_>>()
56        .join(", ")
57}
58
59/// A bespoke error class for chaining a litany of error types with the `?`
60/// operator.  
61#[derive(Clone, Debug, Error)]
62pub enum ApiErrorType {
63    #[error("{}", format_js_error(.0))]
64    JsError(JsValue),
65
66    #[error("{}", format_js_error(.0))]
67    JsRawError(js_sys::Error),
68
69    #[error("Failed to construct table from {0:?}")]
70    TableError(JsValue),
71
72    #[error("{}", format_js_error(.0))]
73    ViewerPluginError(JsValue),
74
75    #[error("{0}")]
76    ExternalError(Rc<Box<dyn std::error::Error>>),
77
78    #[error("{0}")]
79    UnknownError(String),
80
81    #[error("{0}")]
82    ClientError(#[from] ClientError),
83
84    #[error("Cancelled")]
85    CancelledError(#[from] futures::channel::oneshot::Canceled),
86
87    #[error("{0}")]
88    SerdeJsonError(Rc<serde_json::Error>),
89
90    #[error("{0}")]
91    ProstError(#[from] prost::DecodeError),
92
93    #[error("Unknown column \"{1}\" in field `{0}`")]
94    InvalidViewerConfigError(&'static str, String),
95
96    #[error("Invalid `expressions` {}", format_valid_exprs(.0))]
97    InvalidViewerConfigExpressionsError(Rc<ValidateExpressionsData>),
98
99    #[error("No `Table` attached")]
100    NoTableError,
101
102    #[error(transparent)]
103    SerdeWasmBindgenError(Rc<serde_wasm_bindgen::Error>),
104
105    #[error(transparent)]
106    Utf8Error(#[from] FromUtf8Error),
107
108    #[error(transparent)]
109    StdIoError(Rc<std::io::Error>),
110
111    #[error(transparent)]
112    RmpSerdeEncodeError(Rc<rmp_serde::encode::Error>),
113
114    #[error(transparent)]
115    RmpSerdeDecodeError(Rc<rmp_serde::decode::Error>),
116
117    #[error(transparent)]
118    Base64DecodeError(#[from] base64::DecodeError),
119
120    #[error(transparent)]
121    ChronoParseError(#[from] chrono::ParseError),
122}
123
124#[derive(Clone, Debug, Error)]
125pub struct ApiError(pub ApiErrorType, pub JsBackTrace);
126
127impl ApiError {
128    pub fn new<T: Display>(val: T) -> Self {
129        apierror!(UnknownError(format!("{}", val)))
130    }
131
132    /// The error category
133    pub fn kind(&self) -> &'static str {
134        match self.0 {
135            ApiErrorType::JsError(..) => "[JsError]",
136            ApiErrorType::TableError(_) => "[TableError]",
137            ApiErrorType::ExternalError(_) => "[ExternalError]",
138            ApiErrorType::UnknownError(..) => "[UnknownError]",
139            ApiErrorType::ClientError(_) => "[ClientError]",
140            ApiErrorType::CancelledError(_) => "[CancelledError]",
141            ApiErrorType::SerdeJsonError(_) => "[SerdeJsonError]",
142            ApiErrorType::ProstError(_) => "[ProstError]",
143            ApiErrorType::InvalidViewerConfigError(..) => "[InvalidViewerConfigError]",
144            ApiErrorType::InvalidViewerConfigExpressionsError(_) => "[InvalidViewerConfigError]",
145            ApiErrorType::NoTableError => "[NoTableError]",
146            ApiErrorType::SerdeWasmBindgenError(_) => "[SerdeWasmBindgenError]",
147            ApiErrorType::Utf8Error(_) => "[FromUtf8Error]",
148            ApiErrorType::StdIoError(_) => "[StdIoError]",
149            ApiErrorType::RmpSerdeEncodeError(_) => "[RmpSerdeEncodeError]",
150            ApiErrorType::RmpSerdeDecodeError(_) => "[RmpSerdeDecodeError]",
151            ApiErrorType::Base64DecodeError(_) => "[Base64DecodeError]",
152            ApiErrorType::ChronoParseError(_) => "[ChronoParseError]",
153            ApiErrorType::ViewerPluginError(_) => "[ViewerPluginError]",
154            ApiErrorType::JsRawError(_) => "[JsRawError]",
155        }
156    }
157
158    /// The raw internal enum
159    pub fn inner(&self) -> &'_ ApiErrorType {
160        &self.0
161    }
162
163    /// The `Display` for this error
164    pub fn message(&self) -> String {
165        self.0.to_string()
166    }
167
168    /// This error's stacktrace from when it was constructed.
169    pub fn stacktrace(&self) -> String {
170        js_sys::Reflect::get(&self.1.0, &"stack".into())
171            .unwrap()
172            .as_string()
173            .unwrap()
174            .to_string()
175    }
176}
177
178unsafe impl Send for ApiError {}
179unsafe impl Sync for ApiError {}
180
181impl std::fmt::Display for ApiError {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        self.0.fmt(f)
184    }
185}
186
187impl<T: Into<ApiErrorType>> From<T> for ApiError {
188    fn from(value: T) -> Self {
189        let value: ApiErrorType = value.into();
190        let err = js_sys::Error::new(value.to_string().as_str());
191        ApiError(value, JsBackTrace(Rc::new(err.clone())))
192    }
193}
194
195impl From<ApiError> for JsValue {
196    fn from(err: ApiError) -> Self {
197        err.1.0.unchecked_ref::<JsValue>().clone()
198    }
199}
200
201impl From<serde_wasm_bindgen::Error> for ApiError {
202    fn from(value: serde_wasm_bindgen::Error) -> Self {
203        ApiErrorType::SerdeWasmBindgenError(Rc::new(value)).into()
204    }
205}
206
207impl From<std::io::Error> for ApiError {
208    fn from(value: std::io::Error) -> Self {
209        ApiErrorType::StdIoError(Rc::new(value)).into()
210    }
211}
212
213impl From<rmp_serde::decode::Error> for ApiError {
214    fn from(value: rmp_serde::decode::Error) -> Self {
215        ApiErrorType::RmpSerdeDecodeError(Rc::new(value)).into()
216    }
217}
218
219impl From<rmp_serde::encode::Error> for ApiError {
220    fn from(value: rmp_serde::encode::Error) -> Self {
221        ApiErrorType::RmpSerdeEncodeError(Rc::new(value)).into()
222    }
223}
224
225impl From<Box<dyn std::error::Error>> for ApiError {
226    fn from(value: Box<dyn std::error::Error>) -> Self {
227        ApiErrorType::ExternalError(Rc::new(value)).into()
228    }
229}
230
231impl From<serde_json::Error> for ApiError {
232    fn from(value: serde_json::Error) -> Self {
233        ApiErrorType::SerdeJsonError(Rc::new(value)).into()
234    }
235}
236
237impl From<JsValue> for ApiError {
238    fn from(err: JsValue) -> Self {
239        if err.is_instance_of::<js_sys::Error>() {
240            ApiErrorType::JsRawError(err.clone().unchecked_into()).into()
241        } else {
242            apierror!(JsError(err))
243        }
244    }
245}
246
247impl From<String> for ApiError {
248    fn from(value: String) -> Self {
249        apierror!(UnknownError(value.to_owned()))
250    }
251}
252
253impl From<&str> for ApiError {
254    fn from(value: &str) -> Self {
255        apierror!(UnknownError(value.to_owned()))
256    }
257}
258
259/// `ToApiError` handles complex cases that can't be into-d
260pub trait ToApiError<T> {
261    fn into_apierror(self) -> ApiResult<T>;
262}
263
264impl<T> ToApiError<T> for Option<T> {
265    fn into_apierror(self) -> ApiResult<T> {
266        self.ok_or_else(|| "Unwrap on None".into())
267    }
268}
269
270impl ToApiError<JsValue> for Result<(), ApiResult<JsValue>> {
271    fn into_apierror(self) -> ApiResult<JsValue> {
272        self.map_or_else(|x| x, |()| Ok(JsValue::UNDEFINED))
273    }
274}
275
276/// A common Rust error handling idiom (see e.g. `anyhow::Result`)
277pub type ApiResult<T> = Result<T, ApiError>;
278
279// Backtrace
280
281#[derive(Clone, Debug)]
282pub struct JsBackTrace(pub Rc<js_sys::Error>);
283
284impl std::fmt::Display for JsBackTrace {
285    fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        Ok(())
287    }
288}