@perspective-dev/client 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +193 -0
- package/README.md +3 -0
- package/dist/cdn/perspective-server.worker.js +2 -0
- package/dist/cdn/perspective-server.worker.js.map +7 -0
- package/dist/cdn/perspective.js +3 -0
- package/dist/cdn/perspective.js.map +7 -0
- package/dist/esm/perspective-server.worker.d.ts +1 -0
- package/dist/esm/perspective.browser.d.ts +14 -0
- package/dist/esm/perspective.inline.js +3 -0
- package/dist/esm/perspective.inline.js.map +7 -0
- package/dist/esm/perspective.js +3 -0
- package/dist/esm/perspective.js.map +7 -0
- package/dist/esm/perspective.node.d.ts +60 -0
- package/dist/esm/perspective.node.js +2431 -0
- package/dist/esm/perspective.node.js.map +7 -0
- package/dist/esm/ts-rs/Aggregate.d.ts +1 -0
- package/dist/esm/ts-rs/ColumnWindow.d.ts +4 -0
- package/dist/esm/ts-rs/DeleteOptions.d.ts +6 -0
- package/dist/esm/ts-rs/Expressions.d.ts +3 -0
- package/dist/esm/ts-rs/Filter.d.ts +2 -0
- package/dist/esm/ts-rs/FilterReducer.d.ts +1 -0
- package/dist/esm/ts-rs/FilterTerm.d.ts +2 -0
- package/dist/esm/ts-rs/OnUpdateMode.d.ts +9 -0
- package/dist/esm/ts-rs/OnUpdateOptions.d.ts +7 -0
- package/dist/esm/ts-rs/Scalar.d.ts +5 -0
- package/dist/esm/ts-rs/Sort.d.ts +2 -0
- package/dist/esm/ts-rs/SortDir.d.ts +1 -0
- package/dist/esm/ts-rs/SystemInfo.d.ts +40 -0
- package/dist/esm/ts-rs/TableInitOptions.d.ts +22 -0
- package/dist/esm/ts-rs/TableReadFormat.d.ts +7 -0
- package/dist/esm/ts-rs/UpdateOptions.d.ts +8 -0
- package/dist/esm/ts-rs/ViewConfigUpdate.d.ts +90 -0
- package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +4 -0
- package/dist/esm/ts-rs/ViewWindow.d.ts +23 -0
- package/dist/esm/wasm/browser.d.ts +21 -0
- package/dist/esm/wasm/decompress.d.ts +1 -0
- package/dist/esm/wasm/emscripten_api.d.ts +5 -0
- package/dist/esm/wasm/engine.d.ts +40 -0
- package/dist/esm/wasm/perspective-server.poly.d.ts +1 -0
- package/dist/esm/websocket.d.ts +4 -0
- package/dist/wasm/perspective-js.d.ts +712 -0
- package/dist/wasm/perspective-js.js +1934 -0
- package/dist/wasm/perspective-js.wasm +0 -0
- package/dist/wasm/perspective-js.wasm.d.ts +75 -0
- package/package.json +68 -0
- package/src/rust/client.rs +483 -0
- package/src/rust/lib.rs +70 -0
- package/src/rust/table.rs +364 -0
- package/src/rust/table_data.rs +159 -0
- package/src/rust/utils/browser.rs +39 -0
- package/src/rust/utils/console_logger.rs +236 -0
- package/src/rust/utils/errors.rs +288 -0
- package/src/rust/utils/futures.rs +174 -0
- package/src/rust/utils/json.rs +252 -0
- package/src/rust/utils/local_poll_loop.rs +63 -0
- package/src/rust/utils/mod.rs +32 -0
- package/src/rust/utils/serde.rs +46 -0
- package/src/rust/utils/trace_allocator.rs +98 -0
- package/src/rust/view.rs +355 -0
- package/src/ts/perspective-server.worker.ts +54 -0
- package/src/ts/perspective.browser.ts +132 -0
- package/src/ts/perspective.cdn.ts +22 -0
- package/src/ts/perspective.inline.ts +27 -0
- package/src/ts/perspective.node.ts +315 -0
- package/src/ts/ts-rs/Aggregate.ts +3 -0
- package/src/ts/ts-rs/ColumnWindow.ts +3 -0
- package/src/ts/ts-rs/DeleteOptions.ts +6 -0
- package/src/ts/ts-rs/Expressions.ts +3 -0
- package/src/ts/ts-rs/Filter.ts +4 -0
- package/src/ts/ts-rs/FilterReducer.ts +3 -0
- package/src/ts/ts-rs/FilterTerm.ts +4 -0
- package/src/ts/ts-rs/OnUpdateData.ts +8 -0
- package/src/ts/ts-rs/OnUpdateMode.ts +11 -0
- package/src/ts/ts-rs/OnUpdateOptions.ts +7 -0
- package/src/ts/ts-rs/Scalar.ts +7 -0
- package/src/ts/ts-rs/Sort.ts +4 -0
- package/src/ts/ts-rs/SortDir.ts +3 -0
- package/src/ts/ts-rs/SystemInfo.ts +41 -0
- package/src/ts/ts-rs/TableInitOptions.ts +21 -0
- package/src/ts/ts-rs/TableReadFormat.ts +9 -0
- package/src/ts/ts-rs/UpdateOptions.ts +7 -0
- package/src/ts/ts-rs/ViewConfigUpdate.ts +87 -0
- package/src/ts/ts-rs/ViewOnUpdateResp.ts +3 -0
- package/src/ts/ts-rs/ViewWindow.ts +17 -0
- package/src/ts/wasm/browser.ts +123 -0
- package/src/ts/wasm/decompress.ts +64 -0
- package/src/ts/wasm/emscripten_api.ts +63 -0
- package/src/ts/wasm/engine.ts +271 -0
- package/src/ts/wasm/perspective-server.poly.ts +244 -0
- package/src/ts/websocket.ts +95 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,236 @@
|
|
|
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
|
+
|
|
13
|
+
use std::fmt::{Debug, Write};
|
|
14
|
+
use std::sync::OnceLock;
|
|
15
|
+
|
|
16
|
+
use tracing::Subscriber;
|
|
17
|
+
use tracing::field::{Field, Visit};
|
|
18
|
+
use tracing_subscriber::Layer;
|
|
19
|
+
use tracing_subscriber::layer::Context;
|
|
20
|
+
use tracing_subscriber::registry::LookupSpan;
|
|
21
|
+
use wasm_bindgen::prelude::*;
|
|
22
|
+
|
|
23
|
+
use crate::utils::*;
|
|
24
|
+
|
|
25
|
+
/// A struct to implement the `Visit` visitor pattern trait to process
|
|
26
|
+
/// `tracing::Event`s.
|
|
27
|
+
#[derive(Default)]
|
|
28
|
+
struct LogLineBuffer {
|
|
29
|
+
value: String,
|
|
30
|
+
is_tail: bool,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl Visit for LogLineBuffer {
|
|
34
|
+
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
|
|
35
|
+
if field.name() == "message" {
|
|
36
|
+
if !self.value.is_empty() {
|
|
37
|
+
self.value = format!("{:?}\n{}", value, self.value)
|
|
38
|
+
} else {
|
|
39
|
+
self.value = format!("{value:?}")
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
if self.is_tail {
|
|
43
|
+
writeln!(self.value).unwrap();
|
|
44
|
+
} else {
|
|
45
|
+
write!(self.value, " ").unwrap();
|
|
46
|
+
self.is_tail = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
write!(self.value, "{} = {:?};", field.name(), value).unwrap();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[extend::ext]
|
|
55
|
+
impl tracing::Level {
|
|
56
|
+
/// Convert a `tracing::Level` to an equivalent 4-arg call to the
|
|
57
|
+
/// browser console via the `web_sys::console` module.
|
|
58
|
+
fn web_logger_4(&self) -> fn(&JsValue, &JsValue, &JsValue, &JsValue) {
|
|
59
|
+
match *self {
|
|
60
|
+
tracing::Level::TRACE => web_sys::console::trace_4,
|
|
61
|
+
tracing::Level::DEBUG => web_sys::console::debug_4,
|
|
62
|
+
tracing::Level::INFO => web_sys::console::info_4,
|
|
63
|
+
tracing::Level::WARN => web_sys::console::warn_4,
|
|
64
|
+
tracing::Level::ERROR => web_sys::console::error_4,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn web_logger_1(&self) -> fn(&JsValue) {
|
|
69
|
+
match *self {
|
|
70
|
+
tracing::Level::TRACE => web_sys::console::trace_1,
|
|
71
|
+
tracing::Level::DEBUG => web_sys::console::debug_1,
|
|
72
|
+
tracing::Level::INFO => web_sys::console::info_1,
|
|
73
|
+
tracing::Level::WARN => web_sys::console::warn_1,
|
|
74
|
+
tracing::Level::ERROR => web_sys::console::error_1,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Return a pretty color theme for a `tracing::Level`.
|
|
79
|
+
fn web_log_color(&self) -> &'static str {
|
|
80
|
+
match *self {
|
|
81
|
+
tracing::Level::TRACE => "background: #005F73; color: #000",
|
|
82
|
+
tracing::Level::DEBUG => "background: #0A9396; color: #000",
|
|
83
|
+
tracing::Level::INFO => "background: #E9D8A6; color: #000",
|
|
84
|
+
tracing::Level::WARN => "background: #EE9B00; color: #000",
|
|
85
|
+
tracing::Level::ERROR => "background: #AE2012; color: #000",
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static IS_CHROME: OnceLock<bool> = OnceLock::new();
|
|
91
|
+
|
|
92
|
+
fn detect_chrome() -> bool {
|
|
93
|
+
if let Some(window) = web_sys::window() {
|
|
94
|
+
window.get("chrome").is_some()
|
|
95
|
+
} else {
|
|
96
|
+
true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[extend::ext]
|
|
101
|
+
impl<'a> tracing::Metadata<'a> {
|
|
102
|
+
/// Log a message in the style of `WasmLogger`.
|
|
103
|
+
fn console_log(&self, msg: &str) {
|
|
104
|
+
let level = self.level();
|
|
105
|
+
let origin = self
|
|
106
|
+
.module_path()
|
|
107
|
+
.and_then(|file| self.line().map(|ln| format!("{}:{}", &file[11..], ln)))
|
|
108
|
+
.unwrap_or_default();
|
|
109
|
+
|
|
110
|
+
if *IS_CHROME.get_or_init(detect_chrome) {
|
|
111
|
+
level.web_logger_4()(
|
|
112
|
+
&format!("%c {level} %c {origin}%c {msg} ").into(),
|
|
113
|
+
&level.web_log_color().into(),
|
|
114
|
+
&"color: gray; font-style: italic".into(),
|
|
115
|
+
&"color: inherit".into(),
|
|
116
|
+
);
|
|
117
|
+
} else {
|
|
118
|
+
level.web_logger_1()(&format!("{origin} {msg}").into());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// A custom logger modelled afer the `tracing_wasm` crate.
|
|
124
|
+
struct WasmLogger {
|
|
125
|
+
max_level: tracing::Level,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl Default for WasmLogger {
|
|
129
|
+
fn default() -> Self {
|
|
130
|
+
Self {
|
|
131
|
+
max_level: tracing::Level::WARN,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for WasmLogger {
|
|
137
|
+
fn on_event(
|
|
138
|
+
&self,
|
|
139
|
+
event: &tracing::Event<'_>,
|
|
140
|
+
_ctx: tracing_subscriber::layer::Context<'_, S>,
|
|
141
|
+
) {
|
|
142
|
+
let mut recorder = LogLineBuffer::default();
|
|
143
|
+
event.record(&mut recorder);
|
|
144
|
+
event.metadata().console_log(&recorder.value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn enabled(&self, metadata: &tracing::Metadata<'_>, _: Context<'_, S>) -> bool {
|
|
148
|
+
let level = metadata.level();
|
|
149
|
+
level <= &self.max_level
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn on_new_span(
|
|
153
|
+
&self,
|
|
154
|
+
attrs: &tracing::span::Attributes<'_>,
|
|
155
|
+
id: &tracing::Id,
|
|
156
|
+
ctx: Context<'_, S>,
|
|
157
|
+
) {
|
|
158
|
+
let mut new_debug_record = LogLineBuffer::default();
|
|
159
|
+
attrs.record(&mut new_debug_record);
|
|
160
|
+
if let Some(span_ref) = ctx.span(id) {
|
|
161
|
+
span_ref
|
|
162
|
+
.extensions_mut()
|
|
163
|
+
.insert::<LogLineBuffer>(new_debug_record);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
global::performance()
|
|
167
|
+
.mark(&format!("t{:x}", id.into_u64()))
|
|
168
|
+
.unwrap();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn on_record(&self, id: &tracing::Id, values: &tracing::span::Record<'_>, ctx: Context<'_, S>) {
|
|
172
|
+
if let Some(span_ref) = ctx.span(id) {
|
|
173
|
+
if let Some(debug_record) = span_ref.extensions_mut().get_mut::<LogLineBuffer>() {
|
|
174
|
+
values.record(debug_record);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn on_close(&self, id: tracing::Id, ctx: Context<'_, S>) {
|
|
180
|
+
if let Some(span_ref) = ctx.span(&id) {
|
|
181
|
+
let perf = global::performance();
|
|
182
|
+
let meta = span_ref.metadata();
|
|
183
|
+
let mark = format!("t{:x}", id.into_u64());
|
|
184
|
+
let start = perf
|
|
185
|
+
.get_entries_by_name_with_entry_type(&mark, "mark")
|
|
186
|
+
.at(-1)
|
|
187
|
+
.unchecked_into::<web_sys::PerformanceMark>()
|
|
188
|
+
.start_time();
|
|
189
|
+
|
|
190
|
+
meta.console_log(&format!("{:.0}ms", perf.now() - start));
|
|
191
|
+
let msg = format!(
|
|
192
|
+
"\"{}\" {} {}",
|
|
193
|
+
meta.name(),
|
|
194
|
+
meta.module_path().unwrap_or_default(),
|
|
195
|
+
span_ref
|
|
196
|
+
.extensions()
|
|
197
|
+
.get::<LogLineBuffer>()
|
|
198
|
+
.map(|x| &x.value[..])
|
|
199
|
+
.unwrap_or_default(),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
perf.measure_with_start_mark(&msg, &mark).unwrap();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Configure `WasmLogger` as a global default for tracing.
|
|
208
|
+
///
|
|
209
|
+
/// This operation will conflict with any other library which sets a global
|
|
210
|
+
/// default `tracing::Subscriber`, so it should not be called when `perspective`
|
|
211
|
+
/// is used as a library from a larger app; in this case the app itself should
|
|
212
|
+
/// configure `tracing` explicitly.
|
|
213
|
+
pub fn set_global_logging() {
|
|
214
|
+
static INIT_LOGGING: OnceLock<()> = OnceLock::new();
|
|
215
|
+
INIT_LOGGING.get_or_init(|| {
|
|
216
|
+
if !*IS_CHROME.get_or_init(detect_chrome) {
|
|
217
|
+
web_sys::console::warn_1(
|
|
218
|
+
&"Unknown browser detected. Some features may not work, and performance may be \
|
|
219
|
+
degraded."
|
|
220
|
+
.into(),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
use tracing_subscriber::layer::SubscriberExt;
|
|
225
|
+
let filter = tracing_subscriber::filter::filter_fn(|meta| {
|
|
226
|
+
meta.module_path()
|
|
227
|
+
.as_ref()
|
|
228
|
+
.map(|x| x.starts_with("perspective"))
|
|
229
|
+
.unwrap_or_default()
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
let layer = WasmLogger::default().with_filter(filter);
|
|
233
|
+
let subscriber = tracing_subscriber::Registry::default().with(layer);
|
|
234
|
+
tracing::subscriber::set_global_default(subscriber).unwrap();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
|
|
13
|
+
use std::fmt::Display;
|
|
14
|
+
use std::rc::Rc;
|
|
15
|
+
use std::string::FromUtf8Error;
|
|
16
|
+
|
|
17
|
+
use perspective_client::{ClientError, ExprValidationResult};
|
|
18
|
+
use thiserror::*;
|
|
19
|
+
use wasm_bindgen::prelude::*;
|
|
20
|
+
|
|
21
|
+
#[macro_export]
|
|
22
|
+
macro_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
|
+
|
|
35
|
+
fn 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
|
+
|
|
51
|
+
fn format_valid_exprs(recs: &ExprValidationResult) -> 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)]
|
|
62
|
+
pub 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<ExprValidationResult>),
|
|
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)]
|
|
125
|
+
pub struct ApiError(pub ApiErrorType, pub JsBackTrace);
|
|
126
|
+
|
|
127
|
+
impl 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
|
+
|
|
178
|
+
unsafe impl Send for ApiError {}
|
|
179
|
+
unsafe impl Sync for ApiError {}
|
|
180
|
+
|
|
181
|
+
impl 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
|
+
|
|
187
|
+
impl<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
|
+
|
|
195
|
+
impl From<ApiError> for JsValue {
|
|
196
|
+
fn from(err: ApiError) -> Self {
|
|
197
|
+
err.1.0.unchecked_ref::<JsValue>().clone()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
impl 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
|
+
|
|
207
|
+
impl From<std::io::Error> for ApiError {
|
|
208
|
+
fn from(value: std::io::Error) -> Self {
|
|
209
|
+
ApiErrorType::StdIoError(Rc::new(value)).into()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
impl 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
|
+
|
|
219
|
+
impl 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
|
+
|
|
225
|
+
impl 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
|
+
|
|
231
|
+
impl From<serde_json::Error> for ApiError {
|
|
232
|
+
fn from(value: serde_json::Error) -> Self {
|
|
233
|
+
ApiErrorType::SerdeJsonError(Rc::new(value)).into()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
impl 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
|
+
|
|
247
|
+
impl From<String> for ApiError {
|
|
248
|
+
fn from(value: String) -> Self {
|
|
249
|
+
apierror!(UnknownError(value.to_owned()))
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
impl 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
|
|
260
|
+
pub trait ToApiError<T> {
|
|
261
|
+
fn into_apierror(self) -> ApiResult<T>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
impl<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
|
+
|
|
270
|
+
impl 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`)
|
|
277
|
+
pub type ApiResult<T> = Result<T, ApiError>;
|
|
278
|
+
|
|
279
|
+
// Backtrace
|
|
280
|
+
|
|
281
|
+
#[derive(Clone, Debug)]
|
|
282
|
+
pub struct JsBackTrace(pub Rc<js_sys::Error>);
|
|
283
|
+
|
|
284
|
+
impl std::fmt::Display for JsBackTrace {
|
|
285
|
+
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
286
|
+
Ok(())
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
|
|
13
|
+
use std::future::Future;
|
|
14
|
+
use std::pin::Pin;
|
|
15
|
+
|
|
16
|
+
use perspective_client::ClientError;
|
|
17
|
+
// TODO This is risky to rely on, but it is currently impossible to implement
|
|
18
|
+
// this trait locally due to the orphan instance restriction. Using this trait
|
|
19
|
+
// removes alow of boilerplate required by `async` when casting to `Promise`.
|
|
20
|
+
use wasm_bindgen::__rt::IntoJsResult;
|
|
21
|
+
use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi};
|
|
22
|
+
use wasm_bindgen::describe::WasmDescribe;
|
|
23
|
+
use wasm_bindgen::prelude::*;
|
|
24
|
+
use wasm_bindgen_futures::{JsFuture, future_to_promise};
|
|
25
|
+
|
|
26
|
+
use super::errors::*;
|
|
27
|
+
|
|
28
|
+
/// A newtype wrapper for a `Future` trait object which supports being
|
|
29
|
+
/// marshalled to a `JsPromise`.
|
|
30
|
+
///
|
|
31
|
+
/// This avoids implementing an API which requires type casting to
|
|
32
|
+
/// and from `JsValue` and the associated loss of type safety.
|
|
33
|
+
#[must_use]
|
|
34
|
+
pub struct ApiFuture<T>(Pin<Box<dyn Future<Output = ApiResult<T>>>>)
|
|
35
|
+
where
|
|
36
|
+
Result<T, JsValue>: IntoJsResult + 'static;
|
|
37
|
+
|
|
38
|
+
impl<T> ApiFuture<T>
|
|
39
|
+
where
|
|
40
|
+
Result<T, JsValue>: IntoJsResult + 'static,
|
|
41
|
+
{
|
|
42
|
+
/// Constructor for `ApiFuture`. Note that, like a regular `Future`, the
|
|
43
|
+
/// `ApiFuture` created does _not_ execute without being further cast to a
|
|
44
|
+
/// `Promise`, either explicitly or implcitly (when exposed via
|
|
45
|
+
/// `wasm_bindgen`).
|
|
46
|
+
pub fn new<U: Future<Output = ApiResult<T>> + 'static>(x: U) -> Self {
|
|
47
|
+
Self(Box::pin(x))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl<T> ApiFuture<T>
|
|
52
|
+
where
|
|
53
|
+
Result<T, JsValue>: IntoJsResult + 'static,
|
|
54
|
+
{
|
|
55
|
+
/// Construct an `ApiFuture` and execute it immediately. The `Promise`
|
|
56
|
+
/// handle created internally is dropped, but since JavaScript `Promise`
|
|
57
|
+
/// executes on construction, the async invocation persists.
|
|
58
|
+
pub fn spawn<U: Future<Output = ApiResult<T>> + 'static>(x: U) {
|
|
59
|
+
drop(js_sys::Promise::from(Self::new(x)))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl<T> Default for ApiFuture<T>
|
|
64
|
+
where
|
|
65
|
+
Result<T, JsValue>: IntoJsResult + 'static,
|
|
66
|
+
T: Default,
|
|
67
|
+
{
|
|
68
|
+
fn default() -> Self {
|
|
69
|
+
Self::new(async { Ok(Default::default()) })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl<T> From<ApiFuture<T>> for JsValue
|
|
74
|
+
where
|
|
75
|
+
Result<T, Self>: IntoJsResult + 'static,
|
|
76
|
+
{
|
|
77
|
+
fn from(fut: ApiFuture<T>) -> Self {
|
|
78
|
+
js_sys::Promise::from(fut).unchecked_into()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl<T> From<ApiFuture<T>> for js_sys::Promise
|
|
83
|
+
where
|
|
84
|
+
Result<T, JsValue>: IntoJsResult + 'static,
|
|
85
|
+
{
|
|
86
|
+
fn from(fut: ApiFuture<T>) -> Self {
|
|
87
|
+
future_to_promise(async move {
|
|
88
|
+
match fut.0.await.ignore_view_delete()? {
|
|
89
|
+
Some(x) => Ok(x).into_js_result(),
|
|
90
|
+
None => Ok::<_, JsValue>(()).into_js_result(),
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl<T> WasmDescribe for ApiFuture<T>
|
|
97
|
+
where
|
|
98
|
+
Result<T, JsValue>: IntoJsResult + 'static,
|
|
99
|
+
{
|
|
100
|
+
fn describe() {
|
|
101
|
+
<js_sys::Promise as WasmDescribe>::describe()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
impl<T> IntoWasmAbi for ApiFuture<T>
|
|
106
|
+
where
|
|
107
|
+
Result<T, JsValue>: IntoJsResult + 'static,
|
|
108
|
+
{
|
|
109
|
+
type Abi = <js_sys::Promise as IntoWasmAbi>::Abi;
|
|
110
|
+
|
|
111
|
+
#[inline]
|
|
112
|
+
fn into_abi(self) -> Self::Abi {
|
|
113
|
+
js_sys::Promise::from(self).into_abi()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
impl<T> FromWasmAbi for ApiFuture<T>
|
|
118
|
+
where
|
|
119
|
+
Result<T, JsValue>: IntoJsResult + 'static,
|
|
120
|
+
T: From<JsValue> + Into<JsValue>,
|
|
121
|
+
{
|
|
122
|
+
type Abi = <js_sys::Promise as IntoWasmAbi>::Abi;
|
|
123
|
+
|
|
124
|
+
#[inline]
|
|
125
|
+
unsafe fn from_abi(js: Self::Abi) -> Self {
|
|
126
|
+
Self::new(async move {
|
|
127
|
+
let promise = unsafe { js_sys::Promise::from_abi(js) };
|
|
128
|
+
Ok(JsFuture::from(promise).await?.into())
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
impl<T> Future for ApiFuture<T>
|
|
134
|
+
where
|
|
135
|
+
Result<T, JsValue>: IntoJsResult + 'static,
|
|
136
|
+
{
|
|
137
|
+
type Output = ApiResult<T>;
|
|
138
|
+
|
|
139
|
+
fn poll(
|
|
140
|
+
self: Pin<&mut Self>,
|
|
141
|
+
cx: &mut std::task::Context<'_>,
|
|
142
|
+
) -> std::task::Poll<Self::Output> {
|
|
143
|
+
let mut fut = unsafe { self.map_unchecked_mut(|s| &mut s.0) };
|
|
144
|
+
fut.as_mut().poll(cx)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[extend::ext]
|
|
149
|
+
pub impl<T> Result<T, ApiError> {
|
|
150
|
+
/// Wraps an error `JsValue` return from a caught JavaScript exception,
|
|
151
|
+
/// checking for the explicit error type indicating that a
|
|
152
|
+
/// `JsPerspectiveView` call has been cancelled due to it already being
|
|
153
|
+
/// deleted. This is a normal mechanic of the `JsPerspectiveView` to
|
|
154
|
+
/// cancel a `View` call that is no longer need be the viewer, e.g. when
|
|
155
|
+
/// the user updates the UI before the previous update has finished
|
|
156
|
+
/// drawing. Without using exceptions for this, we'd need to wrap every
|
|
157
|
+
/// such `JsPerspectiveView` call individually.
|
|
158
|
+
///
|
|
159
|
+
/// When `"View method cancelled"` message is received, this call should
|
|
160
|
+
/// silently be replaced with `Ok`. The message itself is returned in this
|
|
161
|
+
/// case (instead of whatever the `async` returns), which is helpful for
|
|
162
|
+
/// detecting this condition when debugging.
|
|
163
|
+
fn ignore_view_delete(self) -> Result<Option<T>, ApiError> {
|
|
164
|
+
self.map(|x| Some(x)).or_else(|x| match x.inner() {
|
|
165
|
+
ApiErrorType::ClientError(ClientError::ViewNotFound) => Ok(None),
|
|
166
|
+
ApiErrorType::JsRawError(..) | ApiErrorType::JsError(..)
|
|
167
|
+
if format!("{x}").contains("View not found") =>
|
|
168
|
+
{
|
|
169
|
+
Ok(None)
|
|
170
|
+
},
|
|
171
|
+
x => Err(x.clone().into()),
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}
|