rusty_schema_diff/analyzer/
protobuf.rs1use protobuf::descriptor::{FileDescriptorProto, DescriptorProto};
7use crate::analyzer::{SchemaAnalyzer, SchemaChange, ChangeType};
8use crate::{Schema, CompatibilityReport, MigrationPlan, ValidationResult, SchemaDiffError};
9use crate::error::Result;
10use crate::report::{CompatibilityIssue, IssueSeverity, ValidationError};
11use std::collections::HashMap;
12
13pub struct ProtobufAnalyzer;
15
16impl SchemaAnalyzer for ProtobufAnalyzer {
17 fn analyze_compatibility(&self, old: &Schema, new: &Schema) -> Result<CompatibilityReport> {
28 let old_desc = self.parse_proto(&old.content)?;
29 let new_desc = self.parse_proto(&new.content)?;
30
31 let mut changes = Vec::new();
32 self.compare_descriptors(&old_desc, &new_desc, "", &mut changes)?;
33
34 let compatibility_score = self.calculate_compatibility_score(&changes);
35 let is_compatible = compatibility_score >= 80;
36
37 Ok(CompatibilityReport {
38 compatibility_score: compatibility_score.try_into().unwrap(),
39 is_compatible,
40 changes: changes,
41 issues: vec![],
42 metadata: Default::default(),
43 })
44 }
45
46 fn generate_migration_path(&self, old: &Schema, new: &Schema) -> Result<MigrationPlan> {
57 let old_desc = self.parse_proto(&old.content)?;
58 let new_desc = self.parse_proto(&new.content)?;
59
60 let mut changes = Vec::new();
61 self.compare_descriptors(&old_desc, &new_desc, "", &mut changes)?;
62
63 Ok(MigrationPlan::new(
64 old.version.to_string(),
65 new.version.to_string(),
66 changes,
67 ))
68 }
69
70 fn validate_changes(&self, changes: &[SchemaChange]) -> Result<ValidationResult> {
71 let errors: Vec<ValidationError> = changes
72 .iter()
73 .filter_map(|change| {
74 self.validate_change(change).map(|issue| ValidationError {
75 message: issue.description,
76 path: issue.location,
77 code: format!("PROTO{}", match issue.severity {
78 IssueSeverity::Error => "001",
79 IssueSeverity::Warning => "002",
80 IssueSeverity::Info => "003",
81 }),
82 })
83 })
84 .collect();
85
86 Ok(ValidationResult {
87 is_valid: errors.is_empty(),
88 errors,
89 context: Default::default(),
90 })
91 }
92}
93
94impl ProtobufAnalyzer {
95 fn parse_proto(&self, content: &str) -> Result<FileDescriptorProto> {
97 match protobuf::text_format::parse_from_str(content) {
99 Ok(desc) => Ok(desc),
100 Err(e) => Err(SchemaDiffError::ProtobufError(e.to_string()))
101 }
102 }
103
104 fn compare_descriptors(
106 &self,
107 old: &FileDescriptorProto,
108 new: &FileDescriptorProto,
109 path: &str,
110 changes: &mut Vec<SchemaChange>,
111 ) -> Result<()> {
112 for old_msg in &old.message_type {
114 if let Some(new_msg) = new.message_type.iter().find(|m| m.name() == old_msg.name()) {
115 self.compare_messages(old_msg, new_msg, path, changes)?;
116 } else {
117 changes.push(SchemaChange {
118 change_type: ChangeType::Removal,
119 location: format!("{}/{}", path, old_msg.name()),
120 description: format!("Message '{}' was removed", old_msg.name()),
121 metadata: Default::default(),
122 });
123 }
124 }
125
126 for new_msg in &new.message_type {
128 if !old.message_type.iter().any(|m| m.name() == new_msg.name()) {
129 changes.push(SchemaChange {
130 change_type: ChangeType::Addition,
131 location: format!("{}/{}", path, new_msg.name()),
132 description: format!("Message '{}' was added", new_msg.name()),
133 metadata: Default::default(),
134 });
135 }
136 }
137
138 Ok(())
139 }
140
141 fn compare_messages(
143 &self,
144 old_msg: &DescriptorProto,
145 new_msg: &DescriptorProto,
146 path: &str,
147 changes: &mut Vec<SchemaChange>,
148 ) -> Result<()> {
149 self.compare_fields(path, old_msg, new_msg, changes);
150 Ok(())
151 }
152
153 fn compare_fields(
154 &self,
155 path: &str,
156 old_msg: &DescriptorProto,
157 new_msg: &DescriptorProto,
158 changes: &mut Vec<SchemaChange>,
159 ) {
160 for old_field in old_msg.field.iter() {
161 if let Some(new_field) = new_msg.field.iter().find(|f| f.name() == old_field.name()) {
162 if old_field.type_() != new_field.type_() {
163 let mut metadata = HashMap::new();
164 metadata.insert("message".to_string(), old_msg.name().to_string());
165 metadata.insert("field".to_string(), old_field.name().to_string());
166 metadata.insert("old_type".to_string(), format!("{:?}", old_field.type_()));
167 metadata.insert("new_type".to_string(), format!("{:?}", new_field.type_()));
168
169 changes.push(SchemaChange::new(
170 ChangeType::Modification,
171 format!("{}/{}/{}", path, old_msg.name(), old_field.name()),
172 format!(
173 "Field '{}' type changed from {:?} to {:?}",
174 old_field.name(),
175 old_field.type_(),
176 new_field.type_()
177 ),
178 metadata,
179 ));
180 }
181 } else {
182 let mut metadata = HashMap::new();
183 metadata.insert("message".to_string(), old_msg.name().to_string());
184 metadata.insert("field".to_string(), old_field.name().to_string());
185
186 changes.push(SchemaChange::new(
187 ChangeType::Removal,
188 format!("{}/{}/{}", path, old_msg.name(), old_field.name()),
189 format!("Field '{}' was removed", old_field.name()),
190 metadata,
191 ));
192 }
193 }
194
195 for new_field in new_msg.field.iter() {
197 if !old_msg.field.iter().any(|f| f.name() == new_field.name()) {
198 let mut metadata = HashMap::new();
199 metadata.insert("message".to_string(), new_msg.name().to_string());
200 metadata.insert("field".to_string(), new_field.name().to_string());
201
202 changes.push(SchemaChange::new(
203 ChangeType::Addition,
204 format!("{}/{}/{}", path, new_msg.name(), new_field.name()),
205 format!("New field '{}' was added", new_field.name()),
206 metadata,
207 ));
208 }
209 }
210 }
211
212 fn validate_change(&self, change: &SchemaChange) -> Option<CompatibilityIssue> {
214 match change.change_type {
215 ChangeType::Removal => Some(CompatibilityIssue {
216 severity: IssueSeverity::Error,
217 description: format!("Breaking change: {}", change.description),
218 location: change.location.clone(),
219 }),
220 ChangeType::Modification => Some(CompatibilityIssue {
221 severity: IssueSeverity::Warning,
222 description: format!("Potential compatibility issue: {}", change.description),
223 location: change.location.clone(),
224 }),
225 ChangeType::Rename => {
226 todo!("Implement handling for Rename change type");
227 },
228 _ => None,
229 }
230 }
231
232 fn calculate_compatibility_score(&self, changes: &[SchemaChange]) -> i32 {
234 let base_score: i32 = 100;
235 let mut deductions: i32 = 0;
236
237 for change in changes {
238 match change.change_type {
239 ChangeType::Addition => (),
240 ChangeType::Removal => deductions += 20,
241 ChangeType::Modification => deductions += 10,
242 ChangeType::Rename => {
243 todo!("Implement handling for Rename change type");
244 },
245 }
246 }
247
248 base_score.saturating_sub(deductions)
249 }
250}