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("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 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 pub fn inner(&self) -> &'_ ApiErrorType {
153 &self.0
154 }
155
156 pub fn message(&self) -> String {
158 self.0.to_string()
159 }
160
161 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
171unsafe 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
245pub 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
262pub type ApiResult<T> = Result<T, ApiError>;
264
265#[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}