use serde::{Deserialize, Serialize};
use super::{ChartSpec, Dash, DiagramOrientation, sequence::is_identifier, validate_text};
use crate::error::{ChartError, ChartWarning};
pub(crate) const MAX_NODES: usize = 40;
pub(crate) const MAX_EDGES: usize = 80;
pub(crate) const MAX_LANES: usize = 8;
pub(crate) const MAX_GROUPS: usize = 8;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FlowSpec {
pub nodes: Vec<FlowNodeSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edges: Vec<FlowEdgeSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lanes: Vec<LaneSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<GroupSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub main_path: Vec<String>,
#[serde(default, skip_serializing_if = "DiagramOrientation::is_auto")]
pub orientation: DiagramOrientation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FlowNodeSpec {
pub id: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sublabel: Option<String>,
#[serde(default, skip_serializing_if = "NodeKind::is_process")]
pub kind: NodeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lane: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NodeKind {
Start,
End,
#[default]
Process,
Decision,
Io,
Subprocess,
Store,
External,
}
impl NodeKind {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_process(&self) -> bool {
matches!(self, Self::Process)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FlowEdgeSpec {
pub from: String,
pub to: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dash: Option<Dash>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct LaneSpec {
pub id: String,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GroupSpec {
pub label: String,
pub nodes: Vec<String>,
}
impl FlowSpec {
pub(crate) fn node(&self, id: &str) -> Option<usize> {
self.nodes.iter().position(|node| node.id == id)
}
pub(crate) fn lane_of(&self, node: usize) -> Option<usize> {
let lane = self.nodes[node].lane.as_deref()?;
self.lanes.iter().position(|candidate| candidate.id == lane)
}
pub(crate) fn group_of(&self, node: usize) -> Option<usize> {
let id = &self.nodes[node].id;
self.groups
.iter()
.position(|group| group.nodes.iter().any(|member| member == id))
}
pub(crate) fn ends(&self) -> Vec<(usize, usize)> {
self.edges
.iter()
.map(|edge| {
(
self.node(&edge.from).expect("validated edges name steps"),
self.node(&edge.to).expect("validated edges name steps"),
)
})
.collect()
}
pub(crate) fn on_main_path(&self, index: usize) -> bool {
let edge = &self.edges[index];
self.main_path
.windows(2)
.any(|pair| pair[0] == edge.from && pair[1] == edge.to)
}
}
impl ChartSpec {
pub(super) fn validate_flow(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.reject_map_options("a flow chart", "flow.nodes")?;
let Some(flow) = &self.flow else {
return Err(ChartError::new(
"missing_flow",
"/flow",
"a flow chart requires a flow block",
));
};
flow.validate_lanes()?;
flow.validate_nodes()?;
flow.validate_edges()?;
flow.validate_groups()?;
flow.validate_main_path()?;
Ok(Vec::new())
}
}
fn validate_id<'a>(
id: &str,
earlier: impl Iterator<Item = &'a str>,
path: &str,
noun: &str,
) -> Result<(), ChartError> {
if !is_identifier(id) {
return Err(ChartError::new(
"invalid_id",
format!("{path}/id"),
"use 1–64 ASCII letters, digits, hyphens, or underscores, starting with a letter",
));
}
if earlier.into_iter().any(|other| other == id) {
return Err(ChartError::new(
"duplicate_id",
format!("{path}/id"),
format!("the id \"{id}\" is already taken by an earlier {noun}"),
));
}
Ok(())
}
impl FlowSpec {
fn validate_lanes(&self) -> Result<(), ChartError> {
if self.lanes.len() > MAX_LANES {
return Err(ChartError::new(
"too_many_lanes",
"/flow/lanes",
format!("at most {MAX_LANES} lanes are supported"),
));
}
for (index, lane) in self.lanes.iter().enumerate() {
let path = format!("/flow/lanes/{index}");
let earlier = self.lanes[..index].iter().map(|lane| lane.id.as_str());
validate_id(&lane.id, earlier, &path, "lane")?;
validate_text(&lane.label, &format!("{path}/label"), 40)?;
}
Ok(())
}
fn validate_nodes(&self) -> Result<(), ChartError> {
if self.nodes.is_empty() {
return Err(ChartError::new(
"empty_data",
"/flow/nodes",
"provide at least one step",
));
}
if self.nodes.len() > MAX_NODES {
return Err(ChartError::new(
"too_many_nodes",
"/flow/nodes",
format!("at most {MAX_NODES} steps are supported"),
));
}
for (index, node) in self.nodes.iter().enumerate() {
let path = format!("/flow/nodes/{index}");
let earlier = self.nodes[..index].iter().map(|node| node.id.as_str());
validate_id(&node.id, earlier, &path, "step")?;
validate_text(&node.label, &format!("{path}/label"), 60)?;
if let Some(sublabel) = &node.sublabel {
validate_text(sublabel, &format!("{path}/sublabel"), 60)?;
}
match (&node.lane, self.lanes.is_empty()) {
(None, false) => {
return Err(ChartError::new(
"missing_lane",
format!("{path}/lane"),
"the chart has lanes, so every step names the lane it belongs to",
));
}
(Some(_), true) => {
return Err(ChartError::new(
"unknown_lane",
format!("{path}/lane"),
"the chart declares no lanes; add them under flow.lanes",
));
}
(Some(lane), false) if !self.lanes.iter().any(|known| &known.id == lane) => {
return Err(ChartError::new(
"unknown_lane",
format!("{path}/lane"),
format!("no lane has the id \"{lane}\""),
));
}
_ => {}
}
}
Ok(())
}
fn validate_edges(&self) -> Result<(), ChartError> {
if self.edges.len() > MAX_EDGES {
return Err(ChartError::new(
"too_many_edges",
"/flow/edges",
format!("at most {MAX_EDGES} edges are supported"),
));
}
for (index, edge) in self.edges.iter().enumerate() {
let path = format!("/flow/edges/{index}");
for (field, id) in [("from", &edge.from), ("to", &edge.to)] {
if self.node(id).is_none() {
return Err(ChartError::new(
"unknown_node",
format!("{path}/{field}"),
format!("no step has the id \"{id}\""),
));
}
}
if let Some(label) = &edge.label {
validate_text(label, &format!("{path}/label"), 60)?;
}
}
Ok(())
}
fn validate_groups(&self) -> Result<(), ChartError> {
if self.groups.len() > MAX_GROUPS {
return Err(ChartError::new(
"too_many_groups",
"/flow/groups",
format!("at most {MAX_GROUPS} groups are supported"),
));
}
for (index, group) in self.groups.iter().enumerate() {
let path = format!("/flow/groups/{index}");
validate_text(&group.label, &format!("{path}/label"), 40)?;
if group.nodes.is_empty() {
return Err(ChartError::new(
"empty_data",
format!("{path}/nodes"),
"a group frames at least one step",
));
}
for (member_index, member) in group.nodes.iter().enumerate() {
let member_path = format!("{path}/nodes/{member_index}");
let Some(node) = self.node(member) else {
return Err(ChartError::new(
"unknown_node",
member_path,
format!("no step has the id \"{member}\""),
));
};
let taken = self.groups[..index]
.iter()
.any(|other| other.nodes.contains(member))
|| group.nodes[..member_index].contains(member);
if taken {
return Err(ChartError::new(
"duplicate_member",
member_path,
format!("the step \"{member}\" already belongs to a group"),
));
}
let first = self.node(&group.nodes[0]).expect("checked above");
if self.nodes[node].lane != self.nodes[first].lane {
return Err(ChartError::new(
"group_spans_lanes",
member_path,
"the steps of a group lie in one lane",
));
}
}
}
Ok(())
}
fn validate_main_path(&self) -> Result<(), ChartError> {
for (index, id) in self.main_path.iter().enumerate() {
if self.node(id).is_none() {
return Err(ChartError::new(
"unknown_node",
format!("/flow/mainPath/{index}"),
format!("no step has the id \"{id}\""),
));
}
}
for (index, pair) in self.main_path.windows(2).enumerate() {
let joined = self
.edges
.iter()
.any(|edge| edge.from == pair[0] && edge.to == pair[1]);
if !joined {
return Err(ChartError::new(
"main_path_gap",
format!("/flow/mainPath/{}", index + 1),
format!(
"no edge leads from \"{}\" to \"{}\"; the main path follows edges",
pair[0], pair[1]
),
));
}
}
Ok(())
}
}