perspective_client/view.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 std::collections::HashMap;
14use std::ops::Deref;
15use std::str::FromStr;
16use std::sync::Arc;
17
18use futures::Future;
19use prost::bytes::Bytes;
20use serde::{Deserialize, Serialize};
21use ts_rs::TS;
22
23use self::view_on_update_req::Mode;
24use crate::assert_view_api;
25use crate::client::Client;
26use crate::proto::request::ClientReq;
27use crate::proto::response::ClientResp;
28use crate::proto::*;
29#[cfg(doc)]
30use crate::table::Table;
31pub use crate::utils::*;
32
33/// Options for [`View::on_update`].
34#[derive(Default, Debug, Deserialize, TS)]
35pub struct OnUpdateOptions {
36 pub mode: Option<OnUpdateMode>,
37}
38
39/// The update mode for [`View::on_update`].
40///
41/// `Row` mode calculates and provides the update batch new rows/columns as an
42/// Apache Arrow to the callback provided to [`View::on_update`]. This allows
43/// incremental updates if your callbakc can read this format, but should be
44/// disabled otherwise.
45#[derive(Default, Debug, Deserialize, TS)]
46pub enum OnUpdateMode {
47 #[default]
48 #[serde(rename = "row")]
49 Row,
50}
51
52impl FromStr for OnUpdateMode {
53 type Err = ClientError;
54
55 fn from_str(s: &str) -> Result<Self, Self::Err> {
56 if s == "row" {
57 Ok(OnUpdateMode::Row)
58 } else {
59 Err(ClientError::Option)
60 }
61 }
62}
63
64#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
65pub struct ColumnWindow {
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub start_col: Option<f32>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub end_col: Option<f32>,
71}
72
73/// Options for serializing a window of data from a [`View`].
74///
75/// Some fields of [`ViewWindow`] are only applicable to specific methods of
76/// [`View`].
77#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
78pub struct ViewWindow {
79 #[ts(optional)]
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub start_row: Option<f64>,
82
83 #[ts(optional)]
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub start_col: Option<f64>,
86
87 #[ts(optional)]
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub end_row: Option<f64>,
90
91 #[ts(optional)]
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub end_col: Option<f64>,
94
95 #[ts(optional)]
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub id: Option<bool>,
98
99 #[ts(optional)]
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub index: Option<bool>,
102
103 /// Only impacts [`View::to_csv`]
104 #[ts(optional)]
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub formatted: Option<bool>,
107
108 /// Arrow IPC body compression for [`View::to_arrow`], `"lz4"` or `"zstd"`
109 /// (uncompressed when omitted).
110 #[ts(optional)]
111 #[ts(type = "\"lz4\" | \"zstd\"")]
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub compression: Option<String>,
114
115 /// When `true`, [`View::to_arrow`] names `group_by` key columns
116 /// `__ROW_PATH_N__` and never qualifies an aggregate, instead of the
117 /// human-readable names a key column and its aggregate would otherwise
118 /// take.
119 #[ts(optional)]
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub machine_column_names: Option<bool>,
122}
123
124impl From<ViewWindow> for ViewPort {
125 fn from(window: ViewWindow) -> Self {
126 ViewPort {
127 start_row: window.start_row.map(|x| x.floor() as u32),
128 start_col: window.start_col.map(|x| x.floor() as u32),
129 end_row: window.end_row.map(|x| x.ceil() as u32),
130 end_col: window.end_col.map(|x| x.ceil() as u32),
131 machine_column_names: window.machine_column_names,
132 }
133 }
134}
135
136impl From<ViewPort> for ViewWindow {
137 fn from(window: ViewPort) -> Self {
138 ViewWindow {
139 start_row: window.start_row.map(|x| x as f64),
140 start_col: window.start_col.map(|x| x as f64),
141 end_row: window.end_row.map(|x| x as f64),
142 end_col: window.end_col.map(|x| x as f64),
143 machine_column_names: window.machine_column_names,
144 ..ViewWindow::default()
145 }
146 }
147}
148
149/// Rows updated and port ID corresponding to an update batch, provided to the
150/// callback argument to [`View::on_update`] with the "rows" mode.
151#[derive(TS)]
152pub struct OnUpdateData(crate::proto::ViewOnUpdateResp);
153
154impl Deref for OnUpdateData {
155 type Target = crate::proto::ViewOnUpdateResp;
156
157 fn deref(&self) -> &Self::Target {
158 &self.0
159 }
160}
161
162/// The rule under which the rows reported by [`View::on_remove`] left the
163/// [`View`].
164#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, TS)]
165#[serde(rename_all = "lowercase")]
166pub enum RemoveCause {
167 Key,
168 Group,
169 Filter,
170}
171
172impl RemoveCause {
173 /// This cause as its lowercase name.
174 pub fn as_str(&self) -> &'static str {
175 match self {
176 RemoveCause::Key => "key",
177 RemoveCause::Group => "group",
178 RemoveCause::Filter => "filter",
179 }
180 }
181}
182
183impl From<view_on_remove_resp::Cause> for RemoveCause {
184 fn from(cause: view_on_remove_resp::Cause) -> Self {
185 match cause {
186 view_on_remove_resp::Cause::Key => RemoveCause::Key,
187 view_on_remove_resp::Cause::Group => RemoveCause::Group,
188 view_on_remove_resp::Cause::Filter => RemoveCause::Filter,
189 }
190 }
191}
192
193/// The keys of rows which left a [`View`], the port ID and the [`RemoveCause`]
194/// they left under, provided to the callback argument to [`View::on_remove`].
195#[derive(Clone, Debug, Serialize, TS)]
196pub struct OnRemoveData {
197 pub port_id: u32,
198 #[ts(type = "Uint8Array | undefined")]
199 #[serde(with = "serde_bytes")]
200 pub indices: Option<Vec<u8>>,
201 pub cause: RemoveCause,
202}
203
204impl From<ViewOnRemoveResp> for OnRemoveData {
205 fn from(resp: ViewOnRemoveResp) -> Self {
206 OnRemoveData {
207 port_id: resp.port_id,
208 cause: resp.cause().into(),
209 indices: resp.indices,
210 }
211 }
212}
213/// The [`View`] struct is Perspective's query and serialization interface. It
214/// represents a query on the `Table`'s dataset and is always created from an
215/// existing `Table` instance via the [`Table::view`] method.
216///
217/// [`View`]s are immutable with respect to the arguments provided to the
218/// [`Table::view`] method; to change these parameters, you must create a new
219/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
220/// respect to the [`Table`]'s data, and will (within a conflation window)
221/// update with the latest state as its parent [`Table`] updates, including
222/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
223/// query parameters are composable, in that each parameter works independently
224/// _and_ in conjunction with each other, and there is no limit to the number of
225/// pivots, filters, etc. which can be applied.
226///
227/// To construct a [`View`], call the [`Table::view`] factory method. A
228/// [`Table`] can have as many [`View`]s associated with it as you need -
229/// Perspective conserves memory by relying on a single [`Table`] to power
230/// multiple [`View`]s concurrently.
231///
232/// # Examples
233///
234/// ```no_run
235/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, ViewWindow};
236/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
237/// # let client: Client = todo!();
238/// let opts = TableInitOptions::default();
239/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
240/// let table = client.table(data, opts).await?;
241///
242/// let view = table.view(None).await?;
243/// let arrow = view.to_arrow(ViewWindow::default()).await?;
244/// view.delete().await?;
245/// # Ok(()) }
246/// ```
247///
248/// ```no_run
249/// # use std::collections::HashMap;
250/// # use perspective_client::Table;
251/// # use perspective_client::config::*;
252/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
253/// # let table: Table = todo!();
254/// let view = table
255/// .view(Some(ViewConfigUpdate {
256/// columns: Some(vec![Some("Sales".into())]),
257/// aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
258/// group_by: Some(vec!["Region".into(), "Country".into()]),
259/// filter: Some(vec![Filter::new("Category", "in", &[
260/// "Furniture",
261/// "Technology",
262/// ])]),
263/// ..ViewConfigUpdate::default()
264/// }))
265/// .await?;
266/// # Ok(()) }
267/// ```
268///
269/// Group By
270///
271/// ```no_run
272/// # use perspective_client::Table;
273/// # use perspective_client::config::*;
274/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
275/// # let table: Table = todo!();
276/// let view = table
277/// .view(Some(ViewConfigUpdate {
278/// group_by: Some(vec!["a".into(), "c".into()]),
279/// ..ViewConfigUpdate::default()
280/// }))
281/// .await?;
282/// # Ok(()) }
283/// ```
284///
285/// Split By
286///
287/// ```no_run
288/// # use perspective_client::Table;
289/// # use perspective_client::config::*;
290/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
291/// # let table: Table = todo!();
292/// let view = table
293/// .view(Some(ViewConfigUpdate {
294/// split_by: Some(vec!["a".into(), "c".into()]),
295/// ..ViewConfigUpdate::default()
296/// }))
297/// .await?;
298/// # Ok(()) }
299/// ```
300///
301/// In Javascript, a [`Table`] can be constructed on a [`Table::view`] instance,
302/// which will return a new [`Table`] based on the [`Table::view`]'s dataset,
303/// and all future updates that affect the [`Table::view`] will be forwarded to
304/// the new [`Table`]. This is particularly useful for implementing a
305/// [Client/Server Replicated](server.md#clientserver-replicated) design, by
306/// serializing the `View` to an arrow and setting up an `on_update` callback.
307///
308/// ```no_run
309/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, UpdateOptions};
310/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
311/// # let client: Client = todo!();
312/// let opts = TableInitOptions::default();
313/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
314/// let table = client.table(data, opts.clone()).await?;
315/// let view = table.view(None).await?;
316/// let table2 = client.table(TableData::View(view), opts).await?;
317/// let more = UpdateData::Csv("x,y\n5,6".into());
318/// table.update(more, UpdateOptions::default()).await?;
319/// # Ok(()) }
320/// ```
321#[derive(Clone, Debug)]
322pub struct View {
323 pub name: String,
324 client: Client,
325 pub(crate) source: Option<ViewSource>,
326}
327
328/// The options of the [`Table`] a [`View`] was created from, as known to the
329/// [`Client`] which created it; a [`View`] opened by name alone has no source.
330#[derive(Clone, Debug)]
331pub(crate) struct ViewSource {
332 pub options: crate::table::TableOptions,
333}
334
335assert_view_api!(View);
336
337impl View {
338 pub(crate) fn client(&self) -> &Client {
339 &self.client
340 }
341
342 pub fn new(name: String, client: Client) -> Self {
343 View {
344 name,
345 client,
346 source: None,
347 }
348 }
349
350 pub(crate) fn new_with_source(name: String, client: Client, source: ViewSource) -> Self {
351 View {
352 name,
353 client,
354 source: Some(source),
355 }
356 }
357
358 fn client_message(&self, req: ClientReq) -> Request {
359 crate::proto::Request {
360 msg_id: self.client.gen_id(),
361 entity_id: self.name.clone(),
362 client_req: Some(req),
363 }
364 }
365
366 /// Returns this [`View`]'s column header area for `window`, transposed as
367 /// `area[level][column]`.
368 ///
369 /// A column path shows the columns that a given cell belongs to after
370 /// pivots are applied. The area is rectangular with one level per
371 /// `split_by` plus one: a column's split values occupy the leading levels
372 /// and its name always occupies the last, so a subtotal or grand total
373 /// under `split_rollup_mode: "rollup"` reads [`Scalar::Null`] at the levels
374 /// it does not pivot on. Values keep their column's type - a `datetime`
375 /// split value is epoch milliseconds, not text - so formatting them is the
376 /// caller's choice.
377 ///
378 /// `window` slices the *column* axis; the number of levels does not depend
379 /// on it.
380 pub async fn column_paths(
381 &self,
382 window: ColumnWindow,
383 ) -> ClientResult<Vec<Vec<crate::config::Scalar>>> {
384 let msg = self.client_message(ClientReq::ViewColumnPathsReq(ViewColumnPathsReq {
385 start_col: window.start_col.map(|x| x as u32),
386 end_col: window.end_col.map(|x| x as u32),
387 }));
388
389 match self.client.oneshot(&msg).await? {
390 ClientResp::ViewColumnPathsResp(ViewColumnPathsResp { area }) => Ok(area
391 .into_iter()
392 .map(|level| {
393 level
394 .values
395 .into_iter()
396 .map(crate::config::Scalar::from)
397 .collect()
398 })
399 .collect()),
400 resp => Err(resp.into()),
401 }
402 }
403
404 /// Returns this [`View`]'s _dimensions_, row and column count, as well as
405 /// those of the [`crate::Table`] from which it was derived.
406 ///
407 /// - `num_table_rows` - The number of rows in the underlying
408 /// [`crate::Table`].
409 /// - `num_table_columns` - The number of columns in the underlying
410 /// [`crate::Table`] (including the `index` column if this
411 /// [`crate::Table`] was constructed with one).
412 /// - `num_view_rows` - The number of rows in this [`View`]. If this
413 /// [`View`] has a `group_by` clause, `num_view_rows` will also include
414 /// aggregated rows.
415 /// - `num_view_columns` - The number of columns in this [`View`]. If this
416 /// [`View`] has a `split_by` clause, `num_view_columns` will include all
417 /// _column paths_, e.g. the number of `columns` clause times the number
418 /// of `split_by` groups.
419 pub async fn dimensions(&self) -> ClientResult<ViewDimensionsResp> {
420 let msg = self.client_message(ClientReq::ViewDimensionsReq(ViewDimensionsReq {}));
421 match self.client.oneshot(&msg).await? {
422 ClientResp::ViewDimensionsResp(resp) => Ok(resp),
423 resp => Err(resp.into()),
424 }
425 }
426
427 /// The expression schema of this [`View`], which contains only the
428 /// expressions created on this [`View`]. See [`View::schema`] for
429 /// details.
430 pub async fn expression_schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
431 if self.client.get_features().await?.expressions {
432 let msg = self.client_message(ClientReq::ViewExpressionSchemaReq(
433 ViewExpressionSchemaReq {},
434 ));
435 match self.client.oneshot(&msg).await? {
436 ClientResp::ViewExpressionSchemaResp(ViewExpressionSchemaResp { schema }) => {
437 Ok(schema
438 .into_iter()
439 .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
440 .collect())
441 },
442 resp => Err(resp.into()),
443 }
444 } else {
445 Ok([].into_iter().collect())
446 }
447 }
448
449 /// A copy of the [`ViewConfig`] object passed to the [`Table::view`] method
450 /// which created this [`View`].
451 pub async fn get_config(&self) -> ClientResult<crate::config::ViewConfig> {
452 let msg = self.client_message(ClientReq::ViewGetConfigReq(ViewGetConfigReq {}));
453 match self.client.oneshot(&msg).await? {
454 ClientResp::ViewGetConfigResp(ViewGetConfigResp {
455 config: Some(config),
456 }) => Ok(config.into()),
457 resp => Err(resp.into()),
458 }
459 }
460
461 /// The number of aggregated rows in this [`View`]. This is affected by the
462 /// "group_by" configuration parameter supplied to this view's contructor.
463 ///
464 /// # Returns
465 ///
466 /// The number of aggregated rows.
467 pub async fn num_rows(&self) -> ClientResult<u32> {
468 Ok(self.dimensions().await?.num_view_rows)
469 }
470
471 /// The schema of this [`View`].
472 ///
473 /// The [`View`] schema differs from the `schema` returned by
474 /// [`Table::schema`]; it may have different column names due to
475 /// `expressions` or `columns` configs, or it maye have _different
476 /// column types_ due to the application og `group_by` and `aggregates`
477 /// config. You can think of [`Table::schema`] as the _input_ schema and
478 /// [`View::schema`] as the _output_ schema of a Perspective pipeline.
479 pub async fn schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
480 let msg = self.client_message(ClientReq::ViewSchemaReq(ViewSchemaReq {}));
481 match self.client.oneshot(&msg).await? {
482 ClientResp::ViewSchemaResp(ViewSchemaResp { schema }) => Ok(schema
483 .into_iter()
484 .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
485 .collect()),
486 resp => Err(resp.into()),
487 }
488 }
489
490 /// Serializes a [`View`] to the Apache Arrow data format, with IPC body
491 /// compression per [`ViewWindow::compression`].
492 pub async fn to_arrow(&self, window: ViewWindow) -> ClientResult<Bytes> {
493 let msg = self.client_message(ClientReq::ViewToArrowReq(ViewToArrowReq {
494 viewport: Some(window.clone().into()),
495 compression: window.compression,
496 }));
497
498 match self.client.oneshot(&msg).await? {
499 ClientResp::ViewToArrowResp(ViewToArrowResp { arrow }) => Ok(arrow.into()),
500 resp => Err(resp.into()),
501 }
502 }
503
504 /// Serializes this [`View`] to a string of JSON data. Useful if you want to
505 /// save additional round trip serialize/deserialize cycles.
506 pub async fn to_columns_string(&self, window: ViewWindow) -> ClientResult<String> {
507 let msg = self.client_message(ClientReq::ViewToColumnsStringReq(ViewToColumnsStringReq {
508 viewport: Some(window.clone().into()),
509 id: window.id,
510 index: window.index,
511 formatted: window.formatted,
512 }));
513
514 match self.client.oneshot(&msg).await? {
515 ClientResp::ViewToColumnsStringResp(ViewToColumnsStringResp { json_string }) => {
516 Ok(json_string)
517 },
518 resp => Err(resp.into()),
519 }
520 }
521
522 /// Render this `View` as a JSON string.
523 pub async fn to_json_string(&self, window: ViewWindow) -> ClientResult<String> {
524 let viewport = ViewPort::from(window.clone());
525 let msg = self.client_message(ClientReq::ViewToRowsStringReq(ViewToRowsStringReq {
526 viewport: Some(viewport),
527 id: window.id,
528 index: window.index,
529 formatted: window.formatted,
530 }));
531
532 match self.client.oneshot(&msg).await? {
533 ClientResp::ViewToRowsStringResp(ViewToRowsStringResp { json_string }) => {
534 Ok(json_string)
535 },
536 resp => Err(resp.into()),
537 }
538 }
539
540 /// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
541 /// formatted [`String`].
542 pub async fn to_ndjson(&self, window: ViewWindow) -> ClientResult<String> {
543 let viewport = ViewPort::from(window.clone());
544 let msg = self.client_message(ClientReq::ViewToNdjsonStringReq(ViewToNdjsonStringReq {
545 viewport: Some(viewport),
546 id: window.id,
547 index: window.index,
548 formatted: window.formatted,
549 }));
550
551 match self.client.oneshot(&msg).await? {
552 ClientResp::ViewToNdjsonStringResp(ViewToNdjsonStringResp { ndjson_string }) => {
553 Ok(ndjson_string)
554 },
555 resp => Err(resp.into()),
556 }
557 }
558
559 /// Serializes this [`View`] to CSV data in a standard format.
560 pub async fn to_csv(&self, window: ViewWindow) -> ClientResult<String> {
561 let msg = self.client_message(ClientReq::ViewToCsvReq(ViewToCsvReq {
562 viewport: Some(window.into()),
563 }));
564
565 match self.client.oneshot(&msg).await? {
566 ClientResp::ViewToCsvResp(ViewToCsvResp { csv }) => Ok(csv),
567 resp => Err(resp.into()),
568 }
569 }
570
571 /// Delete this [`View`] and clean up all resources associated with it.
572 /// [`View`] objects do not stop consuming resources or processing
573 /// updates when they are garbage collected - you must call this method
574 /// to reclaim these.
575 pub async fn delete(&self) -> ClientResult<()> {
576 let msg = self.client_message(ClientReq::ViewDeleteReq(ViewDeleteReq {}));
577 match self.client.oneshot(&msg).await? {
578 ClientResp::ViewDeleteResp(_) => Ok(()),
579 resp => Err(resp.into()),
580 }
581 }
582
583 /// Calculates the [min, max] of the leaf nodes of a column `column_name`.
584 ///
585 /// # Returns
586 ///
587 /// A tuple of [min, max], whose types are column and aggregate dependent.
588 pub async fn get_min_max(
589 &self,
590 column_name: String,
591 ) -> ClientResult<(crate::config::Scalar, crate::config::Scalar)> {
592 let msg = self.client_message(ClientReq::ViewGetMinMaxReq(ViewGetMinMaxReq {
593 column_name,
594 }));
595
596 match self.client.oneshot(&msg).await? {
597 ClientResp::ViewGetMinMaxResp(ViewGetMinMaxResp { min, max }) => {
598 let min = min.map(crate::config::Scalar::from).unwrap_or_default();
599 let max = max.map(crate::config::Scalar::from).unwrap_or_default();
600 Ok((min, max))
601 },
602 resp => Err(resp.into()),
603 }
604 }
605
606 /// Register a callback with this [`View`]. Whenever the view's underlying
607 /// table emits an update, this callback will be invoked with an object
608 /// containing `port_id`, indicating which port the update fired on,
609 /// `reset`, which is `true` when the step began with [`Table::clear`] or
610 /// [`Table::replace`] so `delta` is relative to an empty table, and
611 /// optionally `delta`, which is the new data that was updated for each
612 /// cell or each row.
613 ///
614 /// # Arguments
615 ///
616 /// - `on_update` - A callback function invoked on update, which receives an
617 /// object with three keys: `port_id`, indicating which port the update
618 /// was triggered on, `reset`, and `delta`, whose value is dependent on
619 /// the mode parameter.
620 /// - `options` - If this is provided as `OnUpdateOptions { mode:
621 /// Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
622 /// rows. Otherwise `delta` will be [`Option::None`].
623 pub async fn on_update<T, U>(&self, on_update: T, options: OnUpdateOptions) -> ClientResult<u32>
624 where
625 T: Fn(OnUpdateData) -> U + Send + Sync + 'static,
626 U: Future<Output = ()> + Send + 'static,
627 {
628 let on_update = Arc::new(on_update);
629 let callback = move |resp: Response| {
630 let on_update = on_update.clone();
631 async move {
632 match resp.client_resp {
633 Some(ClientResp::ViewOnUpdateResp(resp)) => {
634 on_update(OnUpdateData(resp)).await;
635 Ok(())
636 },
637 resp => Err(resp.into()),
638 }
639 }
640 };
641
642 let msg = self.client_message(ClientReq::ViewOnUpdateReq(ViewOnUpdateReq {
643 mode: options.mode.map(|OnUpdateMode::Row| Mode::Row as i32),
644 }));
645
646 self.client.subscribe(&msg, callback).await?;
647 Ok(msg.msg_id)
648 }
649
650 /// Unregister a previously registered update callback with this [`View`].
651 ///
652 /// # Arguments
653 ///
654 /// - `id` - A callback `id` as returned by a recipricol call to
655 /// [`View::on_update`].
656 ///
657 /// # Examples
658 ///
659 /// ```no_run
660 /// # use perspective_client::{OnUpdateOptions, View};
661 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
662 /// # let view: View = todo!();
663 /// let callback = |_| async { print!("Updated!") };
664 /// let cid = view.on_update(callback, OnUpdateOptions::default()).await?;
665 /// view.remove_update(cid).await?;
666 /// # Ok(()) }
667 /// ```
668 pub async fn remove_update(&self, update_id: u32) -> ClientResult<()> {
669 let msg = self.client_message(ClientReq::ViewRemoveOnUpdateReq(ViewRemoveOnUpdateReq {
670 id: update_id,
671 }));
672
673 self.client.unsubscribe(update_id).await?;
674 match self.client.oneshot(&msg).await? {
675 ClientResp::ViewRemoveOnUpdateResp(_) => Ok(()),
676 resp => Err(resp.into()),
677 }
678 }
679
680 /// Register a callback which is invoked whenever rows leave this [`View`],
681 /// with their keys as an Apache Arrow of one column per key column named
682 /// after it and the [`RemoveCause`] they left under.
683 ///
684 /// For a [`View`] without `group_by` the keys are the `index` columns of
685 /// the [`Table`], which must have one: [`RemoveCause::Key`] when
686 /// [`Table::remove`] removed them ([`Table::replace`] reports the keys it
687 /// does not re-supply and [`Table::clear`] every key) and
688 /// [`RemoveCause::Filter`] when [`Table::update`] moved them outside the
689 /// [`View`]'s `filter`. For a [`View`] with `group_by` the keys are the
690 /// `group_by` columns of each group which no longer has any rows,
691 /// [`RemoveCause::Group`], `null` below the group's depth. It fires once
692 /// per cause per update step.
693 ///
694 /// # Examples
695 ///
696 /// ```no_run
697 /// # use perspective_client::{View, OnRemoveData};
698 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
699 /// # let view: View = todo!();
700 /// let callback = |removed: OnRemoveData| async move { println!("{:?}", removed.port_id) };
701 /// let cid = view.on_remove(callback).await?;
702 /// view.remove_remove(cid).await?;
703 /// # Ok(()) }
704 /// ```
705 pub async fn on_remove<T, U>(&self, on_remove: T) -> ClientResult<u32>
706 where
707 T: Fn(OnRemoveData) -> U + Send + Sync + 'static,
708 U: Future<Output = ()> + Send + 'static,
709 {
710 let on_remove = Arc::new(on_remove);
711 let callback = move |resp: Response| {
712 let on_remove = on_remove.clone();
713 async move {
714 match resp.client_resp {
715 Some(ClientResp::ViewOnRemoveResp(resp)) => {
716 on_remove(OnRemoveData::from(resp)).await;
717 Ok(())
718 },
719 resp => Err(resp.into()),
720 }
721 }
722 };
723
724 let msg = self.client_message(ClientReq::ViewOnRemoveReq(ViewOnRemoveReq {}));
725 self.client.subscribe(&msg, callback).await?;
726 Ok(msg.msg_id)
727 }
728
729 /// Unregister a previously registered [`View::on_remove`] callback.
730 ///
731 /// # Arguments
732 ///
733 /// - `callback_id` - A callback `id` as returned by a reciprocal call to
734 /// [`View::on_remove`].
735 pub async fn remove_remove(&self, callback_id: u32) -> ClientResult<()> {
736 let msg = self.client_message(ClientReq::ViewRemoveOnRemoveReq(ViewRemoveOnRemoveReq {
737 id: callback_id,
738 }));
739
740 self.client.unsubscribe(callback_id).await?;
741 match self.client.oneshot(&msg).await? {
742 ClientResp::ViewRemoveOnRemoveResp(_) => Ok(()),
743 resp => Err(resp.into()),
744 }
745 }
746
747 /// Register a callback with this [`View`]. Whenever the [`View`] is
748 /// deleted, this callback will be invoked.
749 pub async fn on_delete(
750 &self,
751 on_delete: Box<dyn Fn() + Send + Sync + 'static>,
752 ) -> ClientResult<u32> {
753 let callback = move |resp: Response| match resp.client_resp.unwrap() {
754 ClientResp::ViewOnDeleteResp(_) => {
755 on_delete();
756 Ok(())
757 },
758 resp => Err(resp.into()),
759 };
760
761 let msg = self.client_message(ClientReq::ViewOnDeleteReq(ViewOnDeleteReq {}));
762 self.client.subscribe_once(&msg, Box::new(callback)).await?;
763 Ok(msg.msg_id)
764 }
765
766 /// Unregister a previously registered [`View::on_delete`] callback.
767 pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
768 let msg = self.client_message(ClientReq::ViewRemoveDeleteReq(ViewRemoveDeleteReq {
769 id: callback_id,
770 }));
771
772 match self.client.oneshot(&msg).await? {
773 ClientResp::ViewRemoveDeleteResp(ViewRemoveDeleteResp {}) => Ok(()),
774 resp => Err(resp.into()),
775 }
776 }
777
778 /// Collapses the `group_by` row at `row_index`.
779 pub async fn collapse(&self, row_index: u32) -> ClientResult<u32> {
780 let msg = self.client_message(ClientReq::ViewCollapseReq(ViewCollapseReq { row_index }));
781 match self.client.oneshot(&msg).await? {
782 ClientResp::ViewCollapseResp(ViewCollapseResp { num_changed }) => Ok(num_changed),
783 resp => Err(resp.into()),
784 }
785 }
786
787 /// Expand the `group_by` row at `row_index`.
788 pub async fn expand(&self, row_index: u32) -> ClientResult<u32> {
789 let msg = self.client_message(ClientReq::ViewExpandReq(ViewExpandReq { row_index }));
790 match self.client.oneshot(&msg).await? {
791 ClientResp::ViewExpandResp(ViewExpandResp { num_changed }) => Ok(num_changed),
792 resp => Err(resp.into()),
793 }
794 }
795
796 /// Set expansion `depth` of the `group_by` tree.
797 pub async fn set_depth(&self, depth: u32) -> ClientResult<()> {
798 let msg = self.client_message(ClientReq::ViewSetDepthReq(ViewSetDepthReq { depth }));
799 match self.client.oneshot(&msg).await? {
800 ClientResp::ViewSetDepthResp(_) => Ok(()),
801 resp => Err(resp.into()),
802 }
803 }
804}