Skip to main content

pflow_tokenmodel/
cid.rs

1//! Content-addressed identity for schemas.
2
3use sha2::{Digest, Sha256};
4
5use crate::schema::Schema;
6
7impl Schema {
8    /// Computes the content-addressed identifier for this schema.
9    pub fn cid(&self) -> String {
10        let normalized = self.normalize();
11        match serde_json::to_string(&normalized) {
12            Ok(data) => {
13                let hash = Sha256::digest(data.as_bytes());
14                format!("cid:{}", hex::encode(hash))
15            }
16            Err(_) => String::new(),
17        }
18    }
19
20    /// Computes a structural fingerprint (ignoring name/version).
21    pub fn identity_hash(&self) -> String {
22        #[derive(serde::Serialize)]
23        struct Structural {
24            states: Vec<super::schema::State>,
25            actions: Vec<super::schema::Action>,
26            arcs: Vec<super::schema::Arc>,
27        }
28
29        let structural = Structural {
30            states: self.normalize_states(),
31            actions: self.normalize_actions(),
32            arcs: self.normalize_arcs(),
33        };
34
35        match serde_json::to_string(&structural) {
36            Ok(data) => {
37                let hash = Sha256::digest(data.as_bytes());
38                format!("idh:{}", hex::encode(&hash[..16]))
39            }
40            Err(_) => String::new(),
41        }
42    }
43
44    fn normalize(&self) -> Schema {
45        Schema {
46            name: self.name.clone(),
47            version: self.version.clone(),
48            states: self.normalize_states(),
49            actions: self.normalize_actions(),
50            arcs: self.normalize_arcs(),
51            constraints: Vec::new(),
52            events: Vec::new(),
53        }
54    }
55
56    fn normalize_states(&self) -> Vec<super::schema::State> {
57        let mut states = self.states.clone();
58        states.sort_by(|a, b| a.id.cmp(&b.id));
59        states
60    }
61
62    fn normalize_actions(&self) -> Vec<super::schema::Action> {
63        let mut actions = self.actions.clone();
64        actions.sort_by(|a, b| a.id.cmp(&b.id));
65        actions
66    }
67
68    fn normalize_arcs(&self) -> Vec<super::schema::Arc> {
69        let mut arcs = self.arcs.clone();
70        arcs.sort_by(|a, b| {
71            a.source
72                .cmp(&b.source)
73                .then_with(|| a.target.cmp(&b.target))
74        });
75        arcs
76    }
77
78    /// Returns true if two schemas have the same CID.
79    pub fn equal(&self, other: &Schema) -> bool {
80        self.cid() == other.cid()
81    }
82
83    /// Returns true if two schemas have the same structure.
84    pub fn structurally_equal(&self, other: &Schema) -> bool {
85        self.identity_hash() == other.identity_hash()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use crate::schema::*;
92
93    #[test]
94    fn test_cid_deterministic() {
95        let mut s1 = Schema::new("test");
96        s1.add_token_state("p1", 1);
97        s1.add_token_state("p2", 0);
98        s1.add_action(Action {
99            id: "t1".into(),
100            guard: String::new(),
101            event_id: String::new(),
102            event_bindings: None,
103        });
104
105        let mut s2 = Schema::new("test");
106        // Add in different order
107        s2.add_token_state("p2", 0);
108        s2.add_token_state("p1", 1);
109        s2.add_action(Action {
110            id: "t1".into(),
111            guard: String::new(),
112            event_id: String::new(),
113            event_bindings: None,
114        });
115
116        assert_eq!(s1.cid(), s2.cid());
117    }
118
119    #[test]
120    fn test_identity_hash() {
121        let mut s1 = Schema::new("name1");
122        s1.version = "v1".into();
123        s1.add_token_state("p1", 1);
124
125        let mut s2 = Schema::new("name2");
126        s2.version = "v2".into();
127        s2.add_token_state("p1", 1);
128
129        // Same structure, different metadata
130        assert_eq!(s1.identity_hash(), s2.identity_hash());
131        // Different CIDs due to different name/version
132        assert_ne!(s1.cid(), s2.cid());
133    }
134
135    #[test]
136    fn test_cid_not_empty() {
137        let mut s = Schema::new("test");
138        s.add_token_state("p1", 1);
139        let cid = s.cid();
140        assert!(cid.starts_with("cid:"));
141        assert!(cid.len() > 10);
142    }
143}