perspective_viewer/config/
viewer_config.rs1use 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#[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
70type 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 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
153impl 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)]
184pub 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 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 Ok(self.clone())
269 }
270}
271
272#[derive(Clone, Debug, Serialize, TS)]
273#[serde(untagged)]
274pub enum OptionalUpdate<T: Clone> {
276 #[ts(skip)]
277 SetDefault,
278
279 Missing,
282
283 Update(T),
286}
287
288pub 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
307impl<T: Clone> Default for OptionalUpdate<T> {
309 fn default() -> Self {
310 Self::Missing
311 }
312}
313
314impl<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
325impl<'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}