mockforge_core/protocol_abstraction/
matcher.rs

1//! Cross-protocol request matching for caching and replay
2
3use super::{Protocol, ProtocolRequest, RequestMatcher};
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6/// Simple request matcher that matches on operation and path
7pub struct SimpleRequestMatcher;
8
9impl RequestMatcher for SimpleRequestMatcher {
10    fn match_score(&self, _request: &ProtocolRequest) -> f64 {
11        // Base score: operation and path must match exactly
12        1.0
13    }
14
15    fn protocol(&self) -> Protocol {
16        // Supports all protocols
17        Protocol::Http // Return type doesn't matter for cross-protocol matcher
18    }
19}
20
21/// Fuzzy request matcher that considers headers and body
22pub struct FuzzyRequestMatcher {
23    /// Weight for operation match (0.0 to 1.0)
24    pub operation_weight: f64,
25    /// Weight for path match (0.0 to 1.0)
26    pub path_weight: f64,
27    /// Weight for metadata match (0.0 to 1.0)
28    pub metadata_weight: f64,
29    /// Weight for body match (0.0 to 1.0)
30    pub body_weight: f64,
31}
32
33impl Default for FuzzyRequestMatcher {
34    fn default() -> Self {
35        Self {
36            operation_weight: 0.4,
37            path_weight: 0.4,
38            metadata_weight: 0.1,
39            body_weight: 0.1,
40        }
41    }
42}
43
44impl RequestMatcher for FuzzyRequestMatcher {
45    fn match_score(&self, request: &ProtocolRequest) -> f64 {
46        // Fuzzy matching considers multiple factors
47        let mut score = 0.0;
48
49        // Operation match
50        if !request.operation.is_empty() {
51            score += self.operation_weight;
52        }
53
54        // Path match
55        if !request.path.is_empty() {
56            score += self.path_weight;
57        }
58
59        // Metadata match
60        if !request.metadata.is_empty() {
61            score += self.metadata_weight;
62        }
63
64        // Body match
65        if request.body.is_some() {
66            score += self.body_weight;
67        }
68
69        score
70    }
71
72    fn protocol(&self) -> Protocol {
73        Protocol::Http // Supports all protocols
74    }
75}
76
77/// Request fingerprint for caching and replay
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct RequestFingerprint {
80    /// Protocol
81    pub protocol: Protocol,
82    /// Operation hash
83    pub operation_hash: u64,
84    /// Path hash
85    pub path_hash: u64,
86    /// Metadata hash (optional)
87    pub metadata_hash: Option<u64>,
88    /// Body hash (optional)
89    pub body_hash: Option<u64>,
90}
91
92impl RequestFingerprint {
93    /// Create a fingerprint from a protocol request
94    pub fn from_request(request: &ProtocolRequest) -> Self {
95        Self {
96            protocol: request.protocol,
97            operation_hash: Self::hash_string(&request.operation),
98            path_hash: Self::hash_string(&request.path),
99            metadata_hash: if !request.metadata.is_empty() {
100                Some(Self::hash_metadata(&request.metadata))
101            } else {
102                None
103            },
104            body_hash: request.body.as_ref().map(|b| Self::hash_bytes(b)),
105        }
106    }
107
108    /// Create a simple fingerprint (operation + path only)
109    pub fn simple(request: &ProtocolRequest) -> Self {
110        Self {
111            protocol: request.protocol,
112            operation_hash: Self::hash_string(&request.operation),
113            path_hash: Self::hash_string(&request.path),
114            metadata_hash: None,
115            body_hash: None,
116        }
117    }
118
119    /// Hash a string
120    fn hash_string(s: &str) -> u64 {
121        let mut hasher = DefaultHasher::new();
122        s.hash(&mut hasher);
123        hasher.finish()
124    }
125
126    /// Hash bytes
127    fn hash_bytes(bytes: &[u8]) -> u64 {
128        let mut hasher = DefaultHasher::new();
129        bytes.hash(&mut hasher);
130        hasher.finish()
131    }
132
133    /// Hash metadata map
134    fn hash_metadata(metadata: &std::collections::HashMap<String, String>) -> u64 {
135        let mut hasher = DefaultHasher::new();
136        // Sort keys to ensure consistent hashing
137        let mut keys: Vec<&String> = metadata.keys().collect();
138        keys.sort();
139        for key in keys {
140            key.hash(&mut hasher);
141            if let Some(value) = metadata.get(key) {
142                value.hash(&mut hasher);
143            }
144        }
145        hasher.finish()
146    }
147
148    /// Check if this fingerprint matches another
149    pub fn matches(&self, other: &RequestFingerprint) -> bool {
150        self.protocol == other.protocol
151            && self.operation_hash == other.operation_hash
152            && self.path_hash == other.path_hash
153    }
154
155    /// Check if this fingerprint exactly matches another (including metadata and body)
156    pub fn exact_match(&self, other: &RequestFingerprint) -> bool {
157        self == other
158    }
159
160    /// Calculate similarity score (0.0 to 1.0)
161    pub fn similarity(&self, other: &RequestFingerprint) -> f64 {
162        if self.protocol != other.protocol {
163            return 0.0;
164        }
165
166        let mut score = 0.0;
167        let mut factors = 0;
168
169        // Operation match
170        if self.operation_hash == other.operation_hash {
171            score += 1.0;
172        }
173        factors += 1;
174
175        // Path match
176        if self.path_hash == other.path_hash {
177            score += 1.0;
178        }
179        factors += 1;
180
181        // Metadata match (if both have metadata)
182        if let (Some(hash1), Some(hash2)) = (self.metadata_hash, other.metadata_hash) {
183            if hash1 == hash2 {
184                score += 1.0;
185            }
186            factors += 1;
187        }
188
189        // Body match (if both have bodies)
190        if let (Some(hash1), Some(hash2)) = (self.body_hash, other.body_hash) {
191            if hash1 == hash2 {
192                score += 1.0;
193            }
194            factors += 1;
195        }
196
197        score / factors as f64
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use std::collections::HashMap;
205
206    #[test]
207    fn test_simple_matcher() {
208        let matcher = SimpleRequestMatcher;
209        let request = ProtocolRequest {
210            protocol: Protocol::Http,
211            pattern: crate::MessagePattern::RequestResponse,
212            operation: "GET".to_string(),
213            path: "/test".to_string(),
214            topic: None,
215            routing_key: None,
216            partition: None,
217            qos: None,
218            metadata: HashMap::new(),
219            body: None,
220            client_ip: None,
221        };
222
223        assert_eq!(matcher.match_score(&request), 1.0);
224    }
225
226    #[test]
227    fn test_fuzzy_matcher_default() {
228        let matcher = FuzzyRequestMatcher::default();
229        assert_eq!(matcher.operation_weight, 0.4);
230        assert_eq!(matcher.path_weight, 0.4);
231        assert_eq!(matcher.metadata_weight, 0.1);
232        assert_eq!(matcher.body_weight, 0.1);
233    }
234
235    #[test]
236    fn test_fuzzy_matcher_full_request() {
237        let matcher = FuzzyRequestMatcher::default();
238        let request = ProtocolRequest {
239            protocol: Protocol::Http,
240            pattern: crate::MessagePattern::RequestResponse,
241            operation: "GET".to_string(),
242            path: "/test".to_string(),
243            topic: None,
244            routing_key: None,
245            partition: None,
246            qos: None,
247            metadata: {
248                let mut m = HashMap::new();
249                m.insert("content-type".to_string(), "application/json".to_string());
250                m
251            },
252            body: Some(b"{\"test\": true}".to_vec()),
253            client_ip: None,
254        };
255
256        let score = matcher.match_score(&request);
257        assert_eq!(score, 1.0); // All factors present
258    }
259
260    #[test]
261    fn test_request_fingerprint_from_request() {
262        let request = ProtocolRequest {
263            protocol: Protocol::Http,
264            pattern: crate::MessagePattern::RequestResponse,
265            operation: "GET".to_string(),
266            path: "/users".to_string(),
267            topic: None,
268            routing_key: None,
269            partition: None,
270            qos: None,
271            metadata: HashMap::new(),
272            body: None,
273            client_ip: None,
274        };
275
276        let fp = RequestFingerprint::from_request(&request);
277        assert_eq!(fp.protocol, Protocol::Http);
278        assert!(fp.metadata_hash.is_none());
279        assert!(fp.body_hash.is_none());
280    }
281
282    #[test]
283    fn test_request_fingerprint_simple() {
284        let request = ProtocolRequest {
285            protocol: Protocol::Grpc,
286            pattern: crate::MessagePattern::RequestResponse,
287            operation: "greeter.SayHello".to_string(),
288            path: "/greeter.Greeter/SayHello".to_string(),
289            topic: None,
290            routing_key: None,
291            partition: None,
292            qos: None,
293            metadata: {
294                let mut m = HashMap::new();
295                m.insert("grpc-metadata".to_string(), "value".to_string());
296                m
297            },
298            body: Some(b"test".to_vec()),
299            client_ip: None,
300        };
301
302        let fp = RequestFingerprint::simple(&request);
303        assert_eq!(fp.protocol, Protocol::Grpc);
304        assert!(fp.metadata_hash.is_none()); // Simple fingerprint ignores metadata
305        assert!(fp.body_hash.is_none()); // Simple fingerprint ignores body
306    }
307
308    #[test]
309    fn test_fingerprint_matches() {
310        let request1 = ProtocolRequest {
311            protocol: Protocol::GraphQL,
312            pattern: crate::MessagePattern::RequestResponse,
313            operation: "Query.users".to_string(),
314            path: "/graphql".to_string(),
315            topic: None,
316            routing_key: None,
317            partition: None,
318            qos: None,
319            metadata: HashMap::new(),
320            body: None,
321            client_ip: None,
322        };
323
324        let request2 = ProtocolRequest {
325            protocol: Protocol::GraphQL,
326            pattern: crate::MessagePattern::RequestResponse,
327            operation: "Query.users".to_string(),
328            path: "/graphql".to_string(),
329            topic: None,
330            routing_key: None,
331            partition: None,
332            qos: None,
333            metadata: HashMap::new(),
334            body: Some(b"different body".to_vec()),
335            client_ip: None,
336        };
337
338        let fp1 = RequestFingerprint::from_request(&request1);
339        let fp2 = RequestFingerprint::from_request(&request2);
340
341        assert!(fp1.matches(&fp2)); // Matches on protocol, operation, and path
342        assert!(!fp1.exact_match(&fp2)); // Not an exact match due to different body
343    }
344
345    #[test]
346    fn test_fingerprint_similarity() {
347        let request = ProtocolRequest {
348            protocol: Protocol::Http,
349            pattern: crate::MessagePattern::RequestResponse,
350            operation: "GET".to_string(),
351            path: "/test".to_string(),
352            topic: None,
353            routing_key: None,
354            partition: None,
355            qos: None,
356            metadata: HashMap::new(),
357            body: None,
358            client_ip: None,
359        };
360
361        let fp1 = RequestFingerprint::from_request(&request);
362        let fp2 = RequestFingerprint::from_request(&request);
363
364        assert_eq!(fp1.similarity(&fp2), 1.0); // Identical fingerprints
365    }
366
367    #[test]
368    fn test_fingerprint_different_protocol() {
369        let request1 = ProtocolRequest {
370            protocol: Protocol::Http,
371            pattern: crate::MessagePattern::RequestResponse,
372            operation: "GET".to_string(),
373            path: "/test".to_string(),
374            topic: None,
375            routing_key: None,
376            partition: None,
377            qos: None,
378            metadata: HashMap::new(),
379            body: None,
380            client_ip: None,
381        };
382
383        let request2 = ProtocolRequest {
384            protocol: Protocol::Grpc,
385            pattern: crate::MessagePattern::RequestResponse,
386            operation: "GET".to_string(),
387            path: "/test".to_string(),
388            topic: None,
389            routing_key: None,
390            partition: None,
391            qos: None,
392            metadata: HashMap::new(),
393            body: None,
394            client_ip: None,
395        };
396
397        let fp1 = RequestFingerprint::from_request(&request1);
398        let fp2 = RequestFingerprint::from_request(&request2);
399
400        assert_eq!(fp1.similarity(&fp2), 0.0); // Different protocols
401    }
402}