rusty_schema_diff/analyzer/
protobuf.rs

1//! Protobuf specific analyzer implementation
2//!
3//! This module provides functionality for analyzing Protobuf changes and
4//! generating compatibility reports and migration paths.
5
6use 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
13/// Analyzes Protobuf changes and generates compatibility reports.
14pub struct ProtobufAnalyzer;
15
16impl SchemaAnalyzer for ProtobufAnalyzer {
17    /// Analyzes compatibility between two Protobuf versions.
18    ///
19    /// # Arguments
20    ///
21    /// * `old` - The original Protobuf version.
22    /// * `new` - The new Protobuf version to compare against.
23    ///
24    /// # Returns
25    ///
26    /// A `CompatibilityReport` detailing the differences and compatibility status.
27    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    /// Generates a migration path between Protobuf versions.
47    ///
48    /// # Arguments
49    ///
50    /// * `old` - The source Protobuf version.
51    /// * `new` - The target Protobuf version.
52    ///
53    /// # Returns
54    ///
55    /// A `MigrationPlan` detailing the required changes.
56    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    /// Parses protobuf content into a FileDescriptorProto
96    fn parse_proto(&self, content: &str) -> Result<FileDescriptorProto> {
97        // Basic implementation using protobuf parser
98        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    /// Compares two protobuf descriptors
105    fn compare_descriptors(
106        &self,
107        old: &FileDescriptorProto,
108        new: &FileDescriptorProto,
109        path: &str,
110        changes: &mut Vec<SchemaChange>,
111    ) -> Result<()> {
112        // Compare messages
113        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        // Check for new messages
127        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    /// Compares two protobuf messages
142    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        // Check for new fields
196        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    /// Validates a single schema change
213    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    /// Calculates compatibility score for protobuf changes
233    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}