perspective_js/utils/
errors.rs1use 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#[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 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 pub fn inner(&self) -> &'_ ApiErrorType {
149 &self.0
150 }
151
152 pub fn message(&self) -> String {
154 self.0.to_string()
155 }
156
157 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
167unsafe 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
241pub 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
258pub type ApiResult<T> = Result<T, ApiError>;
260
261#[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}