1use sha2::{Digest, Sha256};
4
5use crate::schema::Schema;
6
7impl Schema {
8 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 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 pub fn equal(&self, other: &Schema) -> bool {
80 self.cid() == other.cid()
81 }
82
83 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 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 assert_eq!(s1.identity_hash(), s2.identity_hash());
131 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}