actr_version/
lib.rs

1//! # Actor-RTC Protocol Compatibility Analysis Library
2//!
3//! A library providing semantic protocol compatibility analysis based on protobuf
4//! schema evolution rules, using proto-sign for professional breaking change detection.
5//!
6//! ## Core Features
7//!
8//! - **Semantic Compatibility Analysis**: Deep protobuf schema compatibility checking
9//! - **Breaking Change Detection**: Identify specific breaking changes between versions
10//! - **Service-Level Comparison**: Compare complete ServiceSpec structures from actr-protocol
11//! - **Stable Fingerprinting**: Semantic fingerprints that ignore formatting
12//!
13//! ## Usage
14//!
15//! ```rust,ignore
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! use actr_version::{ServiceCompatibility, CompatibilityLevel, Fingerprint, ProtoFile};
18//! use actr_protocol::ServiceSpec;
19//!
20//! // Example: Create service with proto content
21//! let proto_files = vec![ProtoFile {
22//!     name: "user.proto".to_string(),
23//!     content: r#"
24//!         syntax = "proto3";
25//!         message User { string name = 1; string email = 2; }
26//!     "#.to_string(),
27//!     path: None,
28//! }];
29//!
30//! let fingerprint = Fingerprint::calculate_service_semantic_fingerprint(&proto_files)?;
31//!
32//! let base_service = ServiceSpec {
33//!     version: "1.0.0".to_string(),
34//!     description: Some("User service".to_string()),
35//!     fingerprint,
36//!     protobufs: proto_files.into_iter().map(|pf| actr_protocol::service_spec::Protobuf {
37//!         uri: format!("actr://user-service/{}", pf.name),
38//!         content: pf.content,
39//!         fingerprint: "file_fp".to_string(),
40//!     }).collect(),
41//! };
42//!
43//! # let candidate_service = base_service.clone();
44//!
45//! // Analyze compatibility between versions
46//! let result = ServiceCompatibility::analyze_compatibility(&base_service, &candidate_service)?;
47//!
48//! match result.level {
49//!     CompatibilityLevel::FullyCompatible => println!("✅ No changes"),
50//!     CompatibilityLevel::BackwardCompatible => println!("⚠️ Backward compatible changes"),
51//!     CompatibilityLevel::BreakingChanges => println!("❌ Breaking changes detected"),
52//! }
53//! # Ok(())
54//! # }
55//! ```
56
57use serde::{Deserialize, Serialize};
58use thiserror::Error;
59
60pub mod compatibility;
61pub mod fingerprint;
62pub mod types;
63
64// Re-export actr-protocol types
65pub use actr_protocol::{ServiceSpec, service_spec::Protobuf as ProtoFileSpec};
66
67// Re-export our specific types
68pub use compatibility::{BreakingChange, CompatibilityAnalysis, ServiceCompatibility};
69pub use fingerprint::Fingerprint;
70pub use types::{CompatibilityLevel, ProtoFile};
71
72/// Detailed compatibility analysis result (Rust-specific, extends proto version)
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct CompatibilityAnalysisResult {
75    /// Overall compatibility level
76    pub level: CompatibilityLevel,
77    /// Detailed list of changes detected
78    pub changes: Vec<ProtocolChange>,
79    /// Breaking changes (subset of changes)
80    pub breaking_changes: Vec<BreakingChange>,
81    /// Semantic fingerprint of base version
82    pub base_semantic_fingerprint: String,
83    /// Semantic fingerprint of candidate version
84    pub candidate_semantic_fingerprint: String,
85    /// Analysis timestamp
86    pub analyzed_at: chrono::DateTime<chrono::Utc>,
87}
88
89impl CompatibilityAnalysisResult {
90    /// Check if services are compatible (not breaking)
91    pub fn is_compatible(&self) -> bool {
92        !matches!(self.level, CompatibilityLevel::BreakingChanges)
93    }
94
95    /// Get a summary string of the analysis
96    pub fn summary(&self) -> String {
97        match self.level {
98            CompatibilityLevel::FullyCompatible => "No changes detected".to_string(),
99            CompatibilityLevel::BackwardCompatible => {
100                format!("{} backward compatible changes", self.changes.len())
101            }
102            CompatibilityLevel::BreakingChanges => {
103                format!("{} breaking changes detected", self.breaking_changes.len())
104            }
105        }
106    }
107}
108
109/// Individual protocol change detected
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ProtocolChange {
112    /// Type of change (e.g., "FIELD_REMOVED", "TYPE_CHANGED")
113    pub change_type: String,
114    /// File where change occurred
115    pub file_name: String,
116    /// Location within the file
117    pub location: String,
118    /// Human-readable description
119    pub description: String,
120    /// Whether this change breaks compatibility
121    pub is_breaking: bool,
122}
123
124/// Errors that can occur during compatibility analysis
125#[derive(Error, Debug)]
126pub enum CompatibilityError {
127    #[error("Failed to parse proto file: {file_name}: {source}")]
128    ProtoParseError {
129        file_name: String,
130        #[source]
131        source: anyhow::Error,
132    },
133
134    #[error("Proto-sign analysis failed: {0}")]
135    ProtoSignError(#[from] anyhow::Error),
136
137    #[error("Service has no proto files: {service_name}")]
138    NoProtoFiles { service_name: String },
139
140    #[error("Invalid service structure: {0}")]
141    InvalidService(String),
142}
143
144pub type Result<T> = std::result::Result<T, CompatibilityError>;
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_compatibility_levels() {
152        // Test that compatibility levels work as expected
153        assert_eq!(
154            CompatibilityLevel::FullyCompatible,
155            CompatibilityLevel::FullyCompatible
156        );
157        assert_ne!(
158            CompatibilityLevel::FullyCompatible,
159            CompatibilityLevel::BackwardCompatible
160        );
161    }
162
163    #[test]
164    fn test_protocol_change_creation() {
165        let change = ProtocolChange {
166            change_type: "FIELD_REMOVED".to_string(),
167            file_name: "user.proto".to_string(),
168            location: "User.email".to_string(),
169            description: "Field 'email' was removed from message 'User'".to_string(),
170            is_breaking: true,
171        };
172
173        assert_eq!(change.change_type, "FIELD_REMOVED");
174        assert!(change.is_breaking);
175    }
176
177    #[test]
178    fn test_compatibility_result_structure() {
179        let result = CompatibilityAnalysisResult {
180            level: CompatibilityLevel::BackwardCompatible,
181            changes: vec![],
182            breaking_changes: vec![],
183            base_semantic_fingerprint: "abc123".to_string(),
184            candidate_semantic_fingerprint: "def456".to_string(),
185            analyzed_at: chrono::Utc::now(),
186        };
187
188        assert_eq!(result.level, CompatibilityLevel::BackwardCompatible);
189        assert_eq!(result.changes.len(), 0);
190    }
191
192    #[test]
193    fn test_compatibility_result_methods() {
194        let breaking_result = CompatibilityAnalysisResult {
195            level: CompatibilityLevel::BreakingChanges,
196            changes: vec![ProtocolChange {
197                change_type: "FIELD_REMOVED".to_string(),
198                file_name: "user.proto".to_string(),
199                location: "User.email".to_string(),
200                description: "Field removed".to_string(),
201                is_breaking: true,
202            }],
203            breaking_changes: vec![BreakingChange {
204                rule: "FIELD_REMOVED".to_string(),
205                file: "user.proto".to_string(),
206                location: "User.email".to_string(),
207                message: "Field email was removed".to_string(),
208            }],
209            base_semantic_fingerprint: "base_fp".to_string(),
210            candidate_semantic_fingerprint: "candidate_fp".to_string(),
211            analyzed_at: chrono::Utc::now(),
212        };
213
214        assert!(!breaking_result.is_compatible());
215        assert!(breaking_result.summary().contains("breaking changes"));
216
217        let compatible_result = CompatibilityAnalysisResult {
218            level: CompatibilityLevel::FullyCompatible,
219            changes: vec![],
220            breaking_changes: vec![],
221            base_semantic_fingerprint: "fp".to_string(),
222            candidate_semantic_fingerprint: "fp".to_string(),
223            analyzed_at: chrono::Utc::now(),
224        };
225
226        assert!(compatible_result.is_compatible());
227        assert!(compatible_result.summary().contains("No changes"));
228    }
229
230    #[test]
231    fn test_compatibility_error_display() {
232        // Test ProtoParseError
233        let parse_error = CompatibilityError::ProtoParseError {
234            file_name: "test.proto".to_string(),
235            source: anyhow::anyhow!("syntax error"),
236        };
237        let error_msg = format!("{parse_error}");
238        assert!(error_msg.contains("test.proto"));
239        assert!(error_msg.contains("syntax error"));
240
241        // Test ProtoSignError
242        let sign_error = CompatibilityError::ProtoSignError(anyhow::anyhow!("proto-sign failed"));
243        let error_msg = format!("{sign_error}");
244        assert!(error_msg.contains("proto-sign failed"));
245
246        // Test NoProtoFiles
247        let no_files_error = CompatibilityError::NoProtoFiles {
248            service_name: "empty-service".to_string(),
249        };
250        let error_msg = format!("{no_files_error}");
251        assert!(error_msg.contains("empty-service"));
252
253        // Test InvalidService
254        let invalid_error = CompatibilityError::InvalidService("missing fields".to_string());
255        let error_msg = format!("{invalid_error}");
256        assert!(error_msg.contains("missing fields"));
257    }
258
259    #[test]
260    fn test_error_conversion() {
261        // Test that anyhow::Error converts to ProtoSignError
262        let anyhow_error = anyhow::anyhow!("some error");
263        let compat_error: CompatibilityError = anyhow_error.into();
264        assert!(matches!(
265            compat_error,
266            CompatibilityError::ProtoSignError(_)
267        ));
268    }
269}