openzeppelin_relayer/repositories/
redis_base.rs

1//! Base Redis repository functionality shared across all Redis implementations.
2//!
3//! This module provides common utilities and patterns used by all Redis repository
4//! implementations to reduce code duplication and ensure consistency.
5
6use crate::models::RepositoryError;
7use log::{error, warn};
8use redis::RedisError;
9use serde::{Deserialize, Serialize};
10
11/// Base trait for Redis repositories providing common functionality
12pub trait RedisRepository {
13    fn serialize_entity<T, F>(
14        &self,
15        entity: &T,
16        id_extractor: F,
17        entity_type: &str,
18    ) -> Result<String, RepositoryError>
19    where
20        T: Serialize,
21        F: Fn(&T) -> &str,
22    {
23        serde_json::to_string(entity).map_err(|e| {
24            let id = id_extractor(entity);
25            error!("Serialization failed for {} {}: {}", entity_type, id, e);
26            RepositoryError::InvalidData(format!(
27                "Failed to serialize {} {}: {}",
28                entity_type, id, e
29            ))
30        })
31    }
32
33    /// Deserialize entity with detailed error context
34    /// Default implementation that works for any Deserialize type
35    fn deserialize_entity<T>(
36        &self,
37        json: &str,
38        entity_id: &str,
39        entity_type: &str,
40    ) -> Result<T, RepositoryError>
41    where
42        T: for<'de> Deserialize<'de>,
43    {
44        serde_json::from_str(json).map_err(|e| {
45            error!(
46                "Deserialization failed for {} {}: {}",
47                entity_type, entity_id, e
48            );
49            RepositoryError::InvalidData(format!(
50                "Failed to deserialize {} {}: {} (JSON length: {})",
51                entity_type,
52                entity_id,
53                e,
54                json.len()
55            ))
56        })
57    }
58
59    /// Convert Redis errors to appropriate RepositoryError types
60    fn map_redis_error(&self, error: RedisError, context: &str) -> RepositoryError {
61        warn!("Redis operation failed in context '{}': {}", context, error);
62
63        match error.kind() {
64            redis::ErrorKind::TypeError => RepositoryError::InvalidData(format!(
65                "Redis data type error in operation '{}': {}",
66                context, error
67            )),
68            redis::ErrorKind::AuthenticationFailed => {
69                RepositoryError::InvalidData("Redis authentication failed".to_string())
70            }
71            redis::ErrorKind::NoScriptError => RepositoryError::InvalidData(format!(
72                "Redis script error in operation '{}': {}",
73                context, error
74            )),
75            redis::ErrorKind::ReadOnly => RepositoryError::InvalidData(format!(
76                "Redis is read-only in operation '{}': {}",
77                context, error
78            )),
79            redis::ErrorKind::ExecAbortError => RepositoryError::InvalidData(format!(
80                "Redis transaction aborted in operation '{}': {}",
81                context, error
82            )),
83            redis::ErrorKind::BusyLoadingError => RepositoryError::InvalidData(format!(
84                "Redis is busy in operation '{}': {}",
85                context, error
86            )),
87            redis::ErrorKind::ExtensionError => RepositoryError::InvalidData(format!(
88                "Redis extension error in operation '{}': {}",
89                context, error
90            )),
91            // Default to Other for connection errors and other issues
92            _ => RepositoryError::Other(format!("Redis operation '{}' failed: {}", context, error)),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use serde::{Deserialize, Serialize};
101
102    // Test structs for serialization/deserialization
103    #[derive(Debug, Serialize, Deserialize, PartialEq)]
104    struct TestEntity {
105        id: String,
106        name: String,
107        value: i32,
108    }
109
110    #[derive(Debug, Serialize, Deserialize, PartialEq)]
111    struct SimpleEntity {
112        id: String,
113    }
114
115    // Test implementation of RedisRepository trait
116    struct TestRedisRepository;
117
118    impl RedisRepository for TestRedisRepository {}
119
120    impl TestRedisRepository {
121        fn new() -> Self {
122            TestRedisRepository
123        }
124    }
125
126    #[test]
127    fn test_serialize_entity_success() {
128        let repo = TestRedisRepository::new();
129        let entity = TestEntity {
130            id: "test-id".to_string(),
131            name: "test-name".to_string(),
132            value: 42,
133        };
134
135        let result = repo.serialize_entity(&entity, |e| &e.id, "TestEntity");
136
137        assert!(result.is_ok());
138        let json = result.unwrap();
139        assert!(json.contains("test-id"));
140        assert!(json.contains("test-name"));
141        assert!(json.contains("42"));
142    }
143
144    #[test]
145    fn test_serialize_entity_with_different_id_extractor() {
146        let repo = TestRedisRepository::new();
147        let entity = TestEntity {
148            id: "test-id".to_string(),
149            name: "test-name".to_string(),
150            value: 42,
151        };
152
153        // Use name as ID extractor
154        let result = repo.serialize_entity(&entity, |e| &e.name, "TestEntity");
155
156        assert!(result.is_ok());
157        let json = result.unwrap();
158
159        // Should still serialize the entire entity
160        assert!(json.contains("test-id"));
161        assert!(json.contains("test-name"));
162        assert!(json.contains("42"));
163    }
164
165    #[test]
166    fn test_serialize_entity_simple_struct() {
167        let repo = TestRedisRepository::new();
168        let entity = SimpleEntity {
169            id: "simple-id".to_string(),
170        };
171
172        let result = repo.serialize_entity(&entity, |e| &e.id, "SimpleEntity");
173
174        assert!(result.is_ok());
175        let json = result.unwrap();
176        assert!(json.contains("simple-id"));
177    }
178
179    #[test]
180    fn test_deserialize_entity_success() {
181        let repo = TestRedisRepository::new();
182        let json = r#"{"id":"test-id","name":"test-name","value":42}"#;
183
184        let result: Result<TestEntity, RepositoryError> =
185            repo.deserialize_entity(json, "test-id", "TestEntity");
186
187        assert!(result.is_ok());
188        let entity = result.unwrap();
189        assert_eq!(entity.id, "test-id");
190        assert_eq!(entity.name, "test-name");
191        assert_eq!(entity.value, 42);
192    }
193
194    #[test]
195    fn test_deserialize_entity_invalid_json() {
196        let repo = TestRedisRepository::new();
197        let invalid_json = r#"{"id":"test-id","name":"test-name","value":}"#; // Missing value
198
199        let result: Result<TestEntity, RepositoryError> =
200            repo.deserialize_entity(invalid_json, "test-id", "TestEntity");
201
202        assert!(result.is_err());
203        match result.unwrap_err() {
204            RepositoryError::InvalidData(msg) => {
205                assert!(msg.contains("Failed to deserialize TestEntity test-id"));
206                assert!(msg.contains("JSON length:"));
207            }
208            _ => panic!("Expected InvalidData error"),
209        }
210    }
211
212    #[test]
213    fn test_deserialize_entity_invalid_structure() {
214        let repo = TestRedisRepository::new();
215        let json = r#"{"wrongfield":"test-id"}"#;
216
217        let result: Result<TestEntity, RepositoryError> =
218            repo.deserialize_entity(json, "test-id", "TestEntity");
219
220        assert!(result.is_err());
221        match result.unwrap_err() {
222            RepositoryError::InvalidData(msg) => {
223                assert!(msg.contains("Failed to deserialize TestEntity test-id"));
224            }
225            _ => panic!("Expected InvalidData error"),
226        }
227    }
228
229    #[test]
230    fn test_map_redis_error_type_error() {
231        let repo = TestRedisRepository::new();
232        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
233
234        let result = repo.map_redis_error(redis_error, "test_operation");
235
236        match result {
237            RepositoryError::InvalidData(msg) => {
238                assert!(msg.contains("Redis data type error"));
239                assert!(msg.contains("test_operation"));
240            }
241            _ => panic!("Expected InvalidData error"),
242        }
243    }
244
245    #[test]
246    fn test_map_redis_error_authentication_failed() {
247        let repo = TestRedisRepository::new();
248        let redis_error = RedisError::from((redis::ErrorKind::AuthenticationFailed, "Auth failed"));
249
250        let result = repo.map_redis_error(redis_error, "auth_operation");
251
252        match result {
253            RepositoryError::InvalidData(msg) => {
254                assert!(msg.contains("Redis authentication failed"));
255            }
256            _ => panic!("Expected InvalidData error"),
257        }
258    }
259
260    #[test]
261    fn test_map_redis_error_connection_error() {
262        let repo = TestRedisRepository::new();
263        let redis_error = RedisError::from((redis::ErrorKind::IoError, "Connection failed"));
264
265        let result = repo.map_redis_error(redis_error, "connection_operation");
266
267        match result {
268            RepositoryError::Other(msg) => {
269                assert!(msg.contains("Redis operation"));
270                assert!(msg.contains("connection_operation"));
271            }
272            _ => panic!("Expected Other error"),
273        }
274    }
275
276    #[test]
277    fn test_map_redis_error_no_script_error() {
278        let repo = TestRedisRepository::new();
279        let redis_error = RedisError::from((redis::ErrorKind::NoScriptError, "Script not found"));
280
281        let result = repo.map_redis_error(redis_error, "script_operation");
282
283        match result {
284            RepositoryError::InvalidData(msg) => {
285                assert!(msg.contains("Redis script error"));
286                assert!(msg.contains("script_operation"));
287            }
288            _ => panic!("Expected InvalidData error"),
289        }
290    }
291
292    #[test]
293    fn test_map_redis_error_read_only() {
294        let repo = TestRedisRepository::new();
295        let redis_error = RedisError::from((redis::ErrorKind::ReadOnly, "Read only"));
296
297        let result = repo.map_redis_error(redis_error, "write_operation");
298
299        match result {
300            RepositoryError::InvalidData(msg) => {
301                assert!(msg.contains("Redis is read-only"));
302                assert!(msg.contains("write_operation"));
303            }
304            _ => panic!("Expected InvalidData error"),
305        }
306    }
307
308    #[test]
309    fn test_map_redis_error_exec_abort_error() {
310        let repo = TestRedisRepository::new();
311        let redis_error =
312            RedisError::from((redis::ErrorKind::ExecAbortError, "Transaction aborted"));
313
314        let result = repo.map_redis_error(redis_error, "transaction_operation");
315
316        match result {
317            RepositoryError::InvalidData(msg) => {
318                assert!(msg.contains("Redis transaction aborted"));
319                assert!(msg.contains("transaction_operation"));
320            }
321            _ => panic!("Expected InvalidData error"),
322        }
323    }
324
325    #[test]
326    fn test_map_redis_error_busy_error() {
327        let repo = TestRedisRepository::new();
328        let redis_error = RedisError::from((redis::ErrorKind::BusyLoadingError, "Server busy"));
329
330        let result = repo.map_redis_error(redis_error, "busy_operation");
331
332        match result {
333            RepositoryError::InvalidData(msg) => {
334                assert!(msg.contains("Redis is busy"));
335                assert!(msg.contains("busy_operation"));
336            }
337            _ => panic!("Expected InvalidData error"),
338        }
339    }
340
341    #[test]
342    fn test_map_redis_error_extension_error() {
343        let repo = TestRedisRepository::new();
344        let redis_error = RedisError::from((redis::ErrorKind::ExtensionError, "Extension error"));
345
346        let result = repo.map_redis_error(redis_error, "extension_operation");
347
348        match result {
349            RepositoryError::InvalidData(msg) => {
350                assert!(msg.contains("Redis extension error"));
351                assert!(msg.contains("extension_operation"));
352            }
353            _ => panic!("Expected InvalidData error"),
354        }
355    }
356
357    #[test]
358    fn test_map_redis_error_context_propagation() {
359        let repo = TestRedisRepository::new();
360        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
361        let context = "user_repository_get_operation";
362
363        let result = repo.map_redis_error(redis_error, context);
364
365        match result {
366            RepositoryError::InvalidData(msg) => {
367                assert!(msg.contains("Redis data type error"));
368                // Context should be used in logging but not necessarily in the error message
369            }
370            _ => panic!("Expected InvalidData error"),
371        }
372    }
373
374    #[test]
375    fn test_serialize_deserialize_roundtrip() {
376        let repo = TestRedisRepository::new();
377        let original = TestEntity {
378            id: "roundtrip-id".to_string(),
379            name: "roundtrip-name".to_string(),
380            value: 123,
381        };
382
383        // Serialize
384        let json = repo
385            .serialize_entity(&original, |e| &e.id, "TestEntity")
386            .unwrap();
387
388        // Deserialize
389        let deserialized: TestEntity = repo
390            .deserialize_entity(&json, "roundtrip-id", "TestEntity")
391            .unwrap();
392
393        // Should be identical
394        assert_eq!(original, deserialized);
395    }
396
397    #[test]
398    fn test_serialize_deserialize_unicode_content() {
399        let repo = TestRedisRepository::new();
400        let original = TestEntity {
401            id: "unicode-id".to_string(),
402            name: "测试名称 🚀".to_string(),
403            value: 456,
404        };
405
406        // Serialize
407        let json = repo
408            .serialize_entity(&original, |e| &e.id, "TestEntity")
409            .unwrap();
410
411        // Deserialize
412        let deserialized: TestEntity = repo
413            .deserialize_entity(&json, "unicode-id", "TestEntity")
414            .unwrap();
415
416        // Should handle unicode correctly
417        assert_eq!(original, deserialized);
418    }
419
420    #[test]
421    fn test_serialize_entity_with_complex_data() {
422        let repo = TestRedisRepository::new();
423
424        #[derive(Serialize)]
425        struct ComplexEntity {
426            id: String,
427            nested: NestedData,
428            list: Vec<i32>,
429        }
430
431        #[derive(Serialize)]
432        struct NestedData {
433            field1: String,
434            field2: bool,
435        }
436
437        let complex_entity = ComplexEntity {
438            id: "complex-id".to_string(),
439            nested: NestedData {
440                field1: "nested-value".to_string(),
441                field2: true,
442            },
443            list: vec![1, 2, 3],
444        };
445
446        let result = repo.serialize_entity(&complex_entity, |e| &e.id, "ComplexEntity");
447
448        assert!(result.is_ok());
449        let json = result.unwrap();
450        assert!(json.contains("complex-id"));
451        assert!(json.contains("nested-value"));
452        assert!(json.contains("true"));
453        assert!(json.contains("[1,2,3]"));
454    }
455
456    // Test specifically for u128 serialization/deserialization with large values
457    #[test]
458    fn test_serialize_deserialize_u128_large_values() {
459        use crate::utils::{deserialize_optional_u128, serialize_optional_u128};
460
461        #[derive(Serialize, Deserialize, PartialEq, Debug)]
462        struct TestU128Entity {
463            id: String,
464            #[serde(
465                serialize_with = "serialize_optional_u128",
466                deserialize_with = "deserialize_optional_u128",
467                default
468            )]
469            gas_price: Option<u128>,
470            #[serde(
471                serialize_with = "serialize_optional_u128",
472                deserialize_with = "deserialize_optional_u128",
473                default
474            )]
475            max_fee_per_gas: Option<u128>,
476        }
477
478        let repo = TestRedisRepository::new();
479
480        // Test with very large u128 values that would overflow JSON numbers
481        let original = TestU128Entity {
482            id: "u128-test".to_string(),
483            gas_price: Some(u128::MAX), // 340282366920938463463374607431768211455
484            max_fee_per_gas: Some(999999999999999999999999999999999u128),
485        };
486
487        // Serialize
488        let json = repo
489            .serialize_entity(&original, |e| &e.id, "TestU128Entity")
490            .unwrap();
491
492        // Verify it contains string representations, not numbers
493        assert!(json.contains("\"340282366920938463463374607431768211455\""));
494        assert!(json.contains("\"999999999999999999999999999999999\""));
495        // Make sure they're not stored as numbers (which would cause overflow)
496        assert!(!json.contains("3.4028236692093846e+38"));
497
498        // Deserialize
499        let deserialized: TestU128Entity = repo
500            .deserialize_entity(&json, "u128-test", "TestU128Entity")
501            .unwrap();
502
503        // Should be identical
504        assert_eq!(original, deserialized);
505        assert_eq!(deserialized.gas_price, Some(u128::MAX));
506        assert_eq!(
507            deserialized.max_fee_per_gas,
508            Some(999999999999999999999999999999999u128)
509        );
510    }
511
512    #[test]
513    fn test_serialize_deserialize_u128_none_values() {
514        use crate::utils::{deserialize_optional_u128, serialize_optional_u128};
515
516        #[derive(Serialize, Deserialize, PartialEq, Debug)]
517        struct TestU128Entity {
518            id: String,
519            #[serde(
520                serialize_with = "serialize_optional_u128",
521                deserialize_with = "deserialize_optional_u128",
522                default
523            )]
524            gas_price: Option<u128>,
525        }
526
527        let repo = TestRedisRepository::new();
528
529        // Test with None values
530        let original = TestU128Entity {
531            id: "u128-none-test".to_string(),
532            gas_price: None,
533        };
534
535        // Serialize
536        let json = repo
537            .serialize_entity(&original, |e| &e.id, "TestU128Entity")
538            .unwrap();
539
540        // Should contain null
541        assert!(json.contains("null"));
542
543        // Deserialize
544        let deserialized: TestU128Entity = repo
545            .deserialize_entity(&json, "u128-none-test", "TestU128Entity")
546            .unwrap();
547
548        // Should be identical
549        assert_eq!(original, deserialized);
550        assert_eq!(deserialized.gas_price, None);
551    }
552}