use std::fmt;
#[derive(Debug, Clone)]
pub enum BuildError {
DuplicateNode { id: String },
MissingNode { from: String, to: String },
MissingEntryPoint,
MissingExitPoint,
InvalidEdgeDefinition {
from: String,
to: String,
reason: String,
},
InvalidFallback { node: String, reason: String },
}
impl fmt::Display for BuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DuplicateNode { id } => write!(f, "duplicate node id: '{}'", id),
Self::MissingNode { from, to } => {
write!(
f,
"edge references non-existent node: '{}' (in {}→{})",
to, from, to
)
}
Self::MissingEntryPoint => write!(f, "entry point not set"),
Self::MissingExitPoint => write!(f, "exit point not set"),
Self::InvalidEdgeDefinition { from, to, reason } => {
write!(f, "invalid edge {}→{}: {}", from, to, reason)
}
Self::InvalidFallback { node, reason } => {
write!(f, "invalid fallback for node '{}': {}", node, reason)
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct BuildErrors(pub Vec<BuildError>);
impl BuildErrors {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn push(&mut self, e: BuildError) {
self.0.push(e);
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> impl Iterator<Item = &BuildError> {
self.0.iter()
}
}
impl fmt::Display for BuildErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
write!(f, "no errors")
} else {
writeln!(f, "{} error(s):", self.0.len())?;
for e in &self.0 {
writeln!(f, " - {}", e)?;
}
Ok(())
}
}
}
impl std::error::Error for BuildError {}
impl std::error::Error for BuildErrors {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Info,
Warning,
}
impl fmt::Display for DiagnosticSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Info => write!(f, "info"),
Self::Warning => write!(f, "warning"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticCategory {
Cycle,
FallbackInCycle,
Unreachable,
ConditionOverlap,
EndNodeOutgoing,
Other,
}
impl std::fmt::Display for DiagnosticCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Cycle => write!(f, "cycle"),
Self::FallbackInCycle => write!(f, "fallback-in-cycle"),
Self::Unreachable => write!(f, "unreachable"),
Self::ConditionOverlap => write!(f, "condition-overlap"),
Self::EndNodeOutgoing => write!(f, "end-node-outgoing"),
Self::Other => write!(f, "other"),
}
}
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub category: DiagnosticCategory,
pub message: String,
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] ({}): {}",
self.severity, self.category, self.message
)
}
}
#[derive(Debug, Clone, Default)]
pub struct GraphDiagnostics {
pub warnings: Vec<Diagnostic>,
pub infos: Vec<Diagnostic>,
}
impl GraphDiagnostics {
pub fn new() -> Self {
Self::default()
}
pub fn add_warning(&mut self, category: DiagnosticCategory, message: impl Into<String>) {
self.warnings.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category,
message: message.into(),
});
}
pub fn add_info(&mut self, category: DiagnosticCategory, message: impl Into<String>) {
self.infos.push(Diagnostic {
severity: DiagnosticSeverity::Info,
category,
message: message.into(),
});
}
pub fn is_empty(&self) -> bool {
self.warnings.is_empty() && self.infos.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
}
impl fmt::Display for GraphDiagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.warnings.is_empty() {
writeln!(f, "{} warning(s):", self.warnings.len())?;
for w in &self.warnings {
writeln!(f, " - {}", w)?;
}
}
if !self.infos.is_empty() {
writeln!(f, "{} info(s):", self.infos.len())?;
for i in &self.infos {
writeln!(f, " - {}", i)?;
}
}
if self.is_empty() {
write!(f, "no issues found")
} else {
Ok(())
}
}
}
#[derive(Debug)]
pub enum GraphError {
Terminal(TerminalError),
}
#[derive(Debug)]
pub enum TerminalError {
InvalidGraph(String),
NodeNotFound(String),
MissingEdge { from: String, to: String },
NodeExecutionFailed {
node: String,
source: Box<dyn std::error::Error + Send + Sync>,
},
StepsExceeded { limit: usize },
LoopLimitExceeded { limit: usize },
BarrierTimeout {
node: String,
timeout: std::time::Duration,
},
BarrierCancelled { node: String },
Unrouted {
node: String,
attempted_conditions: Vec<ConditionEval>,
},
StateError(String),
}
#[derive(Debug, Clone)]
pub enum ObservedError {
Warning { node: String, message: String },
Degraded { node: String, message: String },
PartialFailure {
node: String,
succeeded: usize,
failed: usize,
message: String,
},
}
impl fmt::Display for ObservedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Warning { node, message } => write!(f, "node '{}': {}", node, message),
Self::Degraded { node, message } => write!(f, "node '{}' degraded: {}", node, message),
Self::PartialFailure {
node,
succeeded,
failed,
message,
} => {
write!(
f,
"node '{}' partial: {}/{} ok, {}",
node,
succeeded,
succeeded + failed,
message
)
}
}
}
}
#[derive(Debug, Clone)]
pub struct ConditionEval {
pub edge: String,
pub condition: Option<String>,
pub matched: bool,
}
impl fmt::Display for GraphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Terminal(e) => write!(f, "[terminal] {}", e),
}
}
}
impl fmt::Display for TerminalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidGraph(msg) => write!(f, "invalid graph: {msg}"),
Self::NodeNotFound(name) => write!(f, "node not found: {name}"),
Self::MissingEdge { from, to } => {
write!(
f,
"goto '{}' from '{}' failed: no edge {}→{} exists",
to, from, from, to
)
}
Self::NodeExecutionFailed { node, source } => {
write!(f, "node '{node}' execution failed: {source}")
}
Self::StepsExceeded { limit } => {
write!(f, "step limit {limit} exceeded (potential infinite loop)")
}
Self::LoopLimitExceeded { limit } => write!(f, "loop limit exceeded: {limit}"),
Self::BarrierTimeout { node, timeout } => {
write!(f, "barrier '{node}' timed out after {timeout:?}")
}
Self::BarrierCancelled { node } => {
write!(
f,
"barrier '{node}' cancelled: consumer dropped the signal channel"
)
}
Self::Unrouted {
node,
attempted_conditions,
} => {
write!(f, "node '{}' has no matching outgoing edge", node)?;
if !attempted_conditions.is_empty() {
write!(f, ". evaluated: [")?;
for (i, ce) in attempted_conditions.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}={}", ce.edge, ce.matched)?;
}
write!(f, "]")?;
}
Ok(())
}
Self::StateError(msg) => write!(f, "state error: {msg}"),
}
}
}
impl std::error::Error for GraphError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Terminal(TerminalError::NodeExecutionFailed { source, .. }) => {
Some(source.as_ref())
}
Self::Terminal(_) => None,
}
}
}
impl std::error::Error for TerminalError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::NodeExecutionFailed { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}