pub mod liveness;
pub mod structural;
use serde::{Deserialize, Serialize};
use crate::error::{Result, ScxmlError};
use crate::model::{Statechart, TransitionType};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationReport {
pub valid: bool,
pub errors: Vec<String>,
pub state_count: usize,
pub transition_count: usize,
pub chart_name: Option<String>,
pub chart_initial: String,
pub crate_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_sha256: Option<String>,
}
pub fn validate_report(chart: &Statechart) -> ValidationReport {
validate_report_with_hash(chart, None)
}
pub fn validate_report_with_hash(
chart: &Statechart,
input_sha256: Option<String>,
) -> ValidationReport {
let errors = validate_all(chart);
let stats = crate::stats::stats(chart);
ValidationReport {
valid: errors.is_empty(),
errors: errors.iter().map(|e| e.to_string()).collect(),
state_count: stats.total_states,
transition_count: stats.total_transitions,
chart_name: chart.name.as_ref().map(|n| n.to_string()),
chart_initial: chart.initial.to_string(),
crate_version: env!("CARGO_PKG_VERSION").to_string(),
input_sha256,
}
}
pub fn validate(chart: &Statechart) -> Result<()> {
structural::validate_structure(chart)?;
let index = crate::index::StateIndex::new(chart);
liveness::validate_liveness_with_index(chart, index.state_count(), &index)?;
validate_semantics(chart)?;
Ok(())
}
pub fn validate_all(chart: &Statechart) -> Vec<ScxmlError> {
let mut errors = Vec::new();
errors.extend(structural::collect_structural_errors(chart));
let index = crate::index::StateIndex::new(chart);
if let Err(e) = liveness::validate_liveness_with_index(chart, index.state_count(), &index) {
errors.push(e);
}
errors.extend(collect_semantic_errors(chart));
errors
}
fn validate_semantics(chart: &Statechart) -> Result<()> {
for state in chart.iter_all_states() {
if state.id.is_empty() {
return Err(ScxmlError::Xml("state has empty id".into()));
}
for t in &state.transitions {
if let Some(delay) = &t.delay {
if !delay.starts_with('P') {
return Err(ScxmlError::Xml(format!(
"transition in state '{}' has invalid delay '{}' (must be ISO 8601 duration starting with 'P')",
state.id, delay
)));
}
}
if t.transition_type == TransitionType::Internal {
for target in &t.targets {
let is_descendant = state
.children
.iter()
.flat_map(|c| c.iter_all())
.any(|d| d.id == *target);
if !is_descendant {
return Err(ScxmlError::Xml(format!(
"internal transition in state '{}' targets '{}' which is not a descendant",
state.id, target
)));
}
}
}
if t.quorum == Some(0) {
return Err(ScxmlError::Xml(format!(
"transition in state '{}' has quorum=0 (must be >= 1)",
state.id
)));
}
}
let guardless: Vec<_> = state
.transitions
.iter()
.filter(|t| t.guard.is_none())
.collect();
for i in 0..guardless.len() {
for j in (i + 1)..guardless.len() {
if guardless[i].event == guardless[j].event {
let event_str = guardless[i].event.as_deref().unwrap_or("<eventless>");
return Err(ScxmlError::Xml(format!(
"state '{}' has conflicting guardless transitions on event '{}'",
state.id, event_str
)));
}
}
}
}
Ok(())
}
fn collect_semantic_errors(chart: &Statechart) -> Vec<ScxmlError> {
let mut errors = Vec::new();
for state in chart.iter_all_states() {
if state.id.is_empty() {
errors.push(ScxmlError::Xml("state has empty id".into()));
}
for t in &state.transitions {
if let Some(delay) = &t.delay {
if !delay.starts_with('P') {
errors.push(ScxmlError::Xml(format!(
"transition in state '{}' has invalid delay '{}' (must be ISO 8601 duration starting with 'P')",
state.id, delay
)));
}
}
if t.transition_type == TransitionType::Internal {
for target in &t.targets {
let is_descendant = state
.children
.iter()
.flat_map(|c| c.iter_all())
.any(|d| d.id == *target);
if !is_descendant {
errors.push(ScxmlError::Xml(format!(
"internal transition in state '{}' targets '{}' which is not a descendant",
state.id, target
)));
}
}
}
if t.quorum == Some(0) {
errors.push(ScxmlError::Xml(format!(
"transition in state '{}' has quorum=0 (must be >= 1)",
state.id
)));
}
}
let guardless: Vec<_> = state
.transitions
.iter()
.filter(|t| t.guard.is_none())
.collect();
for i in 0..guardless.len() {
for j in (i + 1)..guardless.len() {
if guardless[i].event == guardless[j].event {
let event_str = guardless[i].event.as_deref().unwrap_or("<eventless>");
errors.push(ScxmlError::Xml(format!(
"state '{}' has conflicting guardless transitions on event '{}'",
state.id, event_str
)));
}
}
}
}
errors
}