use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use super::{
ChartSpec, Dash, DiagramRole,
flow::{validate_role, validate_weight},
sequence::is_identifier,
validate_text,
};
use crate::error::{ChartError, ChartWarning};
pub(crate) const MAX_NODES: usize = 40;
pub(crate) const MAX_EDGES: usize = 120;
pub(crate) const MAX_GROUPS: usize = 8;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NetworkSpec {
pub nodes: Vec<NetworkNodeSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edges: Vec<NetworkEdgeSpec>,
#[serde(default, skip_serializing_if = "NetworkLayout::is_circle")]
pub layout: NetworkLayout,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub center: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub directed: bool,
#[serde(default, skip_serializing_if = "NetworkOrder::is_crossings")]
pub order: NetworkOrder,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NetworkNodeSpec {
pub id: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<DiagramRole>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NetworkEdgeSpec {
pub from: String,
pub to: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dash: Option<Dash>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weight: Option<f64>,
#[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 NetworkLayout {
#[default]
Circle,
Rings,
}
impl NetworkLayout {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_circle(&self) -> bool {
matches!(self, Self::Circle)
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NetworkOrder {
#[default]
Crossings,
Listed,
}
impl NetworkOrder {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_crossings(&self) -> bool {
matches!(self, Self::Crossings)
}
}
impl NetworkSpec {
pub(crate) fn node(&self, id: &str) -> Option<usize> {
self.nodes.iter().position(|node| node.id == id)
}
pub(crate) fn ends(&self) -> Vec<(usize, usize)> {
self.edges
.iter()
.map(|edge| {
(
self.node(&edge.from).expect("validated edges name nodes"),
self.node(&edge.to).expect("validated edges name nodes"),
)
})
.collect()
}
pub(crate) fn groups(&self) -> Vec<&str> {
let mut groups: Vec<&str> = Vec::new();
for group in self.nodes.iter().filter_map(|node| node.group.as_deref()) {
if !groups.contains(&group) {
groups.push(group);
}
}
groups
}
pub(crate) fn group_of(&self, node: usize) -> Option<usize> {
let group = self.nodes[node].group.as_deref()?;
self.groups().iter().position(|known| *known == group)
}
}
impl ChartSpec {
pub(super) fn validate_network(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.reject_map_options("a network", "network.nodes")?;
let Some(network) = &self.network else {
return Err(ChartError::new(
"missing_network",
"/network",
"a network requires a network block",
));
};
if network.nodes.is_empty() {
return Err(ChartError::new(
"empty_data",
"/network/nodes",
"provide at least one node",
));
}
if network.nodes.len() > MAX_NODES {
return Err(ChartError::new(
"too_many_nodes",
"/network/nodes",
format!("at most {MAX_NODES} nodes are supported"),
));
}
if network.edges.len() > MAX_EDGES {
return Err(ChartError::new(
"too_many_edges",
"/network/edges",
format!("at most {MAX_EDGES} edges are supported"),
));
}
network.validate_nodes()?;
network.validate_edges()?;
network.validate_layout()?;
Ok(Vec::new())
}
}
impl NetworkSpec {
fn validate_nodes(&self) -> Result<(), ChartError> {
let mut ids = BTreeSet::new();
let mut groups = Vec::new();
for (index, node) in self.nodes.iter().enumerate() {
let path = format!("/network/nodes/{index}");
if !is_identifier(&node.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(node.id.as_str()) {
return Err(ChartError::new(
"duplicate_id",
format!("{path}/id"),
format!("the id \"{}\" is already taken by an earlier node", node.id),
));
}
validate_text(&node.label, &format!("{path}/label"), 60)?;
if let Some(group) = &node.group {
validate_text(group, &format!("{path}/group"), 40)?;
if !groups.contains(group) {
groups.push(group.clone());
}
if groups.len() > MAX_GROUPS {
return Err(ChartError::new(
"too_many_groups",
format!("{path}/group"),
format!("at most {MAX_GROUPS} groups are supported"),
));
}
}
}
Ok(())
}
fn validate_edges(&self) -> Result<(), ChartError> {
for (index, edge) in self.edges.iter().enumerate() {
let path = format!("/network/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 node has the id \"{id}\""),
));
}
}
if edge.from == edge.to {
return Err(ChartError::new(
"self_loop",
format!("{path}/to"),
"an edge of a network joins two different nodes",
));
}
validate_weight(edge.weight, &path)?;
validate_role(edge.role, edge.dash, &path)?;
}
Ok(())
}
fn validate_layout(&self) -> Result<(), ChartError> {
match (self.layout, &self.center) {
(NetworkLayout::Rings, None) => Err(ChartError::new(
"missing_center",
"/network/center",
"rings need a center: the id of the node in the middle",
)),
(NetworkLayout::Rings, Some(center)) if self.node(center).is_none() => {
Err(ChartError::new(
"unknown_node",
"/network/center",
format!("no node has the id \"{center}\""),
))
}
(NetworkLayout::Rings, Some(_)) if self.order != NetworkOrder::Crossings => {
Err(ChartError::new(
"option_not_supported",
"/network/order",
"rings order their nodes by the ring before; order belongs to a circle",
))
}
(NetworkLayout::Circle, Some(_)) => Err(ChartError::new(
"option_not_supported",
"/network/center",
"a circle has no center node; use \"layout\": \"rings\"",
)),
_ => Ok(()),
}
}
}