1use 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 #[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}