use serde::{Deserialize, Serialize};
use super::{ChartSpec, is_false, validate_text};
use crate::error::{ChartError, ChartWarning};
pub(crate) const MAX_PARTICIPANTS: usize = 12;
pub(crate) const MAX_MESSAGES: usize = 60;
pub(crate) const MAX_FRAGMENTS: usize = 12;
pub(crate) const MAX_FRAGMENT_DEPTH: usize = 3;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SequenceSpec {
pub participants: Vec<ParticipantSpec>,
pub messages: Vec<MessageSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fragments: Vec<FragmentSpec>,
#[serde(default, skip_serializing_if = "is_false")]
pub numbered: bool,
#[serde(default, skip_serializing_if = "DiagramOrientation::is_auto")]
pub orientation: DiagramOrientation,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DiagramOrientation {
#[default]
Auto,
Portrait,
Landscape,
}
impl DiagramOrientation {
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(super) const fn is_auto(&self) -> bool {
matches!(self, Self::Auto)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ParticipantSpec {
pub id: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sublabel: Option<String>,
#[serde(default, skip_serializing_if = "ParticipantKind::is_service")]
pub kind: ParticipantKind,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ParticipantKind {
Actor,
#[default]
Service,
Database,
Queue,
External,
}
impl ParticipantKind {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_service(&self) -> bool {
matches!(self, Self::Service)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MessageSpec {
pub from: String,
pub to: String,
pub label: String,
#[serde(default, skip_serializing_if = "MessageKind::is_call")]
pub kind: MessageKind,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MessageKind {
#[default]
Call,
Reply,
Async,
}
impl MessageKind {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_call(&self) -> bool {
matches!(self, Self::Call)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FragmentSpec {
pub kind: FragmentKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub from: usize,
pub to: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty", rename = "else")]
pub branches: Vec<BranchSpec>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum FragmentKind {
Alt,
Opt,
Loop,
Par,
Critical,
Break,
}
impl FragmentKind {
pub(crate) const fn keyword(self) -> &'static str {
match self {
Self::Alt => "alt",
Self::Opt => "opt",
Self::Loop => "loop",
Self::Par => "par",
Self::Critical => "critical",
Self::Break => "break",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BranchSpec {
pub from: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
impl SequenceSpec {
pub(crate) fn participant(&self, id: &str) -> Option<usize> {
self.participants
.iter()
.position(|participant| participant.id == id)
}
pub(crate) fn ends(&self) -> Vec<(usize, usize)> {
self.messages
.iter()
.map(|message| {
(
self.participant(&message.from)
.expect("validated messages name participants"),
self.participant(&message.to)
.expect("validated messages name participants"),
)
})
.collect()
}
pub(crate) fn depth(&self, index: usize) -> usize {
let fragment = &self.fragments[index];
self.fragments
.iter()
.enumerate()
.filter(|(other, outer)| {
*other != index
&& encloses(outer, fragment)
&& !(encloses(fragment, outer) && *other > index)
})
.count()
}
}
fn encloses(outer: &FragmentSpec, inner: &FragmentSpec) -> bool {
outer.from <= inner.from && inner.to <= outer.to
}
pub(super) fn is_identifier(text: &str) -> bool {
let mut characters = text.chars();
characters
.next()
.is_some_and(|character| character.is_ascii_alphabetic())
&& characters
.all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_'))
&& text.len() <= 64
}
impl ChartSpec {
pub(super) fn validate_sequence(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.reject_map_options("a sequence diagram", "sequence.messages")?;
let Some(sequence) = &self.sequence else {
return Err(ChartError::new(
"missing_sequence",
"/sequence",
"a sequence diagram requires a sequence block",
));
};
if sequence.participants.is_empty() {
return Err(ChartError::new(
"empty_data",
"/sequence/participants",
"provide at least one participant",
));
}
if sequence.participants.len() > MAX_PARTICIPANTS {
return Err(ChartError::new(
"too_many_participants",
"/sequence/participants",
format!("at most {MAX_PARTICIPANTS} participants are supported"),
));
}
for (index, participant) in sequence.participants.iter().enumerate() {
let path = format!("/sequence/participants/{index}");
if !is_identifier(&participant.id) {
return Err(ChartError::new(
"invalid_id",
format!("{path}/id"),
"use 1–64 ASCII letters, digits, hyphens, or underscores, starting with a letter",
));
}
if sequence.participants[..index]
.iter()
.any(|earlier| earlier.id == participant.id)
{
return Err(ChartError::new(
"duplicate_id",
format!("{path}/id"),
format!(
"the id \"{}\" is already taken by an earlier participant",
participant.id
),
));
}
validate_text(&participant.label, &format!("{path}/label"), 60)?;
if let Some(sublabel) = &participant.sublabel {
validate_text(sublabel, &format!("{path}/sublabel"), 60)?;
}
}
if sequence.messages.is_empty() {
return Err(ChartError::new(
"empty_data",
"/sequence/messages",
"provide at least one message",
));
}
if sequence.messages.len() > MAX_MESSAGES {
return Err(ChartError::new(
"too_many_messages",
"/sequence/messages",
format!("at most {MAX_MESSAGES} messages are supported"),
));
}
for (index, message) in sequence.messages.iter().enumerate() {
let path = format!("/sequence/messages/{index}");
for (field, id) in [("from", &message.from), ("to", &message.to)] {
if sequence.participant(id).is_none() {
return Err(ChartError::new(
"unknown_participant",
format!("{path}/{field}"),
format!("no participant has the id \"{id}\""),
));
}
}
validate_text(&message.label, &format!("{path}/label"), 80)?;
}
sequence.validate_fragments()?;
Ok(Vec::new())
}
}
impl SequenceSpec {
fn validate_fragments(&self) -> Result<(), ChartError> {
if self.fragments.len() > MAX_FRAGMENTS {
return Err(ChartError::new(
"too_many_fragments",
"/sequence/fragments",
format!("at most {MAX_FRAGMENTS} fragments are supported"),
));
}
let messages = self.messages.len();
for (index, fragment) in self.fragments.iter().enumerate() {
let path = format!("/sequence/fragments/{index}");
if fragment.to >= messages {
return Err(ChartError::new(
"invalid_fragment",
format!("{path}/to"),
format!("to must name a message, from 0 to {}", messages - 1),
));
}
if fragment.from > fragment.to {
return Err(ChartError::new(
"invalid_fragment",
format!("{path}/from"),
"from must not be greater than to",
));
}
if let Some(label) = &fragment.label {
validate_text(label, &format!("{path}/label"), 60)?;
}
if !fragment.branches.is_empty() && fragment.kind != FragmentKind::Alt {
return Err(ChartError::new(
"option_not_supported",
format!("{path}/else"),
"only an alt fragment has further branches",
));
}
let mut previous = fragment.from;
for (branch_index, branch) in fragment.branches.iter().enumerate() {
let branch_path = format!("{path}/else/{branch_index}");
if branch.from <= previous || branch.from > fragment.to {
return Err(ChartError::new(
"invalid_fragment",
format!("{branch_path}/from"),
"a branch starts at a later message than the one before it, inside the frame",
));
}
previous = branch.from;
if let Some(label) = &branch.label {
validate_text(label, &format!("{branch_path}/label"), 60)?;
}
}
for (other, earlier) in self.fragments[..index].iter().enumerate() {
let disjoint = earlier.to < fragment.from || fragment.to < earlier.from;
if !disjoint && !encloses(earlier, fragment) && !encloses(fragment, earlier) {
return Err(ChartError::new(
"fragments_cross",
format!("{path}/from"),
format!(
"this fragment overlaps fragment {other} without one enclosing the other"
),
));
}
}
if self.depth(index) >= MAX_FRAGMENT_DEPTH {
return Err(ChartError::new(
"invalid_fragment",
path,
format!("fragments nest at most {MAX_FRAGMENT_DEPTH} deep"),
));
}
}
Ok(())
}
}