use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use super::{ChartSpec, DiagramRole, sequence::is_identifier, validate_text};
use crate::error::{ChartError, ChartWarning};
pub(crate) const MAX_STACK: usize = 10;
pub(crate) const MAX_NESTED: usize = 6;
pub(crate) const MAX_ITEMS: usize = 12;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct LayersSpec {
pub layers: Vec<StackLayerSpec>,
#[serde(default, skip_serializing_if = "LayersDisplay::is_stack")]
pub display: LayersDisplay,
#[serde(default, skip_serializing_if = "LayersProjection::is_flat")]
pub projection: LayersProjection,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StackLayerSpec {
pub id: String,
pub label: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub items: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<DiagramRole>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LayersDisplay {
#[default]
Stack,
Nested,
}
impl LayersDisplay {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_stack(&self) -> bool {
matches!(self, Self::Stack)
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LayersProjection {
#[default]
Flat,
Exploded,
}
impl LayersProjection {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_flat(&self) -> bool {
matches!(self, Self::Flat)
}
}
impl ChartSpec {
pub(super) fn validate_layers(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.reject_map_options("a layer diagram", "layers.layers")?;
let Some(layers) = &self.layer_stack else {
return Err(ChartError::new(
"missing_layers",
"/layers",
"a layer diagram requires a layers block",
));
};
if layers.layers.is_empty() {
return Err(ChartError::new(
"empty_data",
"/layers/layers",
"provide at least one layer",
));
}
let most = match layers.display {
LayersDisplay::Stack => MAX_STACK,
LayersDisplay::Nested => MAX_NESTED,
};
if layers.layers.len() > most {
return Err(ChartError::new(
"too_many_layers",
"/layers/layers",
format!("at most {most} layers are supported in this display"),
));
}
if layers.projection == LayersProjection::Exploded
&& layers.display == LayersDisplay::Nested
{
return Err(ChartError::new(
"option_not_supported",
"/layers/projection",
"an exploded view lifts the layers of a stack apart; nested scopes have no stack to lift: use \"display\": \"stack\" or remove projection",
));
}
let mut ids = BTreeSet::new();
for (index, layer) in layers.layers.iter().enumerate() {
let path = format!("/layers/layers/{index}");
if !is_identifier(&layer.id) {
return Err(ChartError::new(
"invalid_id",
format!("{path}/id"),
"use 1–64 ASCII letters, digits, hyphens, or underscores, starting with a letter",
));
}
if !ids.insert(layer.id.as_str()) {
return Err(ChartError::new(
"duplicate_id",
format!("{path}/id"),
format!(
"the id \"{}\" is already taken by an earlier layer",
layer.id
),
));
}
validate_text(&layer.label, &format!("{path}/label"), 60)?;
if layer.items.len() > MAX_ITEMS {
return Err(ChartError::new(
"too_many_items",
format!("{path}/items"),
format!("at most {MAX_ITEMS} items fit a layer; group them or split the layer"),
));
}
for (item, text) in layer.items.iter().enumerate() {
validate_text(text, &format!("{path}/items/{item}"), 40)?;
}
if let Some(note) = &layer.note {
validate_text(note, &format!("{path}/note"), 120)?;
}
}
Ok(Vec::new())
}
}