use std::ops::Deref;
use std::sync::LazyLock;
use perspective_client::config::*;
use perspective_js::utils::*;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use ts_rs::TS;
use wasm_bindgen::prelude::*;
use crate::renderer::ColumnConfigMap;
#[derive(Debug, Default, Serialize, PartialEq, TS)]
#[serde(deny_unknown_fields)]
pub struct ViewerConfig<V: TS = String> {
pub settings: bool,
#[serde(flatten)]
pub panel: PanelViewerConfig<V>,
}
#[derive(Debug, Default, Serialize, PartialEq, TS)]
pub struct PanelViewerConfig<V: TS = String> {
pub version: V,
pub columns_config: ColumnConfigMap,
pub plugin: String,
pub plugin_config: serde_json::Map<String, Value>,
pub table: String,
pub theme: Option<String>,
pub title: Option<String>,
#[serde(flatten)]
pub view_config: ViewConfig,
}
impl<V: TS> Deref for ViewerConfig<V> {
type Target = PanelViewerConfig<V>;
fn deref(&self) -> &Self::Target {
&self.panel
}
}
pub static API_VERSION: LazyLock<&'static str> = LazyLock::new(|| {
#[derive(Deserialize)]
struct Package {
version: &'static str,
}
let pkg: &'static str = include_str!("../../../package.json");
let pkg: Package = serde_json::from_str(pkg).unwrap();
pkg.version
});
impl ViewerConfig {
pub fn encode(&self) -> ApiResult<JsValue> {
Ok(JsValue::from_serde_ext(self)?)
}
}
#[derive(Clone, Debug, TS, Deserialize, PartialEq, Serialize)]
#[serde(transparent)]
pub struct PluginConfig(serde_json::Value);
impl Deref for PluginConfig {
type Target = Value;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TS)]
pub struct ViewerConfigUpdate {
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub version: VersionUpdate,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub plugin: PluginUpdate,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub title: TitleUpdate,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub table: TableUpdate,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub theme: ThemeUpdate,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub settings: SettingsUpdate,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub plugin_config: PluginConfigUpdate,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub columns_config: ColumnConfigUpdate,
#[serde(flatten)]
pub view_config: ViewConfigUpdate,
}
impl ViewerConfigUpdate {
pub fn decode(update: &JsValue) -> ApiResult<Self> {
Ok(update.into_serde_ext()?)
}
pub fn migrate(&self) -> ApiResult<Self> {
Ok(self.clone())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
pub struct ViewerConfigInitial {
pub table: String,
#[ts(optional)]
pub version: Option<String>,
#[ts(optional)]
pub plugin: Option<String>,
#[ts(optional)]
pub title: Option<String>,
#[ts(optional)]
pub theme: Option<String>,
#[ts(optional)]
pub plugin_config: Option<serde_json::Map<String, Value>>,
#[ts(optional)]
pub columns_config: Option<ColumnConfigMap>,
#[serde(flatten)]
pub view_config: ViewConfigUpdate,
}
impl ViewerConfigInitial {
pub fn decode(config: &JsValue) -> ApiResult<Self> {
Ok(config.into_serde_ext()?)
}
pub fn new(table: impl Into<String>) -> Self {
Self {
table: table.into(),
version: None,
plugin: None,
title: None,
theme: None,
plugin_config: None,
columns_config: None,
view_config: ViewConfigUpdate::default(),
}
}
}
fn up<T: Clone>(value: Option<T>) -> OptionalUpdate<T> {
match value {
Some(value) => OptionalUpdate::Update(value),
None => OptionalUpdate::Missing,
}
}
impl From<ViewerConfigInitial> for ViewerConfigUpdate {
fn from(value: ViewerConfigInitial) -> Self {
ViewerConfigUpdate {
version: up(value.version),
plugin: up(value.plugin),
title: up(value.title),
table: OptionalUpdate::Update(value.table),
theme: up(value.theme),
settings: OptionalUpdate::Missing,
plugin_config: up(value.plugin_config),
columns_config: up(value.columns_config),
view_config: value.view_config,
}
}
}
impl std::fmt::Display for ViewerConfigUpdate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
serde_json::to_string(self).map_err(|_| std::fmt::Error)?
)
}
}
#[derive(Clone, Debug, Serialize, PartialEq, TS)]
#[serde(untagged)]
pub enum OptionalUpdate<T: Clone> {
#[ts(skip)]
SetDefault,
Missing,
Update(T),
}
pub type PluginUpdate = OptionalUpdate<String>;
pub type SettingsUpdate = OptionalUpdate<bool>;
pub type ThemeUpdate = OptionalUpdate<String>;
pub type TitleUpdate = OptionalUpdate<String>;
pub type TableUpdate = OptionalUpdate<String>;
pub type VersionUpdate = OptionalUpdate<String>;
pub type ColumnConfigUpdate = OptionalUpdate<ColumnConfigMap>;
pub type PluginConfigUpdate = OptionalUpdate<serde_json::Map<String, Value>>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_requires_a_table() {
let json = serde_json::json!({ "group_by": ["State"] });
let err = serde_json::from_value::<ViewerConfigInitial>(json).unwrap_err();
assert!(format!("{err}").contains("table"));
}
#[test]
fn initial_widens_to_an_update() {
let json = serde_json::json!({
"table": "superstore",
"plugin": "Datagrid",
"group_by": ["State"],
});
let initial: ViewerConfigInitial = serde_json::from_value(json).unwrap();
let update = ViewerConfigUpdate::from(initial);
assert!(matches!(&update.table, OptionalUpdate::Update(x) if x == "superstore"));
assert!(matches!(&update.settings, OptionalUpdate::Missing));
assert!(matches!(&update.plugin, OptionalUpdate::Update(x) if x == "Datagrid"));
assert_eq!(
update.view_config.group_by.as_deref(),
Some(&["State".to_owned()][..])
);
}
#[test]
fn a_table_less_update_is_representable() {
let json = serde_json::json!({ "group_by": ["State"] });
let update: ViewerConfigUpdate = serde_json::from_value(json).unwrap();
assert!(matches!(&update.table, OptionalUpdate::Missing));
}
}
impl<T: Clone> Default for OptionalUpdate<T> {
fn default() -> Self {
Self::Missing
}
}
impl<T: Clone> From<Option<T>> for OptionalUpdate<T> {
fn from(opt: Option<T>) -> Self {
match opt {
Some(v) => Self::Update(v),
None => Self::SetDefault,
}
}
}
impl<'a, T> Deserialize<'a> for OptionalUpdate<T>
where
T: Deserialize<'a> + Clone,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'a>,
{
Option::deserialize(deserializer).map(Into::into)
}
}