perspective_viewer/config/
viewer_config.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::io::{Read, Write};
15use std::ops::Deref;
16use std::str::FromStr;
17use std::sync::LazyLock;
18
19use flate2::Compression;
20use flate2::read::ZlibDecoder;
21use flate2::write::ZlibEncoder;
22use perspective_client::config::*;
23use perspective_js::utils::*;
24use serde::{Deserialize, Deserializer, Serialize};
25use serde_json::Value;
26use ts_rs::TS;
27use wasm_bindgen::JsCast;
28use wasm_bindgen::prelude::*;
29
30use super::ColumnConfigValues;
31use crate::presentation::ColumnConfigMap;
32
33pub enum ViewerConfigEncoding {
34    Json,
35    String,
36    ArrayBuffer,
37    JSONString,
38}
39
40impl FromStr for ViewerConfigEncoding {
41    type Err = JsValue;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        match s {
45            "json" => Ok(Self::Json),
46            "string" => Ok(Self::String),
47            "arraybuffer" => Ok(Self::ArrayBuffer),
48            x => Err(format!("Unknown format \"{}\"", x).into()),
49        }
50    }
51}
52
53/// The state of an entire `custom_elements::PerspectiveViewerElement` component
54/// and its `Plugin`.
55#[derive(Serialize, PartialEq)]
56#[serde(deny_unknown_fields)]
57pub struct ViewerConfig {
58    pub version: String,
59    pub plugin: String,
60    pub plugin_config: Value,
61    pub columns_config: ColumnConfigMap,
62    pub settings: bool,
63    pub theme: Option<String>,
64    pub title: Option<String>,
65
66    #[serde(flatten)]
67    pub view_config: ViewConfig,
68}
69
70// `#[serde(flatten)]` makes messagepack 2x as big as they can no longer be
71// struct fields, so make a tuple alternative for serialization in binary.
72type ViewerConfigBinarySerialFormat<'a> = (
73    &'a String,
74    &'a ColumnConfigMap,
75    &'a String,
76    &'a Value,
77    bool,
78    &'a Option<String>,
79    &'a Option<String>,
80    &'a ViewConfig,
81);
82
83type ViewerConfigBinaryDeserialFormat = (
84    VersionUpdate,
85    ColumnConfigUpdate,
86    PluginUpdate,
87    Option<Value>,
88    SettingsUpdate,
89    ThemeUpdate,
90    TitleUpdate,
91    ViewConfigUpdate,
92);
93
94pub static API_VERSION: LazyLock<&'static str> = LazyLock::new(|| {
95    #[derive(Deserialize)]
96    struct Package {
97        version: &'static str,
98    }
99    let pkg: &'static str = include_str!("../../../package.json");
100    let pkg: Package = serde_json::from_str(pkg).unwrap();
101    pkg.version
102});
103
104impl ViewerConfig {
105    fn token(&self) -> ViewerConfigBinarySerialFormat<'_> {
106        (
107            &self.version,
108            &self.columns_config,
109            &self.plugin,
110            &self.plugin_config,
111            self.settings,
112            &self.theme,
113            &self.title,
114            &self.view_config,
115        )
116    }
117
118    /// Encode a `ViewerConfig` to a `JsValue` in a supported type.
119    pub fn encode(&self, format: &Option<ViewerConfigEncoding>) -> ApiResult<JsValue> {
120        match format {
121            Some(ViewerConfigEncoding::String) => {
122                let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
123                let bytes = rmp_serde::to_vec_named(&self.token())?;
124                encoder.write_all(&bytes)?;
125                let encoded = encoder.finish()?;
126                Ok(JsValue::from(base64::encode(encoded)))
127            },
128            Some(ViewerConfigEncoding::ArrayBuffer) => {
129                let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
130                let bytes = rmp_serde::to_vec_named(&self.token())?;
131                encoder.write_all(&bytes)?;
132                let encoded = encoder.finish()?;
133                let array = js_sys::Uint8Array::from(&encoded[..]);
134                let start = array.byte_offset();
135                let len = array.byte_length();
136                Ok(array
137                    .buffer()
138                    .slice_with_end(start, start + len)
139                    .unchecked_into())
140            },
141            Some(ViewerConfigEncoding::JSONString) => {
142                Ok(JsValue::from(serde_json::to_string(self)?))
143            },
144            None | Some(ViewerConfigEncoding::Json) => Ok(JsValue::from_serde_ext(self)?),
145        }
146    }
147}
148
149#[derive(Clone, TS, Deserialize)]
150#[serde(transparent)]
151pub struct PluginConfig(serde_json::Value);
152
153// impl Type for PluginConfig {
154//     fn inline(type_map: &mut specta::TypeMap, generics: specta::Generics) ->
155// specta::DataType {         specta::Map::from(());
156//         // specta::Map {
157//         //     key_ty:
158//         // specta::DataType::Primitive(specta::PrimitiveType::String),
159//         //     value_ty: specta::DataType::Any,
160//         // }
161
162//         // specta::DataType::Map(specta::Map { Box::new((
163//         //     specta::DataType::Primitive(specta::PrimitiveType::String),
164//         //     specta::DataType::Any,
165//         // )))
166//     }
167//     // fn inline(_type_map: &mut specta::TypeMap, _generics:
168// &[specta::DataType]) ->     // specta::DataType {
169// specta::DataType::Map(Box::new((     //
170// specta::DataType::Primitive(specta::PrimitiveType::String),     //
171// specta::DataType::Any,     //     )))
172//     // }
173// }
174
175impl Deref for PluginConfig {
176    type Target = Value;
177
178    fn deref(&self) -> &Self::Target {
179        &self.0
180    }
181}
182
183#[derive(Clone, Deserialize, TS)]
184// #[serde(deny_unknown_fields)]
185pub struct ViewerConfigUpdate {
186    #[serde(default)]
187    #[ts(as = "Option<_>")]
188    #[ts(optional)]
189    pub version: VersionUpdate,
190
191    #[serde(default)]
192    #[ts(as = "Option<_>")]
193    #[ts(optional)]
194    pub plugin: PluginUpdate,
195
196    #[serde(default)]
197    #[ts(as = "Option<_>")]
198    #[ts(optional)]
199    pub title: TitleUpdate,
200
201    #[serde(default)]
202    #[ts(as = "Option<_>")]
203    #[ts(optional)]
204    pub theme: ThemeUpdate,
205
206    #[serde(default)]
207    #[ts(as = "Option<_>")]
208    #[ts(optional)]
209    pub settings: SettingsUpdate,
210
211    #[serde(default)]
212    #[ts(as = "Option<_>")]
213    #[ts(optional)]
214    pub plugin_config: Option<PluginConfig>,
215
216    #[serde(default)]
217    #[ts(as = "Option<_>")]
218    #[ts(optional)]
219    pub columns_config: ColumnConfigUpdate,
220
221    #[serde(flatten)]
222    pub view_config: ViewConfigUpdate,
223}
224
225impl ViewerConfigUpdate {
226    fn from_token(
227        (version, columns_config, plugin, plugin_config, settings, theme, title, view_config): ViewerConfigBinaryDeserialFormat,
228    ) -> ViewerConfigUpdate {
229        ViewerConfigUpdate {
230            version,
231            columns_config,
232            plugin,
233            plugin_config: plugin_config.map(PluginConfig),
234            settings,
235            theme,
236            title,
237            view_config,
238        }
239    }
240
241    /// Decode a `JsValue` into a `ViewerConfigUpdate` by auto-detecting format
242    /// from JavaScript type.
243    pub fn decode(update: &JsValue) -> ApiResult<Self> {
244        if update.is_string() {
245            let js_str = update.as_string().into_apierror()?;
246            let bytes = base64::decode(js_str)?;
247            let mut decoder = ZlibDecoder::new(&*bytes);
248            let mut decoded = vec![];
249            decoder.read_to_end(&mut decoded)?;
250            let token = rmp_serde::from_slice(&decoded[..])?;
251            Ok(ViewerConfigUpdate::from_token(token))
252        } else if update.is_instance_of::<js_sys::ArrayBuffer>() {
253            let uint8array = js_sys::Uint8Array::new(update);
254            let mut slice = vec![0; uint8array.length() as usize];
255            uint8array.copy_to(&mut slice[..]);
256            let mut decoder = ZlibDecoder::new(&*slice);
257            let mut decoded = vec![];
258            decoder.read_to_end(&mut decoded)?;
259            let token = rmp_serde::from_slice(&decoded[..])?;
260            Ok(ViewerConfigUpdate::from_token(token))
261        } else {
262            Ok(update.into_serde_ext()?)
263        }
264    }
265
266    pub fn migrate(&self) -> ApiResult<Self> {
267        // TODO: Call the migrate script from js
268        Ok(self.clone())
269    }
270}
271
272#[derive(Clone, Debug, Serialize, TS)]
273#[serde(untagged)]
274// #[ts(untagged)]
275pub enum OptionalUpdate<T: Clone> {
276    #[ts(skip)]
277    SetDefault,
278
279    // #[ts(skip)]
280    // #[ts(type = "undefined")]
281    Missing,
282
283    // #[ts(type = "_")]
284    // #[ts(untagged)]
285    Update(T),
286}
287
288// #[derive(Clone, Debug, Serialize, TS)]
289// #[serde(flatten)]
290// pub struct OptionalUpdate<T: Clone> {
291//     #[ts(optional)]
292//     inner: Option<OptionalUpdateInner<T>>,
293// }
294
295// // #[ts(optional = nullable)]
296
297// #[derive(Clone, Debug, Serialize, TS)]
298// pub struct OptionalUpdateInner<T: Clone>(Option<T>);
299
300pub type PluginUpdate = OptionalUpdate<String>;
301pub type SettingsUpdate = OptionalUpdate<bool>;
302pub type ThemeUpdate = OptionalUpdate<String>;
303pub type TitleUpdate = OptionalUpdate<String>;
304pub type VersionUpdate = OptionalUpdate<String>;
305pub type ColumnConfigUpdate = OptionalUpdate<HashMap<String, ColumnConfigValues>>;
306
307/// Handles `{}` when included as a field with `#[serde(default)]`.
308impl<T: Clone> Default for OptionalUpdate<T> {
309    fn default() -> Self {
310        Self::Missing
311    }
312}
313
314/// Handles `{plugin: null}` and `{plugin: val}` by treating this type as an
315/// option.
316impl<T: Clone> From<Option<T>> for OptionalUpdate<T> {
317    fn from(opt: Option<T>) -> Self {
318        match opt {
319            Some(v) => Self::Update(v),
320            None => Self::SetDefault,
321        }
322    }
323}
324
325/// Treats `PluginUpdate` enum as an `Option<T>` when present during
326/// deserialization.
327impl<'a, T> Deserialize<'a> for OptionalUpdate<T>
328where
329    T: Deserialize<'a> + Clone,
330{
331    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
332    where
333        D: Deserializer<'a>,
334    {
335        Option::deserialize(deserializer).map(Into::into)
336    }
337}