Skip to main content

wasm4pm_compat/
dfg.rs

1pub use crate::models::{DFG, DFGNode, DFGEdge};
2
3// ── Van der Aalst-grounded DFG types (camelCase, OCEL-compatible) ────────────
4
5/// A single node in a Directly-Follows Graph — one activity class.
6#[derive(Debug, Clone)]
7pub struct DfgNode {
8    activity: String,
9}
10
11impl DfgNode {
12    pub fn new(activity: &str) -> Self { DfgNode { activity: activity.to_owned() } }
13    pub fn activity(&self) -> &str { &self.activity }
14}
15
16/// The weight of a DFG edge — observed co-occurrence count.
17#[derive(Debug, Clone)]
18pub struct DfgWeight {
19    count: u32,
20}
21
22impl DfgWeight {
23    pub fn count(&self) -> u32 { self.count }
24}
25
26/// A directed edge in a Directly-Follows Graph.
27#[derive(Debug, Clone)]
28pub struct DfgEdge {
29    source: String,
30    target: String,
31    weight: DfgWeight,
32}
33
34impl DfgEdge {
35    pub fn new(source: &str, target: &str, count: u32) -> Self {
36        DfgEdge {
37            source: source.to_owned(),
38            target: target.to_owned(),
39            weight: DfgWeight { count },
40        }
41    }
42    pub fn source(&self) -> &str { &self.source }
43    pub fn target(&self) -> &str { &self.target }
44    pub fn weight(&self) -> &DfgWeight { &self.weight }
45}
46
47/// A Directly-Follows Graph — the minimal process evidence structure
48/// per van der Aalst's process mining theory.
49#[derive(Debug, Clone)]
50pub struct Dfg {
51    nodes: Vec<DfgNode>,
52    edges: Vec<DfgEdge>,
53}
54
55impl Dfg {
56    pub fn new(
57        nodes: impl IntoIterator<Item = DfgNode>,
58        edges: impl IntoIterator<Item = DfgEdge>,
59    ) -> Self {
60        Dfg { nodes: nodes.into_iter().collect(), edges: edges.into_iter().collect() }
61    }
62
63    pub fn nodes(&self) -> &[DfgNode] { &self.nodes }
64    pub fn edges(&self) -> &[DfgEdge] { &self.edges }
65
66    #[must_use]
67    pub fn validate(&self) -> Result<(), DfgRefusal> {
68        if self.nodes.is_empty() {
69            return Err(DfgRefusal::EmptyGraph);
70        }
71        let activities: std::collections::HashSet<&str> =
72            self.nodes.iter().map(|n| n.activity.as_str()).collect();
73        for edge in &self.edges {
74            if !activities.contains(edge.source.as_str()) || !activities.contains(edge.target.as_str()) {
75                return Err(DfgRefusal::DanglingEdge);
76            }
77        }
78        Ok(())
79    }
80}
81
82/// Named refusal variants for DFG validation laws.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum DfgRefusal {
85    /// An edge references an activity not present in the node set.
86    DanglingEdge,
87    /// The DFG has no nodes — an empty graph cannot represent process behaviour.
88    EmptyGraph,
89}
90
91impl std::fmt::Display for DfgRefusal {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        match self {
94            DfgRefusal::DanglingEdge => write!(f, "DanglingEdge"),
95            DfgRefusal::EmptyGraph => write!(f, "EmptyGraph"),
96        }
97    }
98}
99
100impl std::error::Error for DfgRefusal {}