perspective_js/
table.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 extend::ext;
14use js_sys::{Array, ArrayBuffer, Function, Object, Reflect, Uint8Array, JSON};
15use macro_rules_attribute::apply;
16use perspective_client::config::*;
17use perspective_client::{
18    assert_table_api, ColumnType, TableData, TableReadFormat, UpdateData, UpdateOptions,
19};
20use wasm_bindgen::convert::TryFromJsValue;
21use wasm_bindgen::prelude::*;
22use wasm_bindgen_futures::spawn_local;
23
24use crate::client::Client;
25use crate::utils::{
26    inherit_docs, ApiError, ApiFuture, ApiResult, JsValueSerdeExt, LocalPollLoop, ToApiError,
27};
28pub use crate::view::*;
29
30#[ext]
31impl Vec<(String, ColumnType)> {
32    fn from_js_value(value: &JsValue) -> ApiResult<Vec<(String, ColumnType)>> {
33        Ok(Object::keys(value.unchecked_ref())
34            .iter()
35            .map(|x| -> Result<_, JsValue> {
36                let key = x.as_string().into_apierror()?;
37                let val = Reflect::get(value, &x)?
38                    .as_string()
39                    .into_apierror()?
40                    .into_serde_ext()?;
41
42                Ok((key, val))
43            })
44            .collect::<Result<Vec<_>, _>>()?)
45    }
46}
47
48#[ext]
49pub(crate) impl TableData {
50    fn from_js_value(value: &JsValue, format: Option<TableReadFormat>) -> ApiResult<TableData> {
51        let err_fn = || JsValue::from(format!("Failed to construct Table {:?}", value));
52        if let Some(result) = UpdateData::from_js_value_partial(value, format)? {
53            Ok(result.into())
54        } else if value.is_instance_of::<Object>() && Reflect::has(value, &"__get_model".into())? {
55            let val = Reflect::get(value, &"__get_model".into())?
56                .dyn_into::<Function>()?
57                .call0(value)?;
58
59            let view = View::try_from_js_value(val)?;
60            Ok(TableData::View(view.0))
61        } else if value.is_instance_of::<Object>() {
62            let all_strings = || {
63                Object::values(value.unchecked_ref())
64                    .to_vec()
65                    .iter()
66                    .all(|x| x.is_string())
67            };
68
69            let all_arrays = || {
70                Object::values(value.unchecked_ref())
71                    .to_vec()
72                    .iter()
73                    .all(|x| x.is_instance_of::<Array>())
74            };
75
76            if all_strings() {
77                Ok(TableData::Schema(Vec::from_js_value(value)?))
78            } else if all_arrays() {
79                let json = JSON::stringify(value)?.as_string().into_apierror()?;
80                Ok(UpdateData::JsonColumns(json).into())
81            } else {
82                Err(err_fn().into())
83            }
84        } else {
85            Err(err_fn().into())
86        }
87    }
88}
89
90#[ext]
91pub(crate) impl UpdateData {
92    fn from_js_value_partial(
93        value: &JsValue,
94        format: Option<TableReadFormat>,
95    ) -> ApiResult<Option<UpdateData>> {
96        let err_fn = || JsValue::from(format!("Failed to construct Table {:?}", value));
97        if value.is_undefined() {
98            Err(err_fn().into())
99        } else if value.is_string() {
100            match format {
101                None | Some(TableReadFormat::Csv) => {
102                    Ok(Some(UpdateData::Csv(value.as_string().into_apierror()?)))
103                },
104                Some(TableReadFormat::JsonString) => Ok(Some(UpdateData::JsonRows(
105                    value.as_string().into_apierror()?,
106                ))),
107                Some(TableReadFormat::ColumnsString) => Ok(Some(UpdateData::JsonColumns(
108                    value.as_string().into_apierror()?,
109                ))),
110                Some(TableReadFormat::Arrow) => Ok(Some(UpdateData::Arrow(
111                    value.as_string().into_apierror()?.into_bytes().into(),
112                ))),
113                Some(TableReadFormat::Ndjson) => {
114                    Ok(Some(UpdateData::Ndjson(value.as_string().into_apierror()?)))
115                },
116            }
117        } else if value.is_instance_of::<ArrayBuffer>() {
118            let uint8array = Uint8Array::new(value);
119            let slice = uint8array.to_vec();
120            match format {
121                Some(TableReadFormat::Csv) => Ok(Some(UpdateData::Csv(String::from_utf8(slice)?))),
122                Some(TableReadFormat::JsonString) => {
123                    Ok(Some(UpdateData::JsonRows(String::from_utf8(slice)?)))
124                },
125                Some(TableReadFormat::ColumnsString) => {
126                    Ok(Some(UpdateData::JsonColumns(String::from_utf8(slice)?)))
127                },
128                Some(TableReadFormat::Ndjson) => {
129                    Ok(Some(UpdateData::Ndjson(String::from_utf8(slice)?)))
130                },
131                None | Some(TableReadFormat::Arrow) => Ok(Some(UpdateData::Arrow(slice.into()))),
132            }
133        } else if let Some(uint8array) = value.dyn_ref::<Uint8Array>() {
134            let slice = uint8array.to_vec();
135            match format {
136                Some(TableReadFormat::Csv) => Ok(Some(UpdateData::Csv(String::from_utf8(slice)?))),
137                Some(TableReadFormat::JsonString) => {
138                    Ok(Some(UpdateData::JsonRows(String::from_utf8(slice)?)))
139                },
140                Some(TableReadFormat::ColumnsString) => {
141                    Ok(Some(UpdateData::JsonColumns(String::from_utf8(slice)?)))
142                },
143                Some(TableReadFormat::Ndjson) => {
144                    Ok(Some(UpdateData::Ndjson(String::from_utf8(slice)?)))
145                },
146                None | Some(TableReadFormat::Arrow) => Ok(Some(UpdateData::Arrow(slice.into()))),
147            }
148        } else if value.is_instance_of::<Array>() {
149            let rows = JSON::stringify(value)?.as_string().into_apierror()?;
150            Ok(Some(UpdateData::JsonRows(rows)))
151        } else {
152            Ok(None)
153        }
154    }
155
156    fn from_js_value(value: &JsValue, format: Option<TableReadFormat>) -> ApiResult<UpdateData> {
157        match TableData::from_js_value(value, format)? {
158            TableData::Schema(_) => Err(ApiError::new(
159                "Method cannot be called with `Schema` argument",
160            )),
161            TableData::Update(x) => Ok(x),
162            TableData::View(_) => Err(ApiError::new(
163                "Method cannot be called with `Schema` argument",
164            )),
165        }
166    }
167}
168
169#[derive(Clone)]
170#[wasm_bindgen]
171pub struct Table(pub(crate) perspective_client::Table);
172
173assert_table_api!(Table);
174
175impl From<perspective_client::Table> for Table {
176    fn from(value: perspective_client::Table) -> Self {
177        Table(value)
178    }
179}
180
181impl Table {
182    pub fn get_table(&self) -> &'_ perspective_client::Table {
183        &self.0
184    }
185}
186
187#[wasm_bindgen]
188extern "C" {
189    // TODO Fix me
190    #[wasm_bindgen(typescript_type = "\
191        string | ArrayBuffer | Record<string, unknown[]> | Record<string, unknown>[]")]
192    pub type JsTableInitData;
193
194    #[wasm_bindgen(typescript_type = "ViewConfigUpdate")]
195    pub type JsViewConfig;
196
197    #[wasm_bindgen(typescript_type = "UpdateOptions")]
198    pub type JsUpdateOptions;
199}
200
201#[wasm_bindgen]
202impl Table {
203    #[apply(inherit_docs)]
204    #[inherit_doc = "table/get_index.md"]
205    #[wasm_bindgen]
206    pub async fn get_index(&self) -> Option<String> {
207        self.0.get_index()
208    }
209
210    #[apply(inherit_docs)]
211    #[inherit_doc = "table/get_client.md"]
212    #[wasm_bindgen]
213    pub async fn get_client(&self) -> Client {
214        Client {
215            close: None,
216            client: self.0.get_client(),
217        }
218    }
219
220    #[apply(inherit_docs)]
221    #[inherit_doc = "table/get_limit.md"]
222    #[wasm_bindgen]
223    pub async fn get_limit(&self) -> Option<u32> {
224        self.0.get_limit()
225    }
226
227    #[apply(inherit_docs)]
228    #[inherit_doc = "table/clear.md"]
229    #[wasm_bindgen]
230    pub async fn clear(&self) -> ApiResult<()> {
231        self.0.clear().await?;
232        Ok(())
233    }
234
235    #[apply(inherit_docs)]
236    #[inherit_doc = "table/delete.md"]
237    #[wasm_bindgen]
238    pub async fn delete(&self) -> ApiResult<()> {
239        self.0.delete().await?;
240        Ok(())
241    }
242
243    #[apply(inherit_docs)]
244    #[inherit_doc = "table/size.md"]
245    #[wasm_bindgen]
246    pub async fn size(&self) -> ApiResult<f64> {
247        Ok(self.0.size().await? as f64)
248    }
249
250    #[apply(inherit_docs)]
251    #[inherit_doc = "table/schema.md"]
252    #[wasm_bindgen]
253    pub async fn schema(&self) -> ApiResult<JsValue> {
254        let schema = self.0.schema().await?;
255        Ok(JsValue::from_serde_ext(&schema)?)
256    }
257
258    #[apply(inherit_docs)]
259    #[inherit_doc = "table/columns.md"]
260    #[wasm_bindgen]
261    pub async fn columns(&self) -> ApiResult<JsValue> {
262        let columns = self.0.columns().await?;
263        Ok(JsValue::from_serde_ext(&columns)?)
264    }
265
266    #[apply(inherit_docs)]
267    #[inherit_doc = "table/make_port.md"]
268    #[wasm_bindgen]
269    pub async fn make_port(&self) -> ApiResult<i32> {
270        Ok(self.0.make_port().await?)
271    }
272
273    #[apply(inherit_docs)]
274    #[inherit_doc = "table/on_delete.md"]
275    #[wasm_bindgen]
276    pub async fn on_delete(&self, on_delete: Function) -> ApiResult<u32> {
277        let emit = LocalPollLoop::new(move |()| on_delete.call0(&JsValue::UNDEFINED));
278        let on_delete = Box::new(move || spawn_local(emit.poll(())));
279        Ok(self.0.on_delete(on_delete).await?)
280    }
281
282    #[apply(inherit_docs)]
283    #[inherit_doc = "table/remove_delete.md"]
284    #[wasm_bindgen]
285    pub fn remove_delete(&self, callback_id: u32) -> ApiFuture<()> {
286        let client = self.0.clone();
287        ApiFuture::new(async move {
288            client.remove_delete(callback_id).await?;
289            Ok(())
290        })
291    }
292
293    #[apply(inherit_docs)]
294    #[inherit_doc = "table/replace.md"]
295    #[wasm_bindgen]
296    pub async fn remove(&self, value: &JsValue, options: Option<JsUpdateOptions>) -> ApiResult<()> {
297        let options = options
298            .into_serde_ext::<Option<UpdateOptions>>()?
299            .unwrap_or_default();
300
301        let input = UpdateData::from_js_value(value, options.format)?;
302        self.0.remove(input).await?;
303        Ok(())
304    }
305
306    #[apply(inherit_docs)]
307    #[inherit_doc = "table/replace.md"]
308    #[wasm_bindgen]
309    pub async fn replace(
310        &self,
311        input: &JsValue,
312        options: Option<JsUpdateOptions>,
313    ) -> ApiResult<()> {
314        let options = options
315            .into_serde_ext::<Option<UpdateOptions>>()?
316            .unwrap_or_default();
317
318        let input = UpdateData::from_js_value(input, options.format)?;
319        self.0.replace(input).await?;
320        Ok(())
321    }
322
323    #[apply(inherit_docs)]
324    #[inherit_doc = "table/update.md"]
325    #[wasm_bindgen]
326    pub async fn update(
327        &self,
328        input: &JsTableInitData,
329        options: Option<JsUpdateOptions>,
330    ) -> ApiResult<()> {
331        let options = options
332            .into_serde_ext::<Option<UpdateOptions>>()?
333            .unwrap_or_default();
334
335        let input = UpdateData::from_js_value(input, options.format)?;
336        self.0.update(input, options).await?;
337        Ok(())
338    }
339
340    #[apply(inherit_docs)]
341    #[inherit_doc = "table/view.md"]
342    #[wasm_bindgen]
343    pub async fn view(&self, config: Option<JsViewConfig>) -> ApiResult<View> {
344        let config = config
345            .map(|config| js_sys::JSON::stringify(&config))
346            .transpose()?
347            .and_then(|x| x.as_string())
348            .map(|x| serde_json::from_str(x.as_str()))
349            .transpose()?;
350
351        let view = self.0.view(config).await?;
352        Ok(View(view))
353    }
354
355    #[apply(inherit_docs)]
356    #[inherit_doc = "table/validate_expressions.md"]
357    #[wasm_bindgen]
358    pub async fn validate_expressions(&self, exprs: &JsValue) -> ApiResult<JsValue> {
359        let exprs = JsValue::into_serde_ext::<Expressions>(exprs.clone())?;
360        let columns = self.0.validate_expressions(exprs).await?;
361        Ok(JsValue::from_serde_ext(&columns)?)
362    }
363
364    #[allow(clippy::use_self)]
365    #[doc(hidden)]
366    pub fn unsafe_get_model(&self) -> *const Table {
367        std::ptr::addr_of!(*self)
368    }
369}