1use std::collections::hash_map::DefaultHasher;
6use std::hash::{Hash, Hasher};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct TypeFingerprint {
11 pub type_name: String,
13
14 pub structure_hash: u64,
16
17 pub fields: Vec<String>,
19
20 pub field_types: Vec<String>,
22}
23
24impl TypeFingerprint {
25 pub fn new(type_name: &str, fields: Vec<(String, String)>) -> Self {
27 let mut hasher = DefaultHasher::new();
28 type_name.hash(&mut hasher);
29 for (name, ty) in &fields {
30 name.hash(&mut hasher);
31 ty.hash(&mut hasher);
32 }
33
34 let (field_names, field_types): (Vec<_>, Vec<_>) = fields.into_iter().unzip();
35
36 Self {
37 type_name: type_name.to_string(),
38 structure_hash: hasher.finish(),
39 fields: field_names,
40 field_types,
41 }
42 }
43
44 pub fn primitive(type_name: &str) -> Self {
46 let mut hasher = DefaultHasher::new();
47 type_name.hash(&mut hasher);
48
49 Self {
50 type_name: type_name.to_string(),
51 structure_hash: hasher.finish(),
52 fields: Vec::new(),
53 field_types: Vec::new(),
54 }
55 }
56
57 pub fn compare(&self, other: &TypeFingerprint) -> SchemaChange {
59 if self.type_name != other.type_name {
60 return SchemaChange::TypeRenamed {
61 old: self.type_name.clone(),
62 new: other.type_name.clone(),
63 };
64 }
65
66 if self.structure_hash == other.structure_hash {
67 return SchemaChange::None;
68 }
69
70 let old_fields: std::collections::HashSet<_> = self.fields.iter().collect();
72 let new_fields: std::collections::HashSet<_> = other.fields.iter().collect();
73
74 let added: Vec<_> = new_fields
75 .difference(&old_fields)
76 .cloned()
77 .cloned()
78 .collect();
79 let removed: Vec<_> = old_fields
80 .difference(&new_fields)
81 .cloned()
82 .cloned()
83 .collect();
84
85 let mut type_changes = Vec::new();
87 for (i, field) in self.fields.iter().enumerate() {
88 if let Some(new_idx) = other.fields.iter().position(|f| f == field)
89 && self.field_types.get(i) != other.field_types.get(new_idx)
90 {
91 type_changes.push((
92 field.clone(),
93 self.field_types.get(i).cloned().unwrap_or_default(),
94 other.field_types.get(new_idx).cloned().unwrap_or_default(),
95 ));
96 }
97 }
98
99 if !removed.is_empty() || !type_changes.is_empty() {
101 SchemaChange::Breaking {
102 added,
103 removed,
104 type_changes,
105 }
106 } else if !added.is_empty() {
107 SchemaChange::Additive { added }
108 } else {
109 SchemaChange::Breaking {
111 added: Vec::new(),
112 removed: Vec::new(),
113 type_changes: Vec::new(),
114 }
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum SchemaChange {
122 None,
124
125 Additive { added: Vec<String> },
127
128 Breaking {
130 added: Vec<String>,
131 removed: Vec<String>,
132 type_changes: Vec<(String, String, String)>, },
134
135 TypeRenamed { old: String, new: String },
137}
138
139impl SchemaChange {
140 pub fn is_breaking(&self) -> bool {
142 matches!(
143 self,
144 SchemaChange::Breaking { .. } | SchemaChange::TypeRenamed { .. }
145 )
146 }
147
148 pub fn description(&self) -> String {
150 match self {
151 SchemaChange::None => "No changes".to_string(),
152 SchemaChange::Additive { added } => {
153 format!("Added fields: {}", added.join(", "))
154 }
155 SchemaChange::Breaking {
156 added,
157 removed,
158 type_changes,
159 } => {
160 let mut parts = Vec::new();
161 if !added.is_empty() {
162 parts.push(format!("added: {}", added.join(", ")));
163 }
164 if !removed.is_empty() {
165 parts.push(format!("removed: {}", removed.join(", ")));
166 }
167 for (field, old, new) in type_changes {
168 parts.push(format!("{}: {} -> {}", field, old, new));
169 }
170 format!("Breaking changes: {}", parts.join("; "))
171 }
172 SchemaChange::TypeRenamed { old, new } => {
173 format!("Type renamed: {} -> {}", old, new)
174 }
175 }
176 }
177}
178
179#[allow(dead_code)]
185pub fn fingerprint_from_struct(item: &syn::ItemStruct) -> TypeFingerprint {
186 let type_name = item.ident.to_string();
187
188 let fields: Vec<(String, String)> = match &item.fields {
189 syn::Fields::Named(named) => named
190 .named
191 .iter()
192 .map(|f| {
193 let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
194 let ty = quote::quote!(#f.ty).to_string();
195 (name, ty)
196 })
197 .collect(),
198 syn::Fields::Unnamed(unnamed) => unnamed
199 .unnamed
200 .iter()
201 .enumerate()
202 .map(|(i, f)| {
203 let ty = quote::quote!(#f.ty).to_string();
204 (format!("{}", i), ty)
205 })
206 .collect(),
207 syn::Fields::Unit => Vec::new(),
208 };
209
210 TypeFingerprint::new(&type_name, fields)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_fingerprint_equality() {
219 let fp1 = TypeFingerprint::new(
220 "TestStruct",
221 vec![
222 ("x".to_string(), "i32".to_string()),
223 ("y".to_string(), "String".to_string()),
224 ],
225 );
226
227 let fp2 = TypeFingerprint::new(
228 "TestStruct",
229 vec![
230 ("x".to_string(), "i32".to_string()),
231 ("y".to_string(), "String".to_string()),
232 ],
233 );
234
235 assert_eq!(fp1, fp2);
236 assert_eq!(fp1.compare(&fp2), SchemaChange::None);
237 }
238
239 #[test]
240 fn test_additive_change() {
241 let fp1 = TypeFingerprint::new("TestStruct", vec![("x".to_string(), "i32".to_string())]);
242
243 let fp2 = TypeFingerprint::new(
244 "TestStruct",
245 vec![
246 ("x".to_string(), "i32".to_string()),
247 ("y".to_string(), "String".to_string()),
248 ],
249 );
250
251 let change = fp1.compare(&fp2);
252 assert!(!change.is_breaking());
253 match change {
254 SchemaChange::Additive { added } => {
255 assert_eq!(added, vec!["y".to_string()]);
256 }
257 _ => panic!("Expected Additive change"),
258 }
259 }
260
261 #[test]
262 fn test_breaking_removal() {
263 let fp1 = TypeFingerprint::new(
264 "TestStruct",
265 vec![
266 ("x".to_string(), "i32".to_string()),
267 ("y".to_string(), "String".to_string()),
268 ],
269 );
270
271 let fp2 = TypeFingerprint::new("TestStruct", vec![("x".to_string(), "i32".to_string())]);
272
273 let change = fp1.compare(&fp2);
274 assert!(change.is_breaking());
275 }
276
277 #[test]
278 fn test_breaking_type_change() {
279 let fp1 = TypeFingerprint::new("TestStruct", vec![("x".to_string(), "i32".to_string())]);
280
281 let fp2 = TypeFingerprint::new("TestStruct", vec![("x".to_string(), "i64".to_string())]);
282
283 let change = fp1.compare(&fp2);
284 assert!(change.is_breaking());
285 match change {
286 SchemaChange::Breaking { type_changes, .. } => {
287 assert_eq!(type_changes.len(), 1);
288 assert_eq!(type_changes[0].0, "x");
289 }
290 _ => panic!("Expected Breaking change"),
291 }
292 }
293
294 #[test]
295 fn test_type_renamed() {
296 let fp1 = TypeFingerprint::new("OldName", vec![]);
297 let fp2 = TypeFingerprint::new("NewName", vec![]);
298
299 let change = fp1.compare(&fp2);
300 assert!(change.is_breaking());
301 match change {
302 SchemaChange::TypeRenamed { old, new } => {
303 assert_eq!(old, "OldName");
304 assert_eq!(new, "NewName");
305 }
306 _ => panic!("Expected TypeRenamed"),
307 }
308 }
309
310 #[test]
311 fn test_primitive_fingerprint() {
312 let fp1 = TypeFingerprint::primitive("i32");
313 let fp2 = TypeFingerprint::primitive("i32");
314 let fp3 = TypeFingerprint::primitive("i64");
315
316 assert_eq!(fp1.compare(&fp2), SchemaChange::None);
317 assert!(fp1.compare(&fp3).is_breaking());
318 }
319
320 #[test]
321 fn test_fingerprint_from_syn() {
322 let code = "struct Point { x: f64, y: f64 }";
323 let item: syn::ItemStruct = syn::parse_str(code).unwrap();
324
325 let fp = fingerprint_from_struct(&item);
326 assert_eq!(fp.type_name, "Point");
327 assert_eq!(fp.fields, vec!["x", "y"]);
328 }
329}