Skip to main content

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