use std::collections::BTreeMap;
use perspective_client::config::Filter;
use crate::config::{CssKind, PanelViewerConfig, ViewerConfigInitial};
#[derive(serde::Serialize, ts_rs::TS)]
pub struct WorkspaceConfig {
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub active: Option<String>,
pub layout: Option<crate::js::Layout>,
pub panels: BTreeMap<String, PanelViewerConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub global_filters: Vec<Filter>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub masters: Vec<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub palette: BTreeMap<String, String>,
}
#[derive(serde::Deserialize, ts_rs::TS)]
pub struct WorkspaceConfigUpdate {
#[serde(default)]
#[ts(optional)]
pub active: Option<String>,
#[serde(default)]
#[ts(optional)]
pub layout: Option<crate::js::Layout>,
pub panels: BTreeMap<String, ViewerConfigInitial>,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub global_filters: Option<Vec<Filter>>,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub masters: Option<Vec<String>>,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub palette: Option<BTreeMap<String, String>>,
}
pub fn validate_palette(
palette: BTreeMap<String, String>,
) -> Result<BTreeMap<String, String>, String> {
palette
.into_iter()
.map(|(name, value)| {
let kind = CssKind::of_var(&name).ok_or_else(|| {
format!(
"`palette` key `{name}` must start with `--psp-user--gradient-`, \
`--psp-user--palette-` or `--psp-user--color-`"
)
})?;
let canonical = kind
.canonicalize(&value)
.map_err(|error| format!("`palette[\"{name}\"]`: {error}"))?;
Ok((name, canonical))
})
.collect()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn palette_serializes_only_when_present() {
let config = WorkspaceConfig {
version: "x".to_owned(),
active: None,
layout: None,
panels: BTreeMap::new(),
global_filters: vec![],
masters: vec![],
palette: BTreeMap::new(),
};
assert_eq!(
serde_json::to_value(&config).unwrap(),
json!({ "version": "x", "layout": null, "panels": {} })
);
let mut palette = BTreeMap::new();
palette.insert("--psp-user--color-hot".to_owned(), "#ff0000".to_owned());
let config = WorkspaceConfig { palette, ..config };
assert_eq!(
serde_json::to_value(&config).unwrap(),
json!({
"version": "x",
"layout": null,
"panels": {},
"palette": { "--psp-user--color-hot": "#ff0000" },
})
);
}
#[test]
fn update_palette_defaults_empty_and_validates_by_prefix() {
let update: WorkspaceConfigUpdate =
serde_json::from_value(json!({ "panels": {} })).unwrap();
assert!(update.palette.is_none());
let update: WorkspaceConfigUpdate = serde_json::from_value(json!({
"panels": {},
"palette": {
"--psp-user--gradient-1": "linear-gradient(#000, #fff)",
"--psp-user--palette-warm": "linear-gradient(90deg, RGB(255,0,0), #ff0)",
"--psp-user--color-hot": "#F00",
},
}))
.unwrap();
let valid = validate_palette(update.palette.unwrap()).unwrap();
assert_eq!(
valid.get("--psp-user--gradient-1").unwrap(),
"linear-gradient(to right, #000000 0%, #ffffff 100%)"
);
assert_eq!(
valid.get("--psp-user--palette-warm").unwrap(),
"linear-gradient(to right, #ff0000, #ffff00)"
);
assert_eq!(valid.get("--psp-user--color-hot").unwrap(), "#ff0000");
let bad = |name: &str, value: &str| {
let mut map = BTreeMap::new();
map.insert(name.to_owned(), value.to_owned());
validate_palette(map).unwrap_err()
};
assert!(bad("--psp-user--other-1", "#ff0000").contains("--psp-user--other-1"));
assert!(bad("--psp-charts--gradient", "#ff0000").contains("must start with"));
assert!(
bad(
"--psp-user--palette-1",
"linear-gradient(#000 0%, #fff 100%)"
)
.contains("--psp-user--palette-1")
);
assert!(bad("--psp-user--gradient-1", "#ff0000").contains("linear-gradient"));
assert!(bad("--psp-user--color-1", "red").contains("red"));
}
}