use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use super::{ChartSpec, validate_text};
use crate::error::{ChartError, ChartWarning};
const MAX_COLUMNS: usize = 6;
const MAX_CARDS: usize = 20;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct KanbanSpec {
pub columns: Vec<KanbanColumnSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct KanbanColumnSpec {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cards: Vec<KanbanCardSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct KanbanCardSpec {
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub blocked: bool,
}
impl KanbanColumnSpec {
pub(crate) fn exceeds_limit(&self) -> bool {
self.limit
.is_some_and(|limit| self.cards.len() > usize::try_from(limit).unwrap_or(usize::MAX))
}
}
impl ChartSpec {
pub(super) fn validate_kanban(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.reject_chart_fields("a kanban board is drawn from its columns and cards")?;
let Some(kanban) = &self.kanban else {
return Err(ChartError::new(
"missing_kanban",
"/kanban",
"a kanban board requires a kanban block",
));
};
if kanban.columns.is_empty() {
return Err(ChartError::new(
"empty_data",
"/kanban/columns",
"provide at least one column",
));
}
if kanban.columns.len() > MAX_COLUMNS {
return Err(ChartError::new(
"too_many_nodes",
"/kanban/columns",
format!("at most {MAX_COLUMNS} columns are supported; merge states"),
));
}
let mut labels = BTreeSet::new();
for (index, column) in kanban.columns.iter().enumerate() {
let path = format!("/kanban/columns/{index}");
validate_text(&column.label, &format!("{path}/label"), 60)?;
if !labels.insert(column.label.as_str()) {
return Err(ChartError::new(
"duplicate_label",
format!("{path}/label"),
"column labels must be unique",
));
}
if column.limit == Some(0) {
return Err(ChartError::new(
"invalid_limit",
format!("{path}/limit"),
"a limit is at least 1; leave it out for a column without one",
));
}
if column.cards.len() > MAX_CARDS {
return Err(ChartError::new(
"too_many_nodes",
format!("{path}/cards"),
format!("at most {MAX_CARDS} cards per column are supported; split the board"),
));
}
for (card_index, card) in column.cards.iter().enumerate() {
let card_path = format!("{path}/cards/{card_index}");
validate_text(&card.title, &format!("{card_path}/title"), 100)?;
for (name, value, length) in [("owner", &card.owner, 40), ("tag", &card.tag, 30)] {
if let Some(value) = value {
validate_text(value, &format!("{card_path}/{name}"), length)?;
}
}
}
}
Ok(Vec::new())
}
}