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("Expected a Table or string table name")]
101    TableRefError,
102
103    #[error("No `Table` attached")]
104    NoTableError,
105
106    #[error(transparent)]
107    SerdeWasmBindgenError(Rc<serde_wasm_bindgen::Error>),
108
109    #[error(transparent)]
110    Utf8Error(#[from] FromUtf8Error),
111
112    #[error(transparent)]
113    StdIoError(Rc<std::io::Error>),
114
115    #[error(transparent)]
116    ChronoParseError(#[from] chrono::ParseError),
117}
118
119#[derive(Clone, Debug, Error)]
120pub struct ApiError(pub ApiErrorType, pub JsBackTrace);
121
122impl ApiError {
123    pub fn new<T: Display>(val: T) -> Self {
124        apierror!(UnknownError(format!("{val}")))
125    }
126
127    /// The error category
128    pub fn kind(&self) -> &'static str {
129        match self.0 {
130            ApiErrorType::JsError(..) => "[JsError]",
131            ApiErrorType::TableError(_) => "[TableError]",
132            ApiErrorType::ExternalError(_) => "[ExternalError]",
133            ApiErrorType::UnknownError(..) => "[UnknownError]",
134            ApiErrorType::ClientError(_) => "[ClientError]",
135            ApiErrorType::CancelledError(_) => "[CancelledError]",
136            ApiErrorType::SerdeJsonError(_) => "[SerdeJsonError]",
137            ApiErrorType::ProstError(_) => "[ProstError]",
138            ApiErrorType::InvalidViewerConfigError(..) => "[InvalidViewerConfigError]",
139            ApiErrorType::InvalidViewerConfigExpressionsError(_) => "[InvalidViewerConfigError]",
140            ApiErrorType::TableRefError => "[TableRefError]",
141            ApiErrorType::NoTableError => "[NoTableError]",
142            ApiErrorType::SerdeWasmBindgenError(_) => "[SerdeWasmBindgenError]",
143            ApiErrorType::Utf8Error(_) => "[FromUtf8Error]",
144            ApiErrorType::StdIoError(_) => "[StdIoError]",
145            ApiErrorType::ChronoParseError(_) => "[ChronoParseError]",
146            ApiErrorType::ViewerPluginError(_) => "[ViewerPluginError]",
147            ApiErrorType::JsRawError(_) => "[JsRawError]",
148        }
149    }
150
151    /// The raw internal enum
152    pub fn inner(&self) -> &'_ ApiErrorType {
153        &self.0
154    }
155
156    /// The `Display` for this error
157    pub fn message(&self) -> String {
158        self.0.to_string()
159    }
160
161    /// This error's stacktrace from when it was constructed.
162    pub fn stacktrace(&self) -> String {
163        js_sys::Reflect::get(&self.1.0, &intern("stack").into())
164            .unwrap()
165            .as_string()
166            .unwrap()
167            .to_string()
168    }
169}
170
171// This type is not thread safe, but the JavaScript environment does not allow
172// threading.
173unsafe impl Send for ApiError {}
174unsafe impl Sync for ApiError {}
175
176impl std::fmt::Display for ApiError {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        self.0.fmt(f)
179    }
180}
181
182impl<T: Into<ApiErrorType>> From<T> for ApiError {
183    fn from(value: T) -> Self {
184        let value: ApiErrorType = value.into();
185        let err = js_sys::Error::new(value.to_string().as_str());
186        ApiError(value, JsBackTrace(Rc::new(err.clone())))
187    }
188}
189
190impl From<ApiError> for JsValue {
191    fn from(err: ApiError) -> Self {
192        err.1.0.unchecked_ref::<JsValue>().clone()
193    }
194}
195
196impl From<serde_wasm_bindgen::Error> for ApiError {
197    fn from(value: serde_wasm_bindgen::Error) -> Self {
198        ApiErrorType::SerdeWasmBindgenError(Rc::new(value)).into()
199    }
200}
201
202impl From<std::io::Error> for ApiError {
203    fn from(value: std::io::Error) -> Self {
204        ApiErrorType::StdIoError(Rc::new(value)).into()
205    }
206}
207
208impl From<Box<dyn std::error::Error>> for ApiError {
209    fn from(value: Box<dyn std::error::Error>) -> Self {
210        ApiErrorType::ExternalError(Rc::new(value)).into()
211    }
212}
213
214impl From<serde_json::Error> for ApiError {
215    fn from(value: serde_json::Error) -> Self {
216        ApiErrorType::SerdeJsonError(Rc::new(value)).into()
217    }
218}
219
220impl From<JsValue> for ApiError {
221    fn from(err: JsValue) -> Self {
222        if err.is_instance_of::<js_sys::Error>() {
223            ApiError(
224                ApiErrorType::JsRawError(err.clone().unchecked_into()),
225                JsBackTrace(Rc::new(err.unchecked_into())),
226            )
227        } else {
228            apierror!(JsError(err))
229        }
230    }
231}
232
233impl From<String> for ApiError {
234    fn from(value: String) -> Self {
235        apierror!(UnknownError(value.to_owned()))
236    }
237}
238
239impl From<&str> for ApiError {
240    fn from(value: &str) -> Self {
241        apierror!(UnknownError(value.to_owned()))
242    }
243}
244
245/// `ToApiError` handles complex cases that can't be into-d
246pub trait ToApiError<T> {
247    fn into_apierror(self) -> ApiResult<T>;
248}
249
250impl<T> ToApiError<T> for Option<T> {
251    fn into_apierror(self) -> ApiResult<T> {
252        self.ok_or_else(|| intern("Unwrap on None").into())
253    }
254}
255
256impl ToApiError<JsValue> for Result<(), ApiResult<JsValue>> {
257    fn into_apierror(self) -> ApiResult<JsValue> {
258        self.map_or_else(|x| x, |()| Ok(JsValue::UNDEFINED))
259    }
260}
261
262/// A common Rust error handling idiom (see e.g. `anyhow::Result`)
263pub type ApiResult<T> = Result<T, ApiError>;
264
265// Backtrace
266
267#[derive(Clone, Debug)]
268pub struct JsBackTrace(pub Rc<js_sys::Error>);
269
270impl std::fmt::Display for JsBackTrace {
271    fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        Ok(())
273    }
274}