use std::collections::BTreeMap;
use perspective_client::config::{Filter, ViewConfig};
use crate::config::{OptionalUpdate, PanelViewerConfig, ViewerConfigInitial};
use crate::utils::CssKind;
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
pub struct DerivedTableConfig<T: ts_rs::TS = String> {
pub table: T,
#[serde(flatten)]
pub view_config: ViewConfig,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
#[serde(untagged)]
pub enum TableRef {
Name(String),
Derived(Box<DerivedTableConfig<TableRef>>),
}
impl ts_rs::TS for TableRef {
type OptionInnerType = Self;
type WithoutGenerics = Self;
fn docs() -> Option<String> {
Some(
"/**\n * What may occupy a `table` position in a single-panel config: a hosted\n * \
`Table`'s name, or an inline derived-table definition recursing through\n * its own \
`table` field until a concrete name.\n */\n"
.to_owned(),
)
}
fn name() -> String {
"TableRef".to_owned()
}
fn decl() -> String {
format!(
"type TableRef = string | (Omit<{}, \"table\"> & {{ table: TableRef }});",
<DerivedTableConfig as ts_rs::TS>::ident()
)
}
fn decl_concrete() -> String {
<Self as ts_rs::TS>::decl()
}
fn inline() -> String {
format!(
"string | (Omit<{}, \"table\"> & {{ table: TableRef }})",
<DerivedTableConfig as ts_rs::TS>::ident()
)
}
fn inline_flattened() -> String {
panic!("`TableRef` cannot be flattened")
}
fn visit_dependencies(visitor: &mut impl ts_rs::TypeVisitor)
where
Self: 'static,
{
visitor.visit::<DerivedTableConfig>();
}
fn output_path() -> Option<std::path::PathBuf> {
Some(std::path::PathBuf::from("TableRef.ts"))
}
}
impl Default for TableRef {
fn default() -> Self {
Self::Name(String::default())
}
}
impl From<String> for TableRef {
fn from(value: String) -> Self {
Self::Name(value)
}
}
impl TableRef {
pub fn name(&self) -> Result<&str, perspective_js::utils::ApiError> {
match self {
Self::Name(name) => Ok(name),
Self::Derived(_) => Err(perspective_js::utils::ApiError::new(
"Unresolved inline derived table",
)),
}
}
}
impl<'de> serde::Deserialize<'de> for TableRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct TableRefVisitor;
impl<'de> serde::de::Visitor<'de> for TableRefVisitor {
type Value = TableRef;
fn expecting(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.write_str("a table name or an inline derived table definition")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(TableRef::Name(value.to_owned()))
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let config = <DerivedTableConfig<TableRef> as serde::Deserialize>::deserialize(
serde::de::value::MapAccessDeserializer::new(map),
)?;
Ok(TableRef::Derived(Box::new(config)))
}
}
deserializer.deserialize_any(TableRefVisitor)
}
}
#[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<String, String>>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub derived_tables: BTreeMap<String, DerivedTableConfig>,
#[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(as = "Option<_>")]
#[ts(optional)]
pub active: OptionalUpdate<String>,
#[serde(default)]
#[ts(optional)]
pub layout: Option<crate::js::Layout>,
#[serde(default)]
#[ts(optional)]
pub panels: Option<BTreeMap<String, ViewerConfigInitial>>,
#[serde(default)]
#[ts(optional)]
pub derived_tables: Option<BTreeMap<String, DerivedTableConfig>>,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub global_filters: OptionalUpdate<Vec<Filter>>,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub masters: OptionalUpdate<Vec<String>>,
#[serde(default)]
#[ts(as = "Option<_>")]
#[ts(optional)]
pub palette: OptionalUpdate<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()
}